mirror of
https://github.com/evennia/evennia.git
synced 2026-03-31 13:07:16 +02:00
Merge branch 'develop' into godot-client
This commit is contained in:
commit
d043e1e934
274 changed files with 19891 additions and 4324 deletions
|
|
@ -7,10 +7,12 @@ on:
|
|||
branches: [ master, develop ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'evennia/contrib/**'
|
||||
pull_request:
|
||||
branches: [ master, develop ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'evennia/contrib/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
|
|||
22
CHANGELOG.md
22
CHANGELOG.md
|
|
@ -160,18 +160,26 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
|
|||
- Attribute storage support defaultdics (Hendher)
|
||||
- Add ObjectParent mixin to default game folder template as an easy, ready-made
|
||||
way to override features on all ObjectDB-inheriting objects easily.
|
||||
source location, mimicking behavior of `at_pre_move` hook - returning False will abort move.
|
||||
- Add `TagProperty`, `AliasProperty` and `PermissionProperty` to assign these
|
||||
data in a similar way to django fields.
|
||||
- New `at_pre_object_receive(obj, source_location)` method on Objects. Called on
|
||||
destination, mimicking behavior of `at_pre_move` hook - returning False will abort move.
|
||||
- New `at_pre_object_leave(obj, destination)` method on Objects. Called on
|
||||
- The db pickle-serializer now checks for methods `__serialize_dbobjs__` and `__deserialize_dbobjs__`
|
||||
to allow custom packing/unpacking of nested dbobjs, to allow storing in Attribute.
|
||||
- Optimizations to rpsystem contrib performance. Breaking change: `.get_sdesc()` will
|
||||
now return `None` instead of `.db.desc` if no sdesc is set; fallback in hook (inspectorCaracal)
|
||||
- Reworked text2html parser to avoid problems with stateful color tags (inspectorCaracal)
|
||||
- Simplified `EvMenu.options_formatter` hook to use `EvColumn` and f-strings (inspectorcaracal)
|
||||
- Allow `# CODE`, `# HEADER` etc as well as `#CODE`/`#HEADER` in batchcode
|
||||
files - this works better with black linting.
|
||||
- Added `move_type` str kwarg to `move_to()` calls, optionally identifying the type of
|
||||
move being done ('teleport', 'disembark', 'give' etc). (volund)
|
||||
- Made RPSystem contrib msg calls pass `pose` or `say` as msg-`type` for use in
|
||||
e.g. webclient pane filtering where desired. (volund)
|
||||
- Added `Account.uses_screenreader(session=None)` as a quick shortcut for
|
||||
finding if a user uses a screenreader (and adjust display accordingly).
|
||||
- Fixed bug in `cmdset.remove()` where a command could not be deleted by `key`,
|
||||
even though doc suggested one could (ChrisLR)
|
||||
- New contrib `name_generator` for building random real-world based or fantasy-names
|
||||
|
|
@ -184,7 +192,19 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
|
|||
- Contrib `buffs` for managing temporary and permanent RPG status buffs effects (tegiminis)
|
||||
- New `at_server_init()` hook called before all other startup hooks for all
|
||||
startup modes. Used for more generic overriding (volund)
|
||||
|
||||
- New `search` lock type used to completely hide an object from being found by
|
||||
the `DefaultObject.search` (`caller.search`) method. (CloudKeeper)
|
||||
- Change setting `MULTISESSION_MODE` to now only control sessions, not how many
|
||||
characters can be puppeted simultaneously. New settings now control that.
|
||||
- Add new setting `AUTO_CREATE_CHARACTER_WITH_ACCOUNT`, a boolean deciding if
|
||||
the new account should also get a matching character (legacy MUD style).
|
||||
- Add new setting `AUTO_PUPPET_ON_LOGIN`, boolean deciding if one should
|
||||
automatically puppet the last/available character on connection (legacy MUD style)
|
||||
- Add new setting `MAX_NR_SIMULTANEUS_PUPPETS` - how many puppets the account
|
||||
can run at the same time. Used to limit multi-playing.
|
||||
- Make setting `MAX_NR_CHARACTERS` interact better with the new settings above.
|
||||
- Allow `$search` funcparser func to search tags and to accept kwargs for more
|
||||
powerful searches passed into the regular search functions.
|
||||
|
||||
## Evennia 0.9.5
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ COPY . /usr/src/evennia
|
|||
|
||||
# add the game source when rebuilding a new docker image from inside
|
||||
# a game dir
|
||||
ONBUILD COPY . /usr/src/game
|
||||
ONBUILD COPY --chown=evennia . /usr/src/game
|
||||
|
||||
# make the game source hierarchy persistent with a named volume.
|
||||
# mount on-disk game location here when using the container
|
||||
|
|
|
|||
|
|
@ -160,15 +160,49 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
|
|||
- Attribute storage support defaultdics (Hendher)
|
||||
- Add ObjectParent mixin to default game folder template as an easy, ready-made
|
||||
way to override features on all ObjectDB-inheriting objects easily.
|
||||
source location, mimicking behavior of `at_pre_move` hook - returning False will abort move.
|
||||
- Add `TagProperty`, `AliasProperty` and `PermissionProperty` to assign these
|
||||
data in a similar way to django fields.
|
||||
- New `at_pre_object_receive(obj, source_location)` method on Objects. Called on
|
||||
destination, mimicking behavior of `at_pre_move` hook - returning False will abort move.
|
||||
- New `at_pre_object_leave(obj, destination)` method on Objects. Called on
|
||||
- The db pickle-serializer now checks for methods `__serialize_dbobjs__` and `__deserialize_dbobjs__`
|
||||
to allow custom packing/unpacking of nested dbobjs, to allow storing in Attribute.
|
||||
- Optimizations to rpsystem contrib performance. Breaking change: `.get_sdesc()` will
|
||||
now return `None` instead of `.db.desc` if no sdesc is set; fallback in hook (inspectorCaracal)
|
||||
- Reworked text2html parser to avoid problems with stateful color tags (inspectorCaracal)
|
||||
- Simplified `EvMenu.options_formatter` hook to use `EvColumn` and f-strings (inspectorcaracal)
|
||||
|
||||
- Allow `# CODE`, `# HEADER` etc as well as `#CODE`/`#HEADER` in batchcode
|
||||
files - this works better with black linting.
|
||||
- Added `move_type` str kwarg to `move_to()` calls, optionally identifying the type of
|
||||
move being done ('teleport', 'disembark', 'give' etc). (volund)
|
||||
- Made RPSystem contrib msg calls pass `pose` or `say` as msg-`type` for use in
|
||||
e.g. webclient pane filtering where desired. (volund)
|
||||
- Added `Account.uses_screenreader(session=None)` as a quick shortcut for
|
||||
finding if a user uses a screenreader (and adjust display accordingly).
|
||||
- Fixed bug in `cmdset.remove()` where a command could not be deleted by `key`,
|
||||
even though doc suggested one could (ChrisLR)
|
||||
- New contrib `name_generator` for building random real-world based or fantasy-names
|
||||
based on phonetic rules.
|
||||
- Enable proper serialization of dict subclasses in Attributes (aogier)
|
||||
- `object.search` fuzzy-matching now uses `icontains` instead of `istartswith`
|
||||
to better match how search works elsewhere (volund)
|
||||
- The `.at_traverse` hook now receives a `exit_obj` kwarg, linking back to the
|
||||
exit triggering the hook (volund)
|
||||
- Contrib `buffs` for managing temporary and permanent RPG status buffs effects (tegiminis)
|
||||
- New `at_server_init()` hook called before all other startup hooks for all
|
||||
startup modes. Used for more generic overriding (volund)
|
||||
- New `search` lock type used to completely hide an object from being found by
|
||||
the `DefaultObject.search` (`caller.search`) method. (CloudKeeper)
|
||||
- Change setting `MULTISESSION_MODE` to now only control sessions, not how many
|
||||
characters can be puppeted simultaneously. New settings now control that.
|
||||
- Add new setting `AUTO_CREATE_CHARACTER_WITH_ACCOUNT`, a boolean deciding if
|
||||
the new account should also get a matching character (legacy MUD style).
|
||||
- Add new setting `AUTO_PUPPET_ON_LOGIN`, boolean deciding if one should
|
||||
automatically puppet the last/available character on connection (legacy MUD style)
|
||||
- Add new setting `MAX_NR_SIMULTANEUS_PUPPETS` - how many puppets the account
|
||||
can run at the same time. Used to limit multi-playing.
|
||||
- Make setting `MAX_NR_CHARACTERS` interact better with the new settings above.
|
||||
|
||||
## Evennia 0.9.5
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ you only the beginning or some part of it, it covers much of the things needed t
|
|||
|
||||
Evennia is developed using Python. Even if you are more of a designer than a coder, it is wise to
|
||||
learn how to read and understand basic Python code. If you are new to Python, or need a refresher,
|
||||
take a look at our [Python introduction](../Howtos/Beginner-Tutorial/Part1/Python-basic-introduction.md).
|
||||
take a look at our [Python introduction](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Python-basic-introduction.md).
|
||||
|
||||
## Explore Evennia interactively
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ using such a checker can be a good start to weed out the simple problems.
|
|||
|
||||
## Plan before you code
|
||||
|
||||
Before you start coding away at your dream game, take a look at our [Game Planning](../Howtos/Beginner-Tutorial/Part2/Game-Planning.md)
|
||||
Before you start coding away at your dream game, take a look at our [Game Planning](../Howtos/Beginner-Tutorial/Part2/Beginner-Tutorial-Game-Planning.md)
|
||||
page. It might hopefully help you avoid some common pitfalls and time sinks.
|
||||
|
||||
## Code in your game folder, not in the evennia/ repository
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ unexpected bug.
|
|||
If you have implemented your own tests for your game you can run them from your game dir
|
||||
with
|
||||
|
||||
evennia test .
|
||||
evennia test --settings settings.py .
|
||||
|
||||
The period (`.`) means to run all tests found in the current directory and all subdirectories. You
|
||||
could also specify, say, `typeclasses` or `world` if you wanted to just run tests in those subdirs.
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ on. The tutorial world included with Evennia showcases a dark room that replaces
|
|||
commands with its own versions because the Character cannot see.
|
||||
|
||||
If you want a quick start into defining your first commands and using them with command sets, you
|
||||
can head over to the [Adding Command Tutorial](../Howtos/Beginner-Tutorial/Part1/Adding-Commands.md) which steps through things
|
||||
can head over to the [Adding Command Tutorial](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md) which steps through things
|
||||
without the explanations.
|
||||
|
||||
## Defining Command Sets
|
||||
|
|
@ -112,7 +112,7 @@ back even if all other cmdsets fail or are removed. It is always persistent and
|
|||
by `cmdset.delete()`. To remove a default cmdset you must explicitly call `cmdset.remove_default()`.
|
||||
|
||||
Command sets are often added to an object in its `at_object_creation` method. For more examples of
|
||||
adding commands, read the [Step by step tutorial](../Howtos/Beginner-Tutorial/Part1/Adding-Commands.md). Generally you can
|
||||
adding commands, read the [Step by step tutorial](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md). Generally you can
|
||||
customize which command sets are added to your objects by using `self.cmdset.add()` or
|
||||
`self.cmdset.add_default()`.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@
|
|||
|
||||
See also:
|
||||
- [Default Commands](./Default-Commands.md)
|
||||
- [Adding Command Tutorial](../Howtos/Beginner-Tutorial/Part1/Adding-Commands.md)
|
||||
- [Adding Command Tutorial](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ object in various ways. Consider a "Tree" object with a cmdset defining the comm
|
|||
|
||||
This page goes into full detail about how to use Commands. To fully use them you must also read the
|
||||
page detailing [Command Sets](./Command-Sets.md). There is also a step-by-step
|
||||
[Adding Command Tutorial](../Howtos/Beginner-Tutorial/Part1/Adding-Commands.md) that will get you started quickly without the
|
||||
[Adding Command Tutorial](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md) that will get you started quickly without the
|
||||
extra explanations.
|
||||
|
||||
## Defining Commands
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ after
|
|||
start node as if it was entered on a fictional previous node. This can be very useful in order to
|
||||
start a menu differently depending on the Command's arguments in which it was initialized.
|
||||
- `session` (Session): Useful when calling the menu from an [Account](./Accounts.md) in
|
||||
`MULTISESSION_MODDE` higher than 2, to make sure only the right Session sees the menu output.
|
||||
`MULTISESSION_MODE` higher than 2, to make sure only the right Session sees the menu output.
|
||||
- `debug` (bool): If set, the `menudebug` command will be made available in the menu. Use it to
|
||||
list the current state of the menu and use `menudebug <variable>` to inspect a specific state
|
||||
variable from the list.
|
||||
|
|
|
|||
|
|
@ -105,7 +105,16 @@ something like `call:false()`.
|
|||
- `examine` - who may examine this object's properties.
|
||||
- `delete` - who may delete the object.
|
||||
- `edit` - who may edit properties and attributes of the object.
|
||||
- `view` - if the `look` command will display/list this object
|
||||
- `view` - if the `look` command will display/list this object in descriptions
|
||||
and if you will be able to see its description. Note that if
|
||||
you target it specifically by name, the system will still find it, just
|
||||
not be able to look at it. See `search` lock to completely hide the item.
|
||||
- `search` - this controls if the object can be found with the
|
||||
`DefaultObject.search` method (usually referred to with `caller.search`
|
||||
in Commands). This is how to create entirely 'undetectable' in-game objects.
|
||||
If not setting this lock excplicitly, all objects are assumed searchable.
|
||||
Note that if you are aiming to make some _permanently invisible game system,
|
||||
using a [Script](./Scripts.md) is a better bet.
|
||||
- `get`- who may pick up the object and carry it around.
|
||||
- `puppet` - who may "become" this object and control it as their "character".
|
||||
- `attrcreate` - who may create new attributes on the object (default True)
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ The default protfuncs available out of the box are defined in `evennia/prototype
|
|||
override the ones available, just add the same-named function in your own protfunc module.
|
||||
|
||||
| Protfunc | Description |
|
||||
|
||||
| --- | --- |
|
||||
| `$random()` | Returns random value in range [0, 1) |
|
||||
| `$randint(start, end)` | Returns random value in range [start, end] |
|
||||
| `$left_justify(<text>)` | Left-justify text |
|
||||
|
|
@ -224,13 +224,10 @@ override the ones available, just add the same-named function in your own protfu
|
|||
| `$mult(<value1>, <value2>)` | Returns value1 * value2 |
|
||||
| `$div(<value1>, <value2>)` | Returns value2 / value1 |
|
||||
| `$toint(<value>)` | Returns value converted to integer (or value if not possible) |
|
||||
| `$eval(<code>)` | Returns result of [literal-
|
||||
eval](https://docs.python.org/2/library/ast.html#ast.literal_eval) of code string. Only simple
|
||||
python expressions. |
|
||||
| `$obj(<query>)` | Returns object #dbref searched globally by key, tag or #dbref. Error if more
|
||||
than one found." |
|
||||
| `$eval(<code>)` | Returns result of [literal-eval](https://docs.python.org/2/library/ast.html#ast.literal_eval) of code string. Only simple python expressions. |
|
||||
| `$obj(<query>)` | Returns object #dbref searched globally by key, tag or #dbref. Error if more than one found. |
|
||||
| `$objlist(<query>)` | Like `$obj`, except always returns a list of zero, one or more results. |
|
||||
| `$dbref(dbref)` | Returns argument if it is formed as a #dbref (e.g. #1234), otherwise error.
|
||||
| `$dbref(dbref)` | Returns argument if it is formed as a #dbref (e.g. #1234), otherwise error. |
|
||||
|
||||
For developers with access to Python, using protfuncs in prototypes is generally not useful. Passing
|
||||
real Python functions is a lot more powerful and flexible. Their main use is to allow in-game
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Evennia comes with a MUD client accessible from a normal web browser. During development you can try
|
||||
it at `http://localhost:4001/webclient`. The client consists of several parts, all under
|
||||
`evennia/web/webclient/`:
|
||||
`evennia/web`:
|
||||
|
||||
`templates/webclient/webclient.html` and `templates/webclient/base.html` are the very simplistic
|
||||
django html templates describing the webclient layout.
|
||||
|
|
@ -18,7 +18,7 @@ be used also if swapping out the gui front end.
|
|||
various plugins, and uses the Evennia object library for all in/out.
|
||||
|
||||
`static/webclient/js/plugins` provides a default set of plugins that implement a "telnet-like"
|
||||
interface.
|
||||
interface, and a couple of example plugins to show how you could implement new plugin features.
|
||||
|
||||
`static/webclient/css/webclient.css` is the CSS file for the client; it also defines things like how
|
||||
to display ANSI/Xterm256 colors etc.
|
||||
|
|
@ -30,17 +30,17 @@ these.
|
|||
## Customizing the web client
|
||||
|
||||
Like was the case for the website, you override the webclient from your game directory. You need to
|
||||
add/modify a file in the matching directory location within one of the _overrides directories.
|
||||
These _override directories are NOT directly used by the web server when the game is running, the
|
||||
server copies everything web related in the Evennia folder over to `mygame/web/static/` and then
|
||||
copies in all of your _overrides. This can cause some cases were you edit a file, but it doesn't
|
||||
add/modify a file in the matching directory locations within your project's `mygame/web/` directories.
|
||||
These directories are NOT directly used by the web server when the game is running, the
|
||||
server copies everything web related in the Evennia folder over to `mygame/server/.static/` and then
|
||||
copies in all of your `mygame/web/` files. This can cause some cases were you edit a file, but it doesn't
|
||||
seem to make any difference in the servers behavior. **Before doing anything else, try shutting
|
||||
down the game and running `evennia collectstatic` from the command line then start it back up, clear
|
||||
your browser cache, and see if your edit shows up.**
|
||||
|
||||
Example: To change the utilized plugin list, you need to override base.html by copying
|
||||
`evennia/web/webclient/templates/webclient/base.html` to
|
||||
`mygame/web/template_overrides/webclient/base.html` and editing it to add your new plugin.
|
||||
Example: To change the list of in-use plugins, you need to override base.html by copying
|
||||
`evennia/web/templates/webclient/base.html` to
|
||||
`mygame/web/templates/webclient/base.html` and editing it to add your new plugin.
|
||||
|
||||
# Evennia Web Client API (from evennia.js)
|
||||
* `Evennia.init( opts )`
|
||||
|
|
@ -96,6 +96,8 @@ manager for drag-n-drop windows, text routing and more.
|
|||
keys to peruse.
|
||||
* `hotbuttons.js` Defines onGotOptions. A Disabled-by-default plugin that defines a button bar with
|
||||
user-assignable commands.
|
||||
* `html.js` A basic plugin to allow the client to handle "raw html" messages from the server, this
|
||||
allows the server to send native HTML messages like >div style='s'<styled text>/div<
|
||||
* `iframe.js` Defines onOptionsUI. A goldenlayout-only plugin to create a restricted browsing sub-
|
||||
window for a side-by-side web/text interface, mostly an example of how to build new HTML
|
||||
"components" for goldenlayout.
|
||||
|
|
@ -108,8 +110,50 @@ from the server and display them as inline HTML.
|
|||
while the tab is hidden.
|
||||
* `oob.js` Defines onSend. Allows the user to test/send Out Of Band json messages to the server.
|
||||
* `options.js` Defines most callbacks. Provides a popup-based UI to coordinate options settings with the server.
|
||||
* `options2.js` Defines most callbacks. Provides a goldenlayout-based version of the options/settings tab. Integrates with other plugins via the custom onOptionsUI callback.
|
||||
* `options2.js` Defines most callbacks. Provides a goldenlayout-based version of the options/settings tab.
|
||||
Integrates with other plugins via the custom onOptionsUI callback.
|
||||
* `popups.js` Provides default popups/Dialog UI for other plugins to use.
|
||||
* `text2html.js` Provides a new message handler type: `text2html`, similar to the multimedia and html
|
||||
plugins. This plugin provides a way to offload rendering the regular pipe-styled ASCII messages
|
||||
to the client. This allows the server to do less work, while also allowing the client a place to
|
||||
customize this conversion process. To use this plugin you will need to override the current commands
|
||||
in Evennia, changing any place where a raw text output message is generated and turn it into a
|
||||
`text2html` message. For example: `target.msg("my text")` becomes: `target.msg(text2html=("my text"))`
|
||||
(even better, use a webclient pane routing tag: `target.msg(text2html=("my text", {"type": "sometag"}))`)
|
||||
`text2html` messages should format and behave identically to the server-side generated text2html() output.
|
||||
|
||||
# A side note on html messages vrs text2html messages
|
||||
|
||||
So...lets say you have a desire to make your webclient output more like standard webpages...
|
||||
For telnet clients, you could collect a bunch of text lines together, with ASCII formatted borders, etc.
|
||||
Then send the results to be rendered client-side via the text2html plugin.
|
||||
|
||||
But for webclients, you could format a message directly with the html plugin to render the whole thing as an
|
||||
HTML table, like so:
|
||||
```
|
||||
# Server Side Python Code:
|
||||
|
||||
if target.is_webclient():
|
||||
# This can be styled however you like using CSS, just add the CSS file to web/static/webclient/css/...
|
||||
table = [
|
||||
"<table>",
|
||||
"<tr><td>1</td><td>2</td><td>3</td></tr>",
|
||||
"<tr><td>4</td><td>5</td><td>6</td></tr>",
|
||||
"</table>"
|
||||
]
|
||||
target.msg( html=( "".join(table), {"type": "mytag"}) )
|
||||
else:
|
||||
# This will use the client to render this as "plain, simple" ASCII text, the same
|
||||
# as if it was rendered server-side via the Portal's text2html() functions
|
||||
table = [
|
||||
"#############",
|
||||
"# 1 # 2 # 3 #",
|
||||
"#############",
|
||||
"# 4 # 5 # 6 #",
|
||||
"#############"
|
||||
]
|
||||
target.msg( html2html=( "\n".join(table), {"type": "mytag"}) )
|
||||
```
|
||||
|
||||
# Writing your own Plugins
|
||||
|
||||
|
|
@ -131,7 +175,7 @@ output and the one starting input window. This is done by modifying your server
|
|||
goldenlayout_default_config.js.
|
||||
|
||||
Start by creating a new
|
||||
`mygame/web/static_overrides/webclient/js/plugins/goldenlayout_default_config.js` file, and adding
|
||||
`mygame/web/static/webclient/js/plugins/goldenlayout_default_config.js` file, and adding
|
||||
the following JSON variable:
|
||||
|
||||
```
|
||||
|
|
@ -222,7 +266,7 @@ type="text/javascript"></script>
|
|||
Remember, plugins are load-order dependent, so make sure the new `<script>` tag comes before the
|
||||
goldenlayout.js
|
||||
|
||||
Next, create a new plugin file `mygame/web/static_overrides/webclient/js/plugins/myplugin.js` and
|
||||
Next, create a new plugin file `mygame/web/static/webclient/js/plugins/myplugin.js` and
|
||||
edit it.
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ to edit the source code.
|
|||
Language-translations are done by volunteers, so support can vary a lot
|
||||
depending on when a given language was last updated. Below are all languages
|
||||
(besides English) with some level of support. Generally, any language not
|
||||
updated after May 2021 will be missing some translations.
|
||||
updated after Sept 2022 will be missing some translations.
|
||||
|
||||
```{eval-rst}
|
||||
|
||||
|
|
@ -27,11 +27,11 @@ updated after May 2021 will be missing some translations.
|
|||
+---------------+----------------------+--------------+
|
||||
| pl | Polish | Feb 2019 |
|
||||
+---------------+----------------------+--------------+
|
||||
| pt | Portugese | Dec 2015 |
|
||||
| pt | Portugese | Oct 2022 |
|
||||
+---------------+----------------------+--------------+
|
||||
| ru | Russian | Apr 2020 |
|
||||
+---------------+----------------------+--------------+
|
||||
| sv | Swedish | June 2021 |
|
||||
| sv | Swedish | Sep 2022 |
|
||||
+---------------+----------------------+--------------+
|
||||
| zh | Chinese (simplified) | May 2019 |
|
||||
+---------------+----------------------+--------------+
|
||||
|
|
@ -58,21 +58,21 @@ the server to activate i18n.
|
|||
|
||||
```{important}
|
||||
|
||||
Even for a 'fully translated' language you will still see English text
|
||||
in many places when you start Evennia. This is because we expect you (the
|
||||
developer) to know English (you are reading this manual after all). So we
|
||||
translate *hard-coded strings that the end player may see* - things you
|
||||
can't easily change from your mygame/ folder. Outputs from Commands and
|
||||
Typeclasses are generally *not* translated, nor are console/log outputs.
|
||||
Even for a 'fully translated' language you will still see English text
|
||||
in many places when you start Evennia. This is because we expect you (the
|
||||
developer) to know English (you are reading this manual after all). So we
|
||||
translate *hard-coded strings that the end player may see* - things you
|
||||
can't easily change from your mygame/ folder. Outputs from Commands and
|
||||
Typeclasses are generally *not* translated, nor are console/log outputs.
|
||||
|
||||
```
|
||||
|
||||
```{sidebar} Windows users
|
||||
|
||||
If you get errors concerning `gettext` or `xgettext` on Windows,
|
||||
see the `Django documentation <https://docs.djangoproject.com/en/3.2/topics/i18n/translation/#gettext-on-windows>`_
|
||||
A self-installing and up-to-date version of gettext for Windows (32/64-bit) is
|
||||
available on `Github <https://github.com/mlocati/gettext-iconv-windows>`_
|
||||
If you get errors concerning `gettext` or `xgettext` on Windows,
|
||||
see the [Django documentation](https://docs.djangoproject.com/en/3.2/topics/i18n/translation/#gettext-on-windows).
|
||||
A self-installing and up-to-date version of gettext for Windows (32/64-bit) is
|
||||
available on Github as [gettext-iconv-windows](https://github.com/mlocati/gettext-iconv-windows).
|
||||
|
||||
```
|
||||
|
||||
|
|
@ -150,10 +150,9 @@ sentence delimiter (if that makes sense in your language).
|
|||
|
||||
Finally, try to get a feel for who a string is for. If a special technical term
|
||||
is used it may be more confusing than helpful to translate it, even if it's
|
||||
outside of a `{...}` tag. Even though the result is a mix of your language and
|
||||
English, clarity is more important. Many languages may also use the English term
|
||||
normally and reaching for a translation may make the result sound awkward
|
||||
instead.
|
||||
outside of a `{...}` tag. A mix of English and your language may be clearer
|
||||
than you forcing some ad-hoc translation for a term everyone usually reads in
|
||||
English anyway.
|
||||
|
||||
Original: "\nError loading cmdset: No cmdset class '{classname}' in '{path}'.
|
||||
\n(Traceback was logged {timestamp})"
|
||||
|
|
|
|||
440
docs/source/Contribs/Contrib-Buffs.md
Normal file
440
docs/source/Contribs/Contrib-Buffs.md
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
# Buffs
|
||||
|
||||
Contribution by Tegiminis 2022
|
||||
|
||||
A buff is a timed object, attached to a game entity. It is capable of modifying values, triggering code, or both.
|
||||
It is a common design pattern in RPGs, particularly action games.
|
||||
|
||||
Features:
|
||||
|
||||
- `BuffHandler`: A buff handler to apply to your objects.
|
||||
- `BaseBuff`: A buff class to extend from to create your own buffs.
|
||||
- `BuffableProperty`: A sample property class to show how to automatically check modifiers.
|
||||
- `CmdBuff`: A command which applies buffs.
|
||||
- `samplebuffs.py`: Some sample buffs to learn from.
|
||||
|
||||
## Quick Start
|
||||
Assign the handler to a property on the object, like so.
|
||||
|
||||
```python
|
||||
@lazy_property
|
||||
def buffs(self) -> BuffHandler:
|
||||
return BuffHandler(self)
|
||||
```
|
||||
|
||||
You may then call the handler to add or manipulate buffs like so: `object.buffs`. See **Using the Handler**.
|
||||
|
||||
### Customization
|
||||
|
||||
If you want to customize the handler, you can feed the constructor two arguments:
|
||||
- `dbkey`: The string you wish to use as the attribute key for the buff database. Defaults to "buffs". This allows you to keep separate buff pools - for example, "buffs" and "perks".
|
||||
- `autopause`: If you want this handler to automatically pause playtime buffs when its owning object is unpuppeted.
|
||||
|
||||
> **Note**: If you enable autopausing, you MUST initialize the property in your owning object's
|
||||
> `at_init` hook. Otherwise, a hot reload can cause playtime buffs to not update properly
|
||||
> on puppet/unpuppet. You have been warned!
|
||||
|
||||
Let's say you want another handler for an object, `perks`, which has a separate database and
|
||||
respects playtime buffs. You'd assign this new property as so:
|
||||
|
||||
```python
|
||||
class BuffableObject(Object):
|
||||
@lazy_property
|
||||
def perks(self) -> BuffHandler:
|
||||
return BuffHandler(self, dbkey='perks', autopause=True)
|
||||
|
||||
def at_init(self):
|
||||
self.perks
|
||||
```
|
||||
|
||||
## Using the Handler
|
||||
|
||||
Here's how to make use of your new handler.
|
||||
|
||||
### Apply a Buff
|
||||
|
||||
Call the handler's `add` method. This requires a class reference, and also contains a number of
|
||||
optional arguments to customize the buff's duration, stacks, and so on. You can also store any arbitrary value
|
||||
in the buff's cache by passing a dictionary through the `to_cache` optional argument. This will not overwrite the normal
|
||||
values on the cache.
|
||||
|
||||
```python
|
||||
self.buffs.add(StrengthBuff) # A single stack of StrengthBuff with normal duration
|
||||
self.buffs.add(DexBuff, stacks=3, duration=60) # Three stacks of DexBuff, with a duration of 60 seconds
|
||||
self.buffs.add(ReflectBuff, to_cache={'reflect': 0.5}) # A single stack of ReflectBuff, with an extra cache value
|
||||
```
|
||||
|
||||
Two important attributes on the buff are checked when the buff is applied: `refresh` and `unique`.
|
||||
- `refresh` (default: True) determines if a buff's timer is refreshed when it is reapplied.
|
||||
- `unique` (default: True) determines if this buff is unique; that is, only one of it exists on the object.
|
||||
|
||||
The combination of these two booleans creates one of three kinds of keys:
|
||||
- `Unique is True, Refresh is True/False`: The buff's default key.
|
||||
- `Unique is False, Refresh is True`: The default key mixed with the applier's dbref. This makes the buff "unique-per-player", so you can refresh through reapplication.
|
||||
- `Unique is False, Refresh is False`: The default key mixed with a randomized number.
|
||||
|
||||
### Get Buffs
|
||||
|
||||
The handler has several getter methods which return instanced buffs. You won't need to use these for basic functionality, but if you want to manipulate
|
||||
buffs after application, they are very useful. The handler's `check`/`trigger` methods utilize some of these getters, while others are just for developer convenience.
|
||||
|
||||
`get(key)` is the most basic getter. It returns a single buff instance, or `None` if the buff doesn't exist on the handler. It is also the only getter
|
||||
that returns a single buff instance, rather than a dictionary.
|
||||
|
||||
> **Note**: The handler method `has(buff)` allows you to check if a matching key (if a string) or buff class (if a class) is present on the handler cache, without actually instantiating the buff. You should use this method for basic "is this buff present?" checks.
|
||||
|
||||
Group getters, listed below, return a dictionary of values in the format `{buffkey: instance}`. If you want to iterate over all of these buffs,
|
||||
you should do so via the `dict.values()` method.
|
||||
|
||||
- `get_all()` returns all buffs on this handler. You can also use the `handler.all` property.
|
||||
- `get_by_type(BuffClass)` returns buffs of the specified type.
|
||||
- `get_by_stat(stat)` returns buffs with a `Mod` object of the specified `stat` string in their `mods` list.
|
||||
- `get_by_trigger(string)` returns buffs with the specified string in their `triggers` list.
|
||||
- `get_by_source(Object)` returns buffs applied by the specified `source` object.
|
||||
- `get_by_cachevalue(key, value)` returns buffs with the matching `key: value` pair in their cache. `value` is optional.
|
||||
|
||||
All group getters besides `get_all()` can "slice" an existing dictionary through the optional `to_filter` argument.
|
||||
|
||||
```python
|
||||
dict1 = handler.get_by_type(Burned) # This finds all "Burned" buffs on the handler
|
||||
dict2 = handler.get_by_source(self, to_filter=dict1) # This filters dict1 to find buffs with the matching source
|
||||
```
|
||||
|
||||
> **Note**: Most of these getters also have an associated handler property. For example, `handler.effects` returns all buffs that can be triggered, which
|
||||
> is then iterated over by the `get_by_trigger` method.
|
||||
|
||||
### Remove Buffs
|
||||
|
||||
There are also a number of remover methods. Generally speaking, these follow the same format as the getters.
|
||||
|
||||
- `remove(key)` removes the buff with the specified key.
|
||||
- `clear()` removes all buffs.
|
||||
- `remove_by_type(BuffClass)` removes buffs of the specified type.
|
||||
- `remove_by_stat(stat)` removes buffs with a `Mod` object of the specified `stat` string in their `mods` list.
|
||||
- `remove_by_trigger(string)` removes buffs with the specified string in their `triggers` list.
|
||||
- `remove_by_source(Object)` removes buffs applied by the specified source
|
||||
- `remove_by_cachevalue(key, value)` removes buffs with the matching `key: value` pair in their cache. `value` is optional.
|
||||
|
||||
You can also remove a buff by calling the instance's `remove` helper method. You can do this on the dictionaries returned by the
|
||||
getters listed above.
|
||||
|
||||
```python
|
||||
to_remove = handler.get_by_trigger(trigger) # Finds all buffs with the specified trigger
|
||||
for buff in to_remove.values(): # Removes all buffs in the to_remove dictionary via helper methods
|
||||
buff.remove()
|
||||
```
|
||||
|
||||
### Check Modifiers
|
||||
|
||||
Call the handler `check(value, stat)` method when you want to see the modified value.
|
||||
This will return the `value`, modified by any relevant buffs on the handler's owner (identified by
|
||||
the `stat` string).
|
||||
|
||||
For example, let's say you want to modify how much damage you take. That might look something like this:
|
||||
|
||||
```python
|
||||
# The method we call to damage ourselves
|
||||
def take_damage(self, source, damage):
|
||||
_damage = self.buffs.check(damage, 'taken_damage')
|
||||
self.db.health -= _damage
|
||||
```
|
||||
|
||||
This method calls the `at_pre_check` and `at_post_check` methods at the relevant points in the process. You can use to this make
|
||||
buffs that are reactive to being checked; for example, removing themselves, altering their values, or interacting with the game state.
|
||||
|
||||
> **Note**: You can also trigger relevant buffs at the same time as you check them by ensuring the optional argument `trigger` is True in the `check` method.
|
||||
|
||||
Modifiers are calculated additively - that is, all modifiers of the same type are added together before being applied. They are then
|
||||
applied through the following formula.
|
||||
|
||||
```python
|
||||
(base + total_add) / max(1, 1.0 + total_div) * max(0, 1.0 + total_mult)
|
||||
```
|
||||
|
||||
#### Multiplicative Buffs (Advanced)
|
||||
|
||||
Multiply/divide modifiers in this buff system are additive by default. This means that two +50% modifiers will equal a +100% modifier. But what if you want to apply mods multiplicatively?
|
||||
|
||||
First, you should carefully consider if you truly want multiplicative modifiers. Here's some things to consider.
|
||||
|
||||
- They are unintuitive to the average user, as two +50% damage buffs equal +125% instead of +100%.
|
||||
- They lead to "power explosion", where stacking buffs in the right way can turn characters into unstoppable forces
|
||||
|
||||
Doing purely-additive multipliers allows you to better control the balance of your game. Conversely, doing multiplicative multipliers enables very fun build-crafting where smart usage of buffs and skills can turn you into a one-shot powerhouse. Each has its place.
|
||||
|
||||
The best design practice for multiplicative buffs is to divide your multipliers into "tiers", where each tier is applied separately. You can easily do this with multiple `check` calls.
|
||||
|
||||
```python
|
||||
damage = damage
|
||||
damage = handler.check(damage, 'damage')
|
||||
damage = handler.check(damage, 'empower')
|
||||
damage = handler.check(damage, 'radiant')
|
||||
damage = handler.check(damage, 'overpower')
|
||||
```
|
||||
|
||||
#### Buff Strength Priority (Advanced)
|
||||
|
||||
Sometimes you only want to apply the strongest modifier to a stat. This is supported by the optional `strongest` bool arg in the handler's check method
|
||||
|
||||
```python
|
||||
def take_damage(self, source, damage):
|
||||
_damage = self.buffs.check(damage, 'taken_damage', strongest=True)
|
||||
self.db.health -= _damage
|
||||
```
|
||||
|
||||
### Trigger Buffs
|
||||
|
||||
Call the handler's `trigger(string)` method when you want an event call. This will call the `at_trigger` hook method on all buffs with the relevant trigger `string`.
|
||||
|
||||
For example, let's say you want to trigger a buff to "detonate" when you hit your target with an attack.
|
||||
You'd write a buff that might look like this:
|
||||
|
||||
```python
|
||||
class Detonate(BaseBuff):
|
||||
...
|
||||
triggers = ['take_damage']
|
||||
def at_trigger(self, trigger, *args, **kwargs)
|
||||
self.owner.take_damage(100)
|
||||
self.remove()
|
||||
```
|
||||
|
||||
And then call `handler.trigger('take_damage')` in the method you use to take damage.
|
||||
|
||||
> **Note** You could also do this through mods and `at_post_check` if you like, depending on how to want to add the damage.
|
||||
|
||||
### Ticking
|
||||
|
||||
Ticking buffs are slightly special. They are similar to trigger buffs in that they run code, but instead of
|
||||
doing so on an event trigger, they do so on a periodic tick. A common use case for a buff like this is a poison,
|
||||
or a heal over time.
|
||||
|
||||
```python
|
||||
class Poison(BaseBuff):
|
||||
...
|
||||
tickrate = 5
|
||||
def at_tick(self, initial=True, *args, **kwargs):
|
||||
_dmg = self.dmg * self.stacks
|
||||
if not initial:
|
||||
self.owner.location.msg_contents(
|
||||
"Poison courses through {actor}'s body, dealing {damage} damage.".format(
|
||||
actor=self.owner.named, damage=_dmg
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
To make a buff ticking, ensure the `tickrate` is 1 or higher, and it has code in its `at_tick`
|
||||
method. Once you add it to the handler, it starts ticking!
|
||||
|
||||
> **Note**: Ticking buffs always tick on initial application, when `initial` is `True`. If you don't want your hook to fire at that time,
|
||||
> make sure to check the value of `initial` in your `at_tick` method.
|
||||
|
||||
### Context
|
||||
|
||||
Every important handler method optionally accepts a `context` dictionary.
|
||||
|
||||
Context is an important concept for this handler. Every method which checks, triggers, or ticks a buff passes this
|
||||
dictionary (default: empty) to the buff hook methods as keyword arguments (`**kwargs`). It is used for nothing else. This allows you to make those
|
||||
methods "event-aware" by storing relevant data in the dictionary you feed to the method.
|
||||
|
||||
For example, let's say you want a "thorns" buff which damages enemies that attack you. Let's take our `take_damage` method
|
||||
and add a context to the mix.
|
||||
|
||||
```python
|
||||
def take_damage(attacker, damage):
|
||||
context = {'attacker': attacker, 'damage': damage}
|
||||
_damage = self.buffs.check(damage, 'taken_damage', context=context)
|
||||
self.buffs.trigger('taken_damage', context=context)
|
||||
self.db.health -= _damage
|
||||
```
|
||||
Now we use the values that context passes to the buff kwargs to customize our logic.
|
||||
```python
|
||||
class ThornsBuff(BaseBuff):
|
||||
...
|
||||
triggers = ['taken_damage']
|
||||
# This is the hook method on our thorns buff
|
||||
def at_trigger(self, trigger, attacker=None, damage=0, **kwargs):
|
||||
if not attacker:
|
||||
return
|
||||
attacker.db.health -= damage * 0.2
|
||||
```
|
||||
Apply the buff, take damage, and watch the thorns buff do its work!
|
||||
|
||||
### Viewing
|
||||
|
||||
There are two helper methods on the handler that allow you to get useful buff information back.
|
||||
|
||||
- `view`: Returns a dictionary of tuples in the format `{buffkey: (buff.name, buff.flavor)}`. Finds all buffs by default, but optionally accepts a dictionary of buffs to filter as well. Useful for basic buff readouts.
|
||||
- `view_modifiers(stat)`: Returns a nested dictionary of information on modifiers that affect the specified stat. The first layer is the modifier type (`add/mult/div`) and the second layer is the value type (`total/strongest`). Does not return the buffs that cause these modifiers, just the modifiers themselves (akin to using `handler.check` but without actually modifying a value). Useful for stat sheets.
|
||||
|
||||
You can also create your own custom viewing methods through the various handler getters, which will always return the entire buff object.
|
||||
|
||||
## Creating New Buffs
|
||||
|
||||
Creating a new buff is very easy: extend `BaseBuff` into a new class, and fill in all the relevant buff details.
|
||||
However, there are a lot of individual moving parts to a buff. Here's a step-through of the important stuff.
|
||||
|
||||
### Basics
|
||||
|
||||
Regardless of any other functionality, all buffs have the following class attributes:
|
||||
|
||||
- They have customizable `key`, `name`, and `flavor` strings.
|
||||
- They have a `duration` (float), and automatically clean-up at the end. Use -1 for infinite duration, and 0 to clean-up immediately. (default: -1)
|
||||
- They have a `tickrate` (float), and automatically tick if it is greater than 1 (default: 0)
|
||||
- They can stack, if `maxstacks` (int) is not equal to 1. If it's 0, the buff stacks forever. (default: 1)
|
||||
- They can be `unique` (bool), which determines if they have a unique namespace or not. (default: True)
|
||||
- They can `refresh` (bool), which resets the duration when stacked or reapplied. (default: True)
|
||||
- They can be `playtime` (bool) buffs, where duration only counts down during active play. (default: False)
|
||||
|
||||
Buffs also have a few useful properties:
|
||||
|
||||
- `owner`: The object this buff is attached to
|
||||
- `ticknum`: How many ticks the buff has gone through
|
||||
- `timeleft`: How much time is remaining on the buff
|
||||
- `ticking`/`stacking`: If this buff ticks/stacks (checks `tickrate` and `maxstacks`)
|
||||
|
||||
#### Buff Cache (Advanced)
|
||||
|
||||
Buffs always store some useful mutable information about themselves in the cache (what is stored on the owning object's database attribute). A buff's cache corresponds to `{buffkey: buffcache}`, where `buffcache` is a dictionary containing __at least__ the information below:
|
||||
|
||||
- `ref` (class): The buff class path we use to construct the buff.
|
||||
- `start` (float): The timestamp of when the buff was applied.
|
||||
- `source` (Object): If specified; this allows you to track who or what applied the buff.
|
||||
- `prevtick` (float): The timestamp of the previous tick.
|
||||
- `duration` (float): The cached duration. This can vary from the class duration, depending on if the duration has been modified (paused, extended, shortened, etc).
|
||||
- `tickrate` (float): The buff's tick rate. Cannot go below 0. Altering the tickrate on an applied buff will not cause it to start ticking if it wasn't ticking before. (`pause` and `unpause` to start/stop ticking on existing buffs)
|
||||
- `stacks` (int): How many stacks they have.
|
||||
- `paused` (bool): Paused buffs do not clean up, modify values, tick, or fire any hook methods.
|
||||
|
||||
Sometimes you will want to dynamically update a buff's cache at runtime, such as changing a tickrate in a hook method, or altering a buff's duration.
|
||||
You can do so by using the interface `buff.cachekey`. As long as the attribute name matches a key in the cache dictionary, it will update the stored
|
||||
cache with the new value.
|
||||
|
||||
If there is no matching key, it will do nothing. If you wish to add a new key to the cache, you must use the `buff.update_cache(dict)` method,
|
||||
which will properly update the cache (including adding new keys) using the dictionary provided.
|
||||
|
||||
> **Example**: You want to increase a buff's duration by 30 seconds. You use `buff.duration += 30`. This new duration is now reflected on both the instance and the cache.
|
||||
|
||||
The buff cache can also store arbitrary information. To do so, pass a dictionary through the handler `add` method (`handler.add(BuffClass, to_cache=dict)`),
|
||||
set the `cache` dictionary attribute on your buff class, or use the aforementioned `buff.update_cache(dict)` method.
|
||||
|
||||
> **Example**: You store `damage` as a value in the buff cache and use it for your poison buff. You want to increase it over time, so you use `buff.damage += 1` in the tick method.
|
||||
|
||||
### Modifiers
|
||||
|
||||
Mods are stored in the `mods` list attribute. Buffs which have one or more Mod objects in them can modify stats. You can use the handler method to check all
|
||||
mods of a specific stat string and apply their modifications to the value; however, you are encouraged to use `check` in a getter/setter, for easy access.
|
||||
|
||||
Mod objects consist of only four values, assigned by the constructor in this order:
|
||||
|
||||
- `stat`: The stat you want to modify. When `check` is called, this string is used to find all the mods that are to be collected.
|
||||
- `mod`: The modifier. Defaults are `add` (addition/subtraction), `mult` (multiply), and `div` (divide). Modifiers are calculated additively (see `_calculate_mods` for more)
|
||||
- `value`: How much value the modifier gives regardless of stacks
|
||||
- `perstack`: How much value the modifier grants per stack, **INCLUDING** the first. (default: 0)
|
||||
|
||||
The most basic way to add a Mod to a buff is to do so in the buff class definition, like this:
|
||||
|
||||
```python
|
||||
class DamageBuff(BaseBuff):
|
||||
mods = [Mod('damage', 'add', 10)]
|
||||
```
|
||||
|
||||
No mods applied to the value are permanent in any way. All calculations are done at runtime, and the mod values are never stored
|
||||
anywhere except on the buff in question. In other words: you don't need to track the origin of particular stat mods, and you will
|
||||
never permanently change a stat modified by a buff. To remove the modification, simply remove the buff from the object.
|
||||
|
||||
> **Note**: You can add your own modifier types by overloading the `_calculate_mods` method, which contains the basic modifier application logic.
|
||||
|
||||
#### Generating Mods (Advanced)
|
||||
|
||||
An advanced way to do mods is to generate them when the buff is initialized. This lets you create mods on the fly that are reactive to the game state.
|
||||
|
||||
```python
|
||||
class GeneratedStatBuff(BaseBuff):
|
||||
...
|
||||
def at_init(self, *args, **kwargs) -> None:
|
||||
# Finds our "modgen" cache value, and generates a mod from it
|
||||
modgen = list(self.cache.get("modgen"))
|
||||
if modgen:
|
||||
self.mods = [Mod(*modgen)]
|
||||
```
|
||||
|
||||
### Triggers
|
||||
|
||||
Buffs which have one or more strings in the `triggers` attribute can be triggered by events.
|
||||
|
||||
When the handler's `trigger` method is called, it searches all buffs on the handler for any with a matchingtrigger,
|
||||
then calls their `at_trigger` hooks. Buffs can have multiple triggers, and you can tell which trigger was used by
|
||||
the `trigger` argument in the hook.
|
||||
|
||||
```python
|
||||
class AmplifyBuff(BaseBuff):
|
||||
triggers = ['damage', 'heal']
|
||||
|
||||
def at_trigger(self, trigger, **kwargs):
|
||||
if trigger == 'damage': print('Damage trigger called!')
|
||||
if trigger == 'heal': print('Heal trigger called!')
|
||||
```
|
||||
|
||||
### Ticking
|
||||
|
||||
A buff which ticks isn't much different than one which triggers. You're still executing arbitrary hooks on
|
||||
the buff class. To tick, the buff must have a `tickrate` of 1 or higher.
|
||||
|
||||
```python
|
||||
class Poison(BaseBuff):
|
||||
...
|
||||
# this buff will tick 6 times between application and cleanup.
|
||||
duration = 30
|
||||
tickrate = 5
|
||||
def at_tick(self, initial, **kwargs):
|
||||
self.owner.take_damage(10)
|
||||
```
|
||||
> **Note**: The buff always ticks once when applied. For this **first tick only**, `initial` will be True in the `at_tick` hook method. `initial` will be False on subsequent ticks.
|
||||
|
||||
Ticks utilize a persistent delay, so they should be pickleable. As long as you are not adding new properties to your buff class, this shouldn't be a concern.
|
||||
If you **are** adding new properties, try to ensure they do not end up with a circular code path to their object or handler, as this will cause pickling errors.
|
||||
|
||||
### Extras
|
||||
|
||||
Buffs have a grab-bag of extra functionality to let you add complexity to your designs.
|
||||
|
||||
#### Conditionals
|
||||
|
||||
You can restrict whether or not the buff will `check`, `trigger`, or `tick` through defining the `conditional` hook. As long
|
||||
as it returns a "truthy" value, the buff will apply itself. This is useful for making buffs dependent on game state - for
|
||||
example, if you want a buff that makes the player take more damage when they are on fire:
|
||||
|
||||
```python
|
||||
class FireSick(BaseBuff):
|
||||
...
|
||||
def conditional(self, *args, **kwargs):
|
||||
if self.owner.buffs.has(FireBuff):
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
Conditionals for `check`/`trigger` are checked when the buffs are gathered by the handler methods for the respective operations. `Tick`
|
||||
conditionals are checked each tick.
|
||||
|
||||
#### Helper Methods
|
||||
|
||||
Buff instances have a number of helper methods.
|
||||
|
||||
- `remove`/`dispel`: Allows you to remove or dispel the buff. Calls `at_remove`/`at_dispel`, depending on optional arguments.
|
||||
- `pause`/`unpause`: Pauses and unpauses the buff. Calls `at_pause`/`at_unpause`.
|
||||
- `reset`: Resets the buff's start to the current time; same as "refreshing" it.
|
||||
- `alter_cache`: Updates the buff's cache with the `{key:value}` pairs in the provided dictionary. Can overwrite default values, so be careful!
|
||||
|
||||
#### Playtime Duration
|
||||
|
||||
If your handler has `autopause` enabled, any buffs with truthy `playtime` value will automatically pause
|
||||
and unpause when the object the handler is attached to is puppetted or unpuppetted. This even works with ticking buffs,
|
||||
although if you have less than 1 second of tick duration remaining, it will round up to 1s.
|
||||
|
||||
> **Note**: If you want more control over this process, you can comment out the signal subscriptions on the handler and move the autopause logic
|
||||
> to your object's `at_pre/post_puppet/unpuppet` hooks.
|
||||
|
||||
----
|
||||
|
||||
<small>This document page is generated from `evennia/contrib/rpg/buffs/README.md`. Changes to this
|
||||
file will be overwritten, so edit that file rather than this one.</small>
|
||||
125
docs/source/Contribs/Contrib-Character-Creator.md
Normal file
125
docs/source/Contribs/Contrib-Character-Creator.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Character Creator contrib
|
||||
|
||||
Commands for managing and initiating an in-game character-creation menu.
|
||||
|
||||
Contribution by InspectorCaracal, 2022
|
||||
|
||||
## Installation
|
||||
|
||||
In your game folder `commands/default_cmdsets.py`, import and add
|
||||
`ContribCmdCharCreate` to your `AccountCmdSet`.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from evennia.contrib.rpg.character_creator.character_creator import ContribCmdCharCreate
|
||||
|
||||
class AccountCmdSet(default_cmds.AccountCmdSet):
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
super().at_cmdset_creation()
|
||||
self.add(ContribCmdCharCreate)
|
||||
```
|
||||
|
||||
In your game folder `typeclasses/accounts.py`, import and inherit from `ContribChargenAccount`
|
||||
on your Account class.
|
||||
|
||||
(Alternatively, you can copy the `at_look` method directly into your own class.)
|
||||
|
||||
### Example:
|
||||
|
||||
```python
|
||||
from evennia.contrib.rpg.character_creator.character_creator import ContribChargenAccount
|
||||
|
||||
class Account(ContribChargenAccount):
|
||||
# your Account class code
|
||||
```
|
||||
|
||||
In your settings file `server/conf/settings.py`, add the following settings:
|
||||
|
||||
```python
|
||||
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = False
|
||||
AUTO_PUPPET_ON_LOGIN = False
|
||||
```
|
||||
|
||||
(If you want to allow players to create more than one character, you can
|
||||
customize that with the setting `MAX_NR_CHARACTERS`.)
|
||||
|
||||
By default, the new `charcreate` command will reference the example menu
|
||||
provided by the contrib, so you can test it out before building your own menu.
|
||||
You can reference
|
||||
[the example menu here](github:develop/evennia/contrib/rpg/character_creator/example_menu.py) for
|
||||
ideas on how to build your own.
|
||||
|
||||
Once you have your own menu, just add it to your settings to use it. e.g. if your menu is in
|
||||
`mygame/word/chargen_menu.py`, you'd add the following to your settings file:
|
||||
|
||||
```python
|
||||
CHARGEN_MENU = "world.chargen_menu"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### The EvMenu
|
||||
|
||||
In order to use the contrib, you will need to create your own chargen EvMenu.
|
||||
The included `example_menu.py` gives a number of useful menu node techniques
|
||||
with basic attribute examples for you to reference. It can be run as-is as a
|
||||
tutorial for yourself/your devs, or used as base for your own menu.
|
||||
|
||||
The example menu includes code, tips, and instructions for the following types
|
||||
of decision nodes:
|
||||
|
||||
#### Informational Pages
|
||||
|
||||
A small set of nodes that let you page through information on different choices before committing to one.
|
||||
|
||||
#### Option Categories
|
||||
|
||||
A pair of nodes which let you divide an arbitrary number of options into separate categories.
|
||||
|
||||
The base node has a list of categories as the options, and the child node displays the actual character choices.
|
||||
|
||||
#### Multiple Choice
|
||||
|
||||
Allows players to select and deselect options from the list in order to choose more than one.
|
||||
|
||||
#### Starting Objects
|
||||
|
||||
Allows players to choose from a selection of starting objects, which are then created on chargen completion.
|
||||
|
||||
#### Choosing a Name
|
||||
|
||||
The contrib assumes the player will choose their name during character creation,
|
||||
so the necessary code for doing so is of course included!
|
||||
|
||||
|
||||
### `charcreate` command
|
||||
|
||||
The contrib overrides the character creation command - `charcreate` - to use a
|
||||
character creator menu, as well as supporting exiting/resuming the process. In
|
||||
addition, unlike the core command, it's designed for the character name to be
|
||||
chosen later on via the menu, so it won't parse any arguments passed to it.
|
||||
|
||||
### Changes to `Account.at_look`
|
||||
|
||||
The contrib version works mostly the same as core evennia, but adds an
|
||||
additional check to recognize an in-progress character. If you've modified your
|
||||
own `at_look` hook, it's an easy addition to make: just add this section to the
|
||||
playable character list loop.
|
||||
|
||||
```python
|
||||
for char in characters:
|
||||
# contrib code starts here
|
||||
if char.db.chargen_step:
|
||||
# currently in-progress character; don't display placeholder names
|
||||
result.append("\n - |Yin progress|n (|wcharcreate|n to continue)")
|
||||
continue
|
||||
# the rest of your code continues here
|
||||
```
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
<small>This document page is generated from `evennia/contrib/rpg/character_creator/README.md`. Changes to this
|
||||
file will be overwritten, so edit that file rather than this one.</small>
|
||||
|
|
@ -200,7 +200,7 @@ in-game command:
|
|||
In code we would do
|
||||
|
||||
```python
|
||||
from evennia.contrub.crafting.crafting import craft
|
||||
from evennia.contrib.crafting.crafting import craft
|
||||
puppet = craft(crafter, "wooden puppet", knife, wood)
|
||||
|
||||
```
|
||||
|
|
|
|||
36
docs/source/Contribs/Contrib-Evadventure.md
Normal file
36
docs/source/Contribs/Contrib-Evadventure.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# EvAdventure
|
||||
|
||||
Contrib by Griatch 2022
|
||||
|
||||
A complete example MUD using Evennia. This is the final result of what is
|
||||
implemented if you follow the Getting-Started tutorial. It's recommended
|
||||
that you follow the tutorial step by step and write your own code. But if
|
||||
you prefer you can also pick apart or use this as a starting point for your
|
||||
own game.
|
||||
|
||||
## Features
|
||||
|
||||
- Uses a MUD-version of the [Knave](https://rpggeek.com/rpg/50827/knave) old-school
|
||||
fantasy ruleset by Ben Milton (classless and overall compatible with early
|
||||
edition D&D), released under the Creative Commons Attribution (all uses,
|
||||
including commercial are allowed
|
||||
as long as attribution is given).
|
||||
- Character creation using an editable character sheet
|
||||
- Weapons, effects, healing and resting
|
||||
- Two alternative combat systems (turn-based and twitch based)
|
||||
- Magic (three spells)
|
||||
- NPC/mobs with simple AI.
|
||||
- Simple Quest system.
|
||||
- Small game world.
|
||||
- Coded using best Evennia practices, with unit tests.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
TODO
|
||||
|
||||
|
||||
----
|
||||
|
||||
<small>This document page is generated from `evennia/contrib/tutorials/evadventure/README.md`. Changes to this
|
||||
file will be overwritten, so edit that file rather than this one.</small>
|
||||
|
|
@ -26,7 +26,7 @@ class CharacterCmdset(default_cmds.Character_CmdSet):
|
|||
|
||||
```
|
||||
|
||||
Then reload to make the bew commands available. Note that they only work
|
||||
Then reload to make the new commands available. Note that they only work
|
||||
on rooms with the typeclass `ExtendedRoom`. Create new rooms with the right
|
||||
typeclass or use the `typeclass` command to swap existing rooms.
|
||||
|
||||
|
|
|
|||
57
docs/source/Contribs/Contrib-Ingame-Map-Display.md
Normal file
57
docs/source/Contribs/Contrib-Ingame-Map-Display.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Basic Map
|
||||
|
||||
Contribution - helpme 2022
|
||||
|
||||
This adds an ascii `map` to a given room which can be viewed with the `map` command.
|
||||
You can easily alter it to add special characters, room colors etc. The map shown is
|
||||
dynamically generated on use, and supports all compass directions and up/down. Other
|
||||
directions are ignored.
|
||||
|
||||
If you don't expect the map to be updated frequently, you could choose to save the
|
||||
calculated map as a .ndb value on the room and render that instead of running mapping
|
||||
calculations anew each time.
|
||||
|
||||
## Installation:
|
||||
|
||||
Adding the `MapDisplayCmdSet` to the default character cmdset will add the `map` command.
|
||||
|
||||
Specifically, in `mygame/commands/default_cmdsets.py`:
|
||||
|
||||
```python
|
||||
...
|
||||
from evennia.contrib.grid.ingame_map_display import MapDisplayCmdSet # <---
|
||||
|
||||
class CharacterCmdset(default_cmds.Character_CmdSet):
|
||||
...
|
||||
def at_cmdset_creation(self):
|
||||
...
|
||||
self.add(MapDisplayCmdSet) # <---
|
||||
|
||||
```
|
||||
|
||||
Then `reload` to make the new commands available.
|
||||
|
||||
## Settings:
|
||||
|
||||
In order to change your default map size, you can add to `mygame/server/settings.py`:
|
||||
|
||||
```python
|
||||
BASIC_MAP_SIZE = 5 # This changes the default map width/height.
|
||||
|
||||
```
|
||||
|
||||
## Features:
|
||||
|
||||
### ASCII map (and evennia supports UTF-8 characters and even emojis)
|
||||
|
||||
This produces an ASCII map for players of configurable size.
|
||||
|
||||
### New command
|
||||
|
||||
- `CmdMap` - view the map
|
||||
|
||||
|
||||
----
|
||||
|
||||
<small>This document page is generated from `evennia/contrib/grid/ingame_map_display/README.md`. Changes to this
|
||||
file will be overwritten, so edit that file rather than this one.</small>
|
||||
282
docs/source/Contribs/Contrib-Name-Generator.md
Normal file
282
docs/source/Contribs/Contrib-Name-Generator.md
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
# Random Name Generator
|
||||
|
||||
Contribution by InspectorCaracal (2022)
|
||||
|
||||
A module for generating random names, both real-world and fantasy. Real-world
|
||||
names can be generated either as first (personal) names, family (last) names, or
|
||||
full names (first, optional middles, and last). The name data is from [Behind the Name](https://www.behindthename.com/)
|
||||
and used under the [CC BY-SA 4.0 license](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||
|
||||
Fantasy names are generated from basic phonetic rules, using CVC syllable syntax.
|
||||
|
||||
Both real-world and fantasy name generation can be extended to include additional
|
||||
information via your game's `settings.py`
|
||||
|
||||
## Installation
|
||||
|
||||
This is a stand-alone utility. Just import this module (`from evennia.contrib.utils import name_generator`) and use its functions wherever you like.
|
||||
|
||||
## Usage
|
||||
|
||||
Import the module where you need it with the following:
|
||||
```py
|
||||
from evennia.contrib.utils.name_generator import namegen
|
||||
```
|
||||
|
||||
By default, all of the functions will return a string with one generated name.
|
||||
If you specify more than one, or pass `return_list=True` as a keyword argument, the returned value will be a list of strings.
|
||||
|
||||
The module is especially useful for naming newly-created NPCs, like so:
|
||||
```py
|
||||
npc_name = namegen.full_name()
|
||||
npc_obj = create_object(key=npc_name, typeclass="typeclasses.characters.NPC")
|
||||
```
|
||||
|
||||
## Available Settings
|
||||
|
||||
These settings can all be defined in your game's `server/conf/settings.py` file.
|
||||
|
||||
- `NAMEGEN_FIRST_NAMES` adds a new list of first (personal) names.
|
||||
- `NAMEGEN_LAST_NAMES` adds a new list of last (family) names.
|
||||
- `NAMEGEN_REPLACE_LISTS` - set to `True` if you want to use only the names defined in your settings.
|
||||
- `NAMEGEN_FANTASY_RULES` lets you add new phonetic rules for generating entirely made-up names. See the section "Custom Fantasy Name style rules" for details on how this should look.
|
||||
|
||||
Examples:
|
||||
```py
|
||||
NAMEGEN_FIRST_NAMES = [
|
||||
("Evennia", 'mf'),
|
||||
("Green Tea", 'f'),
|
||||
]
|
||||
|
||||
NAMEGEN_LAST_NAMES = [ "Beeblebrox", "Son of Odin" ]
|
||||
|
||||
NAMEGEN_FANTASY_RULES = {
|
||||
"example_style": {
|
||||
"syllable": "(C)VC",
|
||||
"consonants": [ 'z','z','ph','sh','r','n' ],
|
||||
"start": ['m'],
|
||||
"end": ['x','n'],
|
||||
"vowels": [ "e","e","e","a","i","i","u","o", ],
|
||||
"length": (2,4),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Generating Real Names
|
||||
|
||||
The contrib offers three functions for generating random real-world names:
|
||||
`first_name()`, `last_name()`, and `full_name()`. If you want more than one name
|
||||
generated at once, you can use the `num` keyword argument to specify how many.
|
||||
|
||||
Example:
|
||||
```
|
||||
>>> namegen.first_name(num=5)
|
||||
['Genesis', 'Tali', 'Budur', 'Dominykas', 'Kamau']
|
||||
>>> namegen.first_name(gender='m')
|
||||
'Blanchard'
|
||||
```
|
||||
|
||||
The `first_name` function also takes a `gender` keyword argument to filter names
|
||||
by gender association. 'f' for feminine, 'm' for masculine, 'mf' for feminine
|
||||
_and_ masculine, or the default `None` to match any gendering.
|
||||
|
||||
The `full_name` function also takes the `gender` keyword, as well as `parts` which
|
||||
defines how many names make up the full name. The minimum is two: a first name and
|
||||
a last name. You can also generate names with the family name first by setting
|
||||
the keyword arg `surname_first` to `True`
|
||||
|
||||
Example:
|
||||
```
|
||||
>>> namegen.full_name()
|
||||
'Keeva Bernat'
|
||||
>>> namegen.full_name(parts=4)
|
||||
'Suzu Shabnam Kafka Baier'
|
||||
>>> namegen.full_name(parts=3, surname_first=True)
|
||||
'Ó Muircheartach Torunn Dyson'
|
||||
>>> namegen.full_name(gender='f')
|
||||
'Wikolia Ó Deasmhumhnaigh'
|
||||
```
|
||||
|
||||
### Adding your own names
|
||||
|
||||
You can add additional names with the settings `NAMEGEN_FIRST_NAMES` and
|
||||
`NAMEGEN_LAST_NAMES`
|
||||
|
||||
`NAMEGEN_FIRST_NAMES` should be a list of tuples, where the first value is the name
|
||||
and then second value is the gender flag - 'm' for masculine-only, 'f' for feminine-
|
||||
only, and 'mf' for either one.
|
||||
|
||||
`NAMEGEN_LAST_NAMES` should be a list of strings, where each item is an available
|
||||
surname.
|
||||
|
||||
Examples:
|
||||
```py
|
||||
NAMEGEN_FIRST_NAMES = [
|
||||
("Evennia", 'mf'),
|
||||
("Green Tea", 'f'),
|
||||
]
|
||||
|
||||
NAMEGEN_LAST_NAMES = [ "Beeblebrox", "Son of Odin" ]
|
||||
```
|
||||
|
||||
Set `NAMEGEN_REPLACE_LISTS = True` if you want your custom lists above to entirely replace the built-in lists rather than extend them.
|
||||
|
||||
## Generating Fantasy Names
|
||||
|
||||
Generating completely made-up names is done with the `fantasy_name` function. The
|
||||
contrib comes with three built-in styles of names which you can use, or you can
|
||||
put a dictionary of custom name rules into `settings.py`
|
||||
|
||||
Generating a fantasy name takes the ruleset key as the "style" keyword, and can
|
||||
return either a single name or multiple names. By default, it will return a
|
||||
single name in the built-in "harsh" style. The contrib also comes with "fluid" and "alien" styles.
|
||||
|
||||
```py
|
||||
>>> namegen.fantasy_name()
|
||||
'Vhon'
|
||||
>>> namegen.fantasy_name(num=3, style="harsh")
|
||||
['Kha', 'Kizdhu', 'Godögäk']
|
||||
>>> namegen.fantasy_name(num=3, style="fluid")
|
||||
['Aewalisash', 'Ayi', 'Iaa']
|
||||
>>> namegen.fantasy_name(num=5, style="alien")
|
||||
["Qz'vko'", "Xv'w'hk'hxyxyz", "Wxqv'hv'k", "Wh'k", "Xbx'qk'vz"]
|
||||
```
|
||||
|
||||
### Multi-Word Fantasy Names
|
||||
|
||||
The `fantasy_name` function will only generate one name-word at a time, so for multi-word names
|
||||
you'll need to combine pieces together. Depending on what kind of end result you want, there are
|
||||
several approaches.
|
||||
|
||||
|
||||
#### The simple approach
|
||||
|
||||
If all you need is for it to have multiple parts, you can generate multiple names at once and `join` them.
|
||||
|
||||
```py
|
||||
>>> name = " ".join(namegen.fantasy_name(num=2))
|
||||
>>> name
|
||||
'Dezhvözh Khäk'
|
||||
```
|
||||
|
||||
If you want a little more variation between first/last names, you can also generate names for
|
||||
different styles and then combine them.
|
||||
|
||||
```py
|
||||
>>> first = namegen.fantasy_name(style="fluid")
|
||||
>>> last = namegen.fantasy_name(style="harsh")
|
||||
>>> name = f"{first} {last}"
|
||||
>>> name
|
||||
'Ofasa Käkudhu'
|
||||
```
|
||||
|
||||
#### "Nakku Silversmith"
|
||||
|
||||
One common fantasy name practice is profession- or title-based surnames. To achieve this effect,
|
||||
you can use the `last_name` function with a custom list of last names and combine it with your generated
|
||||
fantasy name.
|
||||
|
||||
Example:
|
||||
```py
|
||||
NAMEGEN_LAST_NAMES = [ "Silversmith", "the Traveller", "Destroyer of Worlds" ]
|
||||
NAMEGEN_REPLACE_LISTS = True
|
||||
|
||||
>>> first = namegen.fantasy_name()
|
||||
>>> last = namegen.last_name()
|
||||
>>> name = f"{first} {last}"
|
||||
>>> name
|
||||
'Tözhkheko the Traveller'
|
||||
```
|
||||
|
||||
#### Elarion d'Yrinea, Thror Obinson
|
||||
|
||||
Another common flavor of fantasy names is to use a surname suffix or prefix. For that, you'll
|
||||
need to add in the extra bit yourself.
|
||||
|
||||
Examples:
|
||||
```py
|
||||
>>> names = namegen.fantasy_name(num=2)
|
||||
>>> name = f"{names[0]} za'{names[1]}"
|
||||
>>> name
|
||||
"Tithe za'Dhudozkok"
|
||||
|
||||
>>> names = namegen.fantasy_name(num=2)
|
||||
>>> name = f"{names[0]} {names[1]}son"
|
||||
>>> name
|
||||
'Kön Ködhöddoson'
|
||||
```
|
||||
|
||||
|
||||
### Custom Fantasy Name style rules
|
||||
|
||||
The style rules are contained in a dictionary of dictionaries, where the style name
|
||||
is the key and the style rules are the dictionary value.
|
||||
|
||||
The following is how you would add a custom style to `settings.py`:
|
||||
```py
|
||||
NAMEGEN_FANTASY_RULES = {
|
||||
"example_style": {
|
||||
"syllable": "(C)VC",
|
||||
"consonants": [ 'z','z','ph','sh','r','n' ],
|
||||
"start": ['m'],
|
||||
"end": ['x','n'],
|
||||
"vowels": [ "e","e","e","a","i","i","u","o", ],
|
||||
"length": (2,4),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then you could generate names following that ruleset with `namegen.fantasy_name(style="example_style")`.
|
||||
|
||||
The keys `syllable`, `consonants`, `vowels`, and `length` must be present, and `length` must be the minimum and maximum syllable counts. `start` and `end` are optional.
|
||||
|
||||
|
||||
#### syllable
|
||||
The "syllable" field defines the structure of each syllable. C is consonant, V is vowel,
|
||||
and parentheses mean it's optional. So, the example `(C)VC` means that every syllable
|
||||
will always have a vowel followed by a consonant, and will *sometimes* have another
|
||||
consonant at the beginning. e.g. `en`, `bak`
|
||||
|
||||
*Note:* While it's not standard, the contrib lets you nest parentheses, with each layer
|
||||
being less likely to show up. Additionally, any other characters put into the syllable
|
||||
structure - e.g. an apostrophe - will be read and inserted as written. The
|
||||
"alien" style rules in the module gives an example of both: the syllable structure is `C(C(V))(')(C)`
|
||||
which results in syllables such as `khq`, `xho'q`, and `q'` with a much lower frequency of vowels than
|
||||
`C(C)(V)(')(C)` would have given.
|
||||
|
||||
#### consonants
|
||||
A simple list of consonant phonemes that can be chosen from. Multi-character strings are
|
||||
perfectly acceptable, such as "th", but each one will be treated as a single consonant.
|
||||
|
||||
The function uses a naive form of weighting, where you make a phoneme more likely to
|
||||
occur by putting more copies of it into the list.
|
||||
|
||||
#### start and end
|
||||
These are **optional** lists for the first and last letters of a syllable, if they're
|
||||
a consonant. You can add on additional consonants which can only occur at the beginning
|
||||
or end of a syllable, or you can add extra copies of already-defined consonants to
|
||||
increase the frequency of them at the start/end of syllables.
|
||||
|
||||
For example, in the `example_style` above, we have a `start` of m, and `end` of x and n.
|
||||
Taken with the rest of the consonants/vowels, this means you can have the syllables of `mez`
|
||||
but not `zem`, and you can have `phex` or `phen` but not `xeph` or `neph`.
|
||||
|
||||
They can be left out of custom rulesets entirely.
|
||||
|
||||
#### vowels
|
||||
Vowels is a simple list of vowel phonemes - exactly like consonants, but instead used for the
|
||||
vowel selection. Single-or multi-character strings are equally fine. It uses the same naive weighting system
|
||||
as consonants - you can increase the frequency of any given vowel by putting it into the list multiple times.
|
||||
|
||||
#### length
|
||||
A tuple with the minimum and maximum number of syllables a name can have.
|
||||
|
||||
When setting this, keep in mind how long your syllables can get! 4 syllables might
|
||||
not seem like very many, but if you have a (C)(V)VC structure with one- and
|
||||
two-letter phonemes, you can get up to eight characters per syllable.
|
||||
|
||||
----
|
||||
|
||||
<small>This document page is generated from `evennia/contrib/utils/name_generator/README.md`. Changes to this
|
||||
file will be overwritten, so edit that file rather than this one.</small>
|
||||
|
|
@ -363,6 +363,7 @@ contribs related to rooms, exits and map building._
|
|||
:maxdepth: 1
|
||||
|
||||
Contrib-Extended-Room.md
|
||||
Contrib-Ingame-Map-Display.md
|
||||
Contrib-Mapbuilder.md
|
||||
Contrib-Simpledoor.md
|
||||
Contrib-Slow-Exit.md
|
||||
|
|
@ -384,6 +385,19 @@ supported by new `look` and `desc` commands.
|
|||
|
||||
|
||||
|
||||
### Contrib: `ingame_map_display`
|
||||
|
||||
_Contribution - helpme 2022_
|
||||
|
||||
This adds an ascii `map` to a given room which can be viewed with the `map` command.
|
||||
You can easily alter it to add special characters, room colors etc. The map shown is
|
||||
dynamically generated on use, and supports all compass directions and up/down. Other
|
||||
directions are ignored.
|
||||
|
||||
[Read the documentation](./Contrib-Ingame-Map-Display.md) - [Browse the Code](evennia.contrib.grid.ingame_map_display)
|
||||
|
||||
|
||||
|
||||
### Contrib: `mapbuilder`
|
||||
|
||||
_Contribution by Cloud_Keeper 2016_
|
||||
|
|
@ -458,6 +472,8 @@ and rule implementation like character traits, dice rolling and emoting._
|
|||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Contrib-Buffs.md
|
||||
Contrib-Character-Creator.md
|
||||
Contrib-Dice.md
|
||||
Contrib-Health-Bar.md
|
||||
Contrib-RPSystem.md
|
||||
|
|
@ -465,6 +481,27 @@ Contrib-Traits.md
|
|||
```
|
||||
|
||||
|
||||
### Contrib: `buffs`
|
||||
|
||||
_Contribution by Tegiminis 2022_
|
||||
|
||||
A buff is a timed object, attached to a game entity. It is capable of modifying values, triggering code, or both.
|
||||
It is a common design pattern in RPGs, particularly action games.
|
||||
|
||||
[Read the documentation](./Contrib-Buffs.md) - [Browse the Code](evennia.contrib.rpg.buffs)
|
||||
|
||||
|
||||
|
||||
### Contrib: `character_creator`
|
||||
|
||||
_Commands for managing and initiating an in-game character-creation menu._
|
||||
|
||||
Contribution by InspectorCaracal, 2022
|
||||
|
||||
[Read the documentation](./Contrib-Character-Creator.md) - [Browse the Code](evennia.contrib.rpg.character_creator)
|
||||
|
||||
|
||||
|
||||
### Contrib: `dice`
|
||||
|
||||
_Contribution by Griatch, 2012_
|
||||
|
|
@ -537,6 +574,7 @@ tutorials are found here. Also the home of the Tutorial World demo adventure._
|
|||
|
||||
Contrib-Batchprocessor.md
|
||||
Contrib-Bodyfunctions.md
|
||||
Contrib-Evadventure.md
|
||||
Contrib-Mirror.md
|
||||
Contrib-Red-Button.md
|
||||
Contrib-Talking-Npc.md
|
||||
|
|
@ -567,6 +605,20 @@ character make small verbal observations at irregular intervals.
|
|||
|
||||
|
||||
|
||||
### Contrib: `evadventure`
|
||||
|
||||
_Contrib by Griatch 2022_
|
||||
|
||||
A complete example MUD using Evennia. This is the final result of what is
|
||||
implemented if you follow the Getting-Started tutorial. It's recommended
|
||||
that you follow the tutorial step by step and write your own code. But if
|
||||
you prefer you can also pick apart or use this as a starting point for your
|
||||
own game.
|
||||
|
||||
[Read the documentation](./Contrib-Evadventure.md) - [Browse the Code](evennia.contrib.tutorials.evadventure)
|
||||
|
||||
|
||||
|
||||
### Contrib: `mirror`
|
||||
|
||||
_Contribution by Griatch, 2017_
|
||||
|
|
@ -628,6 +680,7 @@ and more._
|
|||
|
||||
Contrib-Auditing.md
|
||||
Contrib-Fieldfill.md
|
||||
Contrib-Name-Generator.md
|
||||
Contrib-Random-String-Generator.md
|
||||
Contrib-Tree-Select.md
|
||||
```
|
||||
|
|
@ -661,6 +714,19 @@ to any callable of your choice.
|
|||
|
||||
|
||||
|
||||
### Contrib: `name_generator`
|
||||
|
||||
_Contribution by InspectorCaracal (2022)_
|
||||
|
||||
A module for generating random names, both real-world and fantasy. Real-world
|
||||
names can be generated either as first (personal) names, family (last) names, or
|
||||
full names (first, optional middles, and last). The name data is from [Behind the Name](https://www.behindthename.com/)
|
||||
and used under the [CC BY-SA 4.0 license](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||
|
||||
[Read the documentation](./Contrib-Name-Generator.md) - [Browse the Code](evennia.contrib.utils.name_generator)
|
||||
|
||||
|
||||
|
||||
### Contrib: `random_string_generator`
|
||||
|
||||
_Contribution by Vincent Le Goff (vlgeoff), 2017_
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ connect to the demo via your telnet client you can do so at `demo.evennia.com`,
|
|||
|
||||
Once you installed Evennia yourself it comes with its own tutorial - this shows off some of the
|
||||
possibilities _and_ gives you a small single-player quest to play. The tutorial takes only one
|
||||
single in-game command to install as explained [here](Howtos/Beginner-Tutorial/Part1/Tutorial-World.md).
|
||||
single in-game command to install as explained [here](Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Tutorial-World.md).
|
||||
|
||||
## What you need to know to work with Evennia
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ online](https://github.com/evennia/evennia). We also have a comprehensive [onlin
|
|||
manual](https://evennia.com/docs) with lots of examples. But while Python is
|
||||
considered a very easy programming language to get into, you do have a learning curve to climb if
|
||||
you are new to programming. Evennia's [Starting-tutorial](Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Part1-Intro.md) has a [basic introduction
|
||||
to Python](Howtos/Beginner-Tutorial/Part1/Python-basic-introduction.md) but you should probably also sit down
|
||||
to Python](Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Python-basic-introduction.md) but you should probably also sit down
|
||||
with a full Python beginner's tutorial at some point (there are plenty of them on
|
||||
the web if you look around). See also our [link
|
||||
page](./Links.md) for some reading suggestions. To efficiently code your dream game in
|
||||
|
|
@ -124,7 +124,7 @@ chat](https://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE
|
|||
on IRC. This allows you to chat directly with other developers new and old as well as with the devs
|
||||
of Evennia itself. This chat is logged (you can find links on https://www.evennia.com) and can also
|
||||
be searched from the same place for discussion topics you are interested in.
|
||||
2. Read the [Game Planning](Howtos/Beginner-Tutorial/Part2/Game-Planning.md) wiki page. It gives some ideas for your work flow and the
|
||||
2. Read the [Game Planning](Howtos/Beginner-Tutorial/Part2/Beginner-Tutorial-Game-Planning.md) wiki page. It gives some ideas for your work flow and the
|
||||
state of mind you should aim for - including cutting down the scope of your game for its first
|
||||
release.
|
||||
3. Do the [Tutorial for basic MUSH-like game](Howtos/Tutorial-for-basic-MUSH-like-game.md) carefully from
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ There is usually no need to know the details of Django's database handling in or
|
|||
it will handle most of the complexity for you under the hood using what we call
|
||||
[typeclasses](./Glossary.md#typeclass). But should you need the power of Django you can always get it.
|
||||
Most commonly people want to use "raw" Django when doing more advanced/custom database queries than
|
||||
offered by Evennia's [default search functions](Howtos/Beginner-Tutorial/Part1/Searching-Things.md). One will then need
|
||||
offered by Evennia's [default search functions](Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md). One will then need
|
||||
to read about Django's _querysets_. Querysets are Python method calls on a special form that lets
|
||||
you build complex queries. They get converted into optimized SQL queries under the hood, suitable
|
||||
for your current database. [Here is our tutorial/explanation of Django queries](Tutorial-Searching-
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[prev lesson](../../../Unimplemented.md) | [next lesson](../../../Unimplemented.md)
|
||||
[prev lesson](../Unimplemented.md) | [next lesson](../Unimplemented.md)
|
||||
|
||||
# Making a sittable object
|
||||
|
||||
|
|
@ -524,7 +524,7 @@ class CmdStand(Command):
|
|||
# ...
|
||||
```
|
||||
|
||||
We define a [Lock](../../../Components/Locks.md) on the command. The `cmd:` is in what situation Evennia will check
|
||||
We define a [Lock](../Components/Locks.md) on the command. The `cmd:` is in what situation Evennia will check
|
||||
the lock. The `cmd` means that it will check the lock when determining if a user has access to this command or not.
|
||||
What will be checked is the `sitsonthis` _lock function_ which doesn't exist yet.
|
||||
|
||||
|
|
@ -753,7 +753,7 @@ class CmdStand2(Command):
|
|||
```
|
||||
|
||||
This forced us to to use the full power of the `caller.search` method. If we wanted to search for something
|
||||
more complex we would likely need to break out a [Django query](../Part1/Django-queries.md) to do it. The key here is that
|
||||
more complex we would likely need to break out a [Django query](Beginner-Tutorial/Part1/Beginner-Tutorial-Django-queries.md) to do it. The key here is that
|
||||
we know that the object we are looking for is a `Sittable` and that it must have an Attribute named `sitter`
|
||||
which should be set to us, the one sitting on/in the thing. Once we have that we just call `.do_stand` on it
|
||||
and let the Typeclass handle the rest.
|
||||
|
|
@ -799,4 +799,4 @@ Eagle-eyed readers will notice that the `stand` command sitting "on" the chair (
|
|||
together with the `sit` command sitting "on" the Character (variant 2). There is nothing stopping you from
|
||||
mixing them, or even try a third solution that better fits what you have in mind.
|
||||
|
||||
[prev lesson](../../../Unimplemented.md) | [next lesson](../../../Unimplemented.md)
|
||||
[prev lesson](../Unimplemented.md) | [next lesson](../Unimplemented.md)
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
# Add a wiki on your website
|
||||
|
||||
|
||||
**Before doing this tutorial you will probably want to read the intro in
|
||||
[Basic Web tutorial](Beginner-Tutorial/Part5/Web-Tutorial.md).** Reading the three first parts of the
|
||||
**Before doing this tutorial you will probably want to read the intro in
|
||||
[Basic Web tutorial](Beginner-Tutorial/Part5/Web-Tutorial.md).** Reading the three first parts of the
|
||||
[Django tutorial](https://docs.djangoproject.com/en/1.9/intro/tutorial01/) might help as well.
|
||||
|
||||
This tutorial will provide a step-by-step process to installing a wiki on your website.
|
||||
|
|
@ -10,14 +9,9 @@ Fortunately, you don't have to create the features manually, since it has been d
|
|||
we can integrate their work quite easily with Django. I have decided to focus on
|
||||
the [Django-wiki](https://django-wiki.readthedocs.io/).
|
||||
|
||||
> Note: this article has been updated for Evennia 0.9. If you're not yet using this version, be
|
||||
careful, as the django wiki doesn't support Python 2 anymore. (Remove this note when enough time
|
||||
has passed.)
|
||||
|
||||
The [Django-wiki](https://django-wiki.readthedocs.io/) offers a lot of features associated with
|
||||
wikis, is
|
||||
actively maintained (at this time, anyway), and isn't too difficult to install in Evennia. You can
|
||||
see a [demonstration of Django-wiki here](https://demo.django.wiki).
|
||||
wikis, is actively maintained (at this time, anyway), and isn't too difficult to install in Evennia. You can
|
||||
see a [demonstration of Django-wiki here](https://demo.django-wiki.org).
|
||||
|
||||
## Basic installation
|
||||
|
||||
|
|
@ -25,6 +19,8 @@ You should begin by shutting down the Evennia server if it is running. We will
|
|||
alter the virtual environment just a bit. Open a terminal and activate your Python environment, the
|
||||
one you use to run the `evennia` command.
|
||||
|
||||
If you used the default location from the Evennia installation instructions, it should be one of the following:
|
||||
|
||||
* On Linux:
|
||||
```
|
||||
source evenv/bin/activate
|
||||
|
|
@ -40,26 +36,15 @@ Install the wiki using pip:
|
|||
|
||||
pip install wiki
|
||||
|
||||
> Note: this will install the last version of Django wiki. Version >0.4 doesn't support Python 2, so
|
||||
install wiki 0.3 if you haven't updated to Python 3 yet.
|
||||
|
||||
It might take some time, the Django-wiki having some dependencies.
|
||||
|
||||
### Adding the wiki in the settings
|
||||
|
||||
You will need to add a few settings to have the wiki app on your website. Open your
|
||||
`server/conf/settings.py` file and add the following at the bottom (but before importing
|
||||
`secret_settings`). Here's what you'll find in my own setting file (add the whole Django-wiki
|
||||
section):
|
||||
`secret_settings`). Here's an example of a settings file with the Django-wiki added:
|
||||
|
||||
```python
|
||||
r"""
|
||||
Evennia settings file.
|
||||
|
||||
...
|
||||
|
||||
"""
|
||||
|
||||
# Use the defaults from Evennia unless explicitly overridden
|
||||
from evennia.settings_default import *
|
||||
|
||||
|
|
@ -98,65 +83,87 @@ except ImportError:
|
|||
print("secret_settings.py file not found or failed to import.")
|
||||
```
|
||||
|
||||
Everything in the section "Django-wiki settings" is what you'll need to include.
|
||||
|
||||
### Adding the new URLs
|
||||
|
||||
Next we need to add two URLs in our `web/urls.py` file. Open it and compare the following output:
|
||||
you will need to add two URLs in `custom_patterns` and add one import line:
|
||||
Next you will need to add two URLs to the file `web/urls.py`. You'll do that by modifying
|
||||
`urlpatterns` to look something like this:
|
||||
|
||||
```python
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import path # NEW!
|
||||
|
||||
# default evenni a patterns
|
||||
from evennia.web.urls import urlpatterns
|
||||
|
||||
# eventual custom patterns
|
||||
custom_patterns = [
|
||||
# url(r'/desired/url/', view, name='example'),
|
||||
url('notifications/', include('django_nyt.urls')), # NEW!
|
||||
url('wiki/', include('wiki.urls')), # NEW!
|
||||
# add patterns
|
||||
urlpatterns = [
|
||||
# website
|
||||
path("", include("web.website.urls")),
|
||||
# webclient
|
||||
path("webclient/", include("web.webclient.urls")),
|
||||
# web admin
|
||||
path("admin/", include("web.admin.urls")),
|
||||
# wiki
|
||||
path("wiki/", include("wiki.urls")),
|
||||
path("notifications/", include("django_nyt.urls")),
|
||||
]
|
||||
|
||||
# this is required by Django.
|
||||
urlpatterns = custom_patterns + urlpatterns
|
||||
```
|
||||
|
||||
You will probably need to copy line 2, 10, and 11. Be sure to place them correctly, as shown in
|
||||
the example above.
|
||||
The last two lines are what you'll need to add.
|
||||
|
||||
### Running migrations
|
||||
|
||||
It's time to run the new migrations. The wiki app adds a few tables in our database. We'll need to
|
||||
run:
|
||||
Next you'll need to run migrations, since the wiki app adds a few tables in our database:
|
||||
|
||||
evennia migrate
|
||||
|
||||
And that's it, you can start the server. If you go to http://localhost:4001/wiki , you should see
|
||||
the wiki. Use your account's username and password to connect to it. That's how simple it is.
|
||||
|
||||
## Customizing privileges
|
||||
### Initializing the wiki
|
||||
|
||||
A wiki can be a great collaborative tool, but who can see it? Who can modify it? Django-wiki comes
|
||||
with a privilege system centered around four values per wiki page. The owner of an article can
|
||||
always read and write in it (which is somewhat logical). The group of the article defines who can
|
||||
read and who can write, if the user seeing the page belongs to this group. The topic of groups in
|
||||
wiki pages will not be discussed here. A last setting determines which other user (that is, these
|
||||
who aren't in the groups, and aren't the article's owner) can read and write. Each article has
|
||||
these four settings (group read, group write, other read, other write). Depending on your purpose,
|
||||
it might not be a good default choice, particularly if you have to remind every builder to keep the
|
||||
pages private. Fortunately, Django-wiki gives us additional settings to customize who can read, and
|
||||
who can write, a specific article.
|
||||
Last step! Go ahead and start up your server again.
|
||||
|
||||
These settings must be placed, as usual, in your `server/conf/settings.py` file. They take a
|
||||
function as argument, said function (or callback) will be called with the article and the user.
|
||||
Remember, a Django user, for us, is an account. So we could check lockstrings on them if needed.
|
||||
Here is a default setting to restrict the wiki: only builders can write in it, but anyone (including
|
||||
non-logged in users) can read it. The superuser has some additional privileges.
|
||||
evennia start
|
||||
|
||||
Once that's finished booting, go to your evennia website (e.g. http://localhost:4001 ) and log in
|
||||
with your superuser account, if you aren't already. Then, go to your new wiki (e.g.
|
||||
http://localhost:4001/wiki ). It'll prompt you to create a starting page - put whatever you want,
|
||||
you can change it later.
|
||||
|
||||
Congratulations! You're all done!
|
||||
|
||||
## Defining wiki permissions
|
||||
|
||||
A wiki is usually intended as a collaborative effort - but you probably still want to set
|
||||
some rules about who is allowed to do what. Who can create new articles? Edit them? Delete
|
||||
them? Etc.
|
||||
|
||||
The two simplest ways to do this are to use Django-wiki's group-based permissions
|
||||
system - or, since this is an Evennia site, to define your own custom permission rules
|
||||
tied to Evennia's permissions system in your settings file.
|
||||
|
||||
### Group permissions
|
||||
|
||||
The wiki itself controls reading/editing permissions per article. The creator of an article will
|
||||
always have read/write permissions on that article. Additionally, the article will have Group-based
|
||||
permissions and general permissions.
|
||||
|
||||
By default, Evennia's permission groups *won't* be recognized by the wiki, so you'll have to create your own.
|
||||
Go to the Groups page of your game's Django admin panel (e.g. http://localhost:4001/admin/auth/group )
|
||||
and add whichever permission groups you want for your wiki here.
|
||||
|
||||
***Note:*** *If you want to connect those groups to your game's permission levels, you'll need to modify the game to apply both to accounts.*
|
||||
|
||||
Once you've added those groups, they'll be usable in your wiki right away!
|
||||
|
||||
### Settings permissions
|
||||
|
||||
Django-wiki also allows you to bypass its article-based permissions with custom site-wide permissions
|
||||
rules in your settings file. If you don't want to use the Group system, or if you want a simple
|
||||
solution for connecting the Evennia permission levels to wiki access, this is the way to go.
|
||||
|
||||
Here's an example of a basic set-up that would go in your `settings.py` file:
|
||||
|
||||
```python
|
||||
# In server/conf/settings.py
|
||||
# ...
|
||||
|
||||
# Custom methods to link wiki permissions to game perms
|
||||
def is_superuser(article, user):
|
||||
"""Return True if user is a superuser, False otherwise."""
|
||||
return not user.is_anonymous() and user.is_superuser
|
||||
|
|
@ -165,68 +172,39 @@ def is_builder(article, user):
|
|||
"""Return True if user is a builder, False otherwise."""
|
||||
return not user.is_anonymous() and user.locks.check_lockstring(user, "perm(Builders)")
|
||||
|
||||
def is_anyone(article, user):
|
||||
"""Return True even if the user is anonymous."""
|
||||
return True
|
||||
def is_player(article, user):
|
||||
"""Return True if user is a builder, False otherwise."""
|
||||
return not user.is_anonymous() and user.locks.check_lockstring(user, "perm(Players)")
|
||||
|
||||
# Who can create new groups and users from the wiki?
|
||||
# Create new users
|
||||
WIKI_CAN_ADMIN = is_superuser
|
||||
# Who can change owner and group membership?
|
||||
|
||||
# Change the owner and group for an article
|
||||
WIKI_CAN_ASSIGN = is_superuser
|
||||
# Who can change group membership?
|
||||
|
||||
# Change the GROUP of an article, despite the name
|
||||
WIKI_CAN_ASSIGN_OWNER = is_superuser
|
||||
# Who can change read/write access to groups or others?
|
||||
|
||||
# Change read/write permissions on an article
|
||||
WIKI_CAN_CHANGE_PERMISSIONS = is_superuser
|
||||
# Who can soft-delete an article?
|
||||
|
||||
# Mark an article as deleted
|
||||
WIKI_CAN_DELETE = is_builder
|
||||
# Who can lock an article and permanently delete it?
|
||||
|
||||
# Lock or permanently delete an article
|
||||
WIKI_CAN_MODERATE = is_superuser
|
||||
# Who can edit articles?
|
||||
|
||||
# Create or edit any pages
|
||||
WIKI_CAN_WRITE = is_builder
|
||||
# Who can read articles?
|
||||
WIKI_CAN_READ = is_anyone
|
||||
|
||||
# Read any pages
|
||||
WIKI_CAN_READ = is_player
|
||||
|
||||
# Completely disallow editing and article creation when not logged in
|
||||
WIKI_ANONYMOUS_WRITE = False
|
||||
```
|
||||
|
||||
Here, we have created three functions: one to return `True` if the user is the superuser, one to
|
||||
return `True` if the user is a builder, one to return `True` no matter what (this includes if the
|
||||
user is anonymous, E.G. if it's not logged-in). We then change settings to allow either the
|
||||
superuser or
|
||||
each builder to moderate, read, write, delete, and more. You can, of course, add more functions,
|
||||
adapting them to your need. This is just a demonstration.
|
||||
The permission functions can check anything you like on the accessing user, so long as the function
|
||||
returns either True (they're allowed) or False (they're not).
|
||||
|
||||
Providing the `WIKI_CAN*...` settings will bypass the original permission system. The superuser
|
||||
could change permissions of an article, but still, only builders would be able to write it. If you
|
||||
need something more custom, you will have to expand on the functions you use.
|
||||
|
||||
### Managing wiki pages from Evennia
|
||||
|
||||
Unfortunately, Django wiki doesn't provide a clear and clean entry point to read and write articles
|
||||
from Evennia and it doesn't seem to be a very high priority. If you really need to keep Django wiki
|
||||
and to create and manage wiki pages from your code, you can do so, but this article won't elaborate,
|
||||
as this is somewhat more technical.
|
||||
|
||||
However, it is a good opportunity to present a small project that has been created more recently:
|
||||
[evennia-wiki](https://github.com/vincent-lg/evennia-wiki) has been created to provide a simple
|
||||
wiki, more tailored to Evennia and easier to connect. It doesn't, as yet, provide as many options
|
||||
as does Django wiki, but it's perfectly usable:
|
||||
|
||||
- Pages have an inherent and much-easier to understand hierarchy based on URLs.
|
||||
- Article permissions are connected to Evennia groups and are much easier to accommodate specific
|
||||
requirements.
|
||||
- Articles can easily be created, read or updated from the Evennia code itself.
|
||||
- Markdown is fully-supported with a default integration to Bootstrap to look good on an Evennia
|
||||
website. Tables and table of contents are supported as well as wiki links.
|
||||
- The process to override wiki templates makes full use of the `template_overrides` directory.
|
||||
|
||||
However evennia-wiki doesn't yet support:
|
||||
|
||||
- Images in markdown and the uploading schema. If images are important to you, please consider
|
||||
contributing to this new project.
|
||||
- Modifying permissions on a per page/setting basis.
|
||||
- Moving pages to new locations.
|
||||
- Viewing page history.
|
||||
|
||||
Considering the list of features in Django wiki, obviously other things could be added to the list.
|
||||
However, these features may be the most important and useful. Additional ones might not be that
|
||||
necessary. If you're interested in supporting this little project, you are more than welcome to
|
||||
[contribute to it](https://github.com/vincent-lg/evennia-wiki). Thanks!
|
||||
For a full list of possible settings, you can check out [the django-wiki documentation](https://django-wiki.readthedocs.io/en/latest/settings.html).
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ A new folder `myarx` should appear next to the ones you already had. You could r
|
|||
something else if you want.
|
||||
|
||||
`cd` into `myarx`. If you wonder about the structure of the game dir, you can
|
||||
[read more about it here](Beginner-Tutorial/Part1/Gamedir-Overview.md).
|
||||
[read more about it here](Beginner-Tutorial/Part1/Beginner-Tutorial-Gamedir-Overview.md).
|
||||
|
||||
### Clean up settings
|
||||
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ class CmdEcho(Command):
|
|||
First we added a docstring. This is always a good thing to do in general, but for a Command class, it will also
|
||||
automatically become the in-game help entry! Next we add the `func` method. It has one active line where it
|
||||
makes use of some of those variables we found the Command offers to us. If you did the
|
||||
[basic Python tutorial](./Python-basic-introduction.md), you will recognize `.msg` - this will send a message
|
||||
[basic Python tutorial](./Beginner-Tutorial-Python-basic-introduction.md), you will recognize `.msg` - this will send a message
|
||||
to the object it is attached to us - in this case `self.caller`, that is, us. We grab `self.args` and includes
|
||||
that in the message.
|
||||
|
||||
|
|
@ -149,7 +149,7 @@ the raw description of your current room (including color codes), so that you ca
|
|||
set its description to something else.
|
||||
|
||||
You create new Commands (or modify existing ones) in Python outside the game. We will get to that
|
||||
later, in the [Commands tutorial](./Adding-Commands.md).
|
||||
later, in the [Commands tutorial](./Beginner-Tutorial-Adding-Commands.md).
|
||||
|
||||
## Get a Personality
|
||||
|
||||
|
|
@ -200,7 +200,7 @@ people change and re-structure this in various ways to better fit their ideas.
|
|||
|
||||
- [batch_cmds.ev](github:evennia/game_template/world/batch_cmds.ev) - This is an `.ev` file, which is essentially
|
||||
just a list of Evennia commands to execute in sequence. This one is empty and ready to expand on. The
|
||||
[Tutorial World](./Tutorial-World.md) was built with such a batch-file.
|
||||
[Tutorial World](./Beginner-Tutorial-Tutorial-World.md) was built with such a batch-file.
|
||||
- [prototypes.py](github:evennia/game_template/world/prototypes.py) - A [prototype](../../../Components/Prototypes.md) is a way
|
||||
to easily vary objects without changing their base typeclass. For example, one could use prototypes to
|
||||
tell that Two goblins, while both of the class 'Goblin' (so they follow the same code logic), should have different
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Now that we have learned a little about how to find things in the Evennia library, let's use it.
|
||||
|
||||
In the [Python classes and objects](./Python-classes-and-objects.md) lesson we created the dragons Fluffy, Cuddly
|
||||
In the [Python classes and objects](./Beginner-Tutorial-Python-classes-and-objects.md) lesson we created the dragons Fluffy, Cuddly
|
||||
and Smaug and made them fly and breathe fire. So far our dragons are short-lived - whenever we `restart`
|
||||
the server or `quit()` out of python mode they are gone.
|
||||
|
||||
|
|
@ -251,7 +251,7 @@ You are specifying exactly which typeclass you want to use to build the Giantess
|
|||
desc = You see nothing special.
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
We used the `examine` command briefly in the [lesson about building in-game](./Building-Quickstart.md). Now these lines
|
||||
We used the `examine` command briefly in the [lesson about building in-game](./Beginner-Tutorial-Building-Quickstart.md). Now these lines
|
||||
may be more useful to us:
|
||||
- **Name/key** - The name of this thing. The value `(#14)` is probably different for you. This is the
|
||||
unique 'primary key' or _dbref_ for this entity in the database.
|
||||
|
|
@ -357,7 +357,7 @@ You got a lot longer output this time. You have a lot more going on than a simpl
|
|||
- **Session id(s)**: This identifies the _Session_ (that is, the individual connection to a player's game client).
|
||||
- **Account** shows, well the `Account` object associated with this Character and Session.
|
||||
- **Stored/Merged Cmdsets** and **Commands available** is related to which _Commands_ are stored on you. We will
|
||||
get to them in the [next lesson](./Adding-Commands.md). For now it's enough to know these consitute all the
|
||||
get to them in the [next lesson](./Beginner-Tutorial-Adding-Commands.md). For now it's enough to know these consitute all the
|
||||
commands available to you at a given moment.
|
||||
- **Non-Persistent attributes** are Attributes that are only stored temporarily and will go away on next reload.
|
||||
|
||||
|
|
@ -143,8 +143,8 @@ change (no code changed, only stuff in the database).
|
|||
## Adding a Command to an object
|
||||
|
||||
The commands of a cmdset attached to an object with `obj.cmdset.add()` will by default be made available to that object
|
||||
but _also to those in the same location as that object_. If you did the [Building introduction](./Building-Quickstart.md)
|
||||
you've seen an example of this with the "Red Button" object. The [Tutorial world](./Tutorial-World.md)
|
||||
but _also to those in the same location as that object_. If you did the [Building introduction](./Beginner-Tutorial-Building-Quickstart.md)
|
||||
you've seen an example of this with the "Red Button" object. The [Tutorial world](./Beginner-Tutorial-Tutorial-World.md)
|
||||
also has many examples of objects with commands on them.
|
||||
|
||||
To show how this could work, let's put our 'hit' Command on our simple `sword` object from the previous section.
|
||||
|
|
@ -30,18 +30,18 @@ these concepts in the context of Evennia before.
|
|||
:maxdepth: 1
|
||||
:numbered:
|
||||
|
||||
Building-Quickstart
|
||||
Tutorial-World
|
||||
Python-basic-introduction
|
||||
Gamedir-Overview
|
||||
Python-classes-and-objects
|
||||
Evennia-Library-Overview
|
||||
Learning-Typeclasses
|
||||
Adding-Commands
|
||||
More-on-Commands
|
||||
Creating-Things
|
||||
Searching-Things
|
||||
Django-queries
|
||||
Beginner-Tutorial-Building-Quickstart
|
||||
Beginner-Tutorial-Tutorial-World
|
||||
Beginner-Tutorial-Python-basic-introduction
|
||||
Beginner-Tutorial-Gamedir-Overview
|
||||
Beginner-Tutorial-Python-classes-and-objects
|
||||
Beginner-Tutorial-Evennia-Library-Overview
|
||||
Beginner-Tutorial-Learning-Typeclasses
|
||||
Beginner-Tutorial-Adding-Commands
|
||||
Beginner-Tutorial-More-on-Commands
|
||||
Beginner-Tutorial-Creating-Things
|
||||
Beginner-Tutorial-Searching-Things
|
||||
Beginner-Tutorial-Django-queries
|
||||
|
||||
```
|
||||
|
||||
|
|
@ -50,17 +50,17 @@ Django-queries
|
|||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Building-Quickstart
|
||||
Tutorial-World
|
||||
Python-basic-introduction
|
||||
Gamedir-Overview
|
||||
Python-classes-and-objects
|
||||
Evennia-Library-Overview
|
||||
Learning-Typeclasses
|
||||
Adding-Commands
|
||||
More-on-Commands
|
||||
Creating-Things
|
||||
Searching-Things
|
||||
Django-queries
|
||||
Beginner-Tutorial-Building-Quickstart
|
||||
Beginner-Tutorial-Tutorial-World
|
||||
Beginner-Tutorial-Python-basic-introduction
|
||||
Beginner-Tutorial-Gamedir-Overview
|
||||
Beginner-Tutorial-Python-classes-and-objects
|
||||
Beginner-Tutorial-Evennia-Library-Overview
|
||||
Beginner-Tutorial-Learning-Typeclasses
|
||||
Beginner-Tutorial-Adding-Commands
|
||||
Beginner-Tutorial-More-on-Commands
|
||||
Beginner-Tutorial-Creating-Things
|
||||
Beginner-Tutorial-Searching-Things
|
||||
Beginner-Tutorial-Django-queries
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ The form `from ... import ... as ...` renames the import.
|
|||
> Avoid renaming unless it's to avoid a name-collistion like above - you want to make things as
|
||||
> easy to read as possible, and renaming adds another layer of potential confusion.
|
||||
|
||||
In [the basic intro to Python](./Python-basic-introduction.md) we learned how to open the in-game
|
||||
In [the basic intro to Python](./Beginner-Tutorial-Python-basic-introduction.md) we learned how to open the in-game
|
||||
multi-line interpreter.
|
||||
|
||||
> py
|
||||
|
|
@ -153,7 +153,7 @@ Next we have a `class` named `Object`, which _inherits_ from `DefaultObject`. Th
|
|||
actually do anything on its own, its only code (except the docstring) is `pass` which means,
|
||||
well, to pass and don't do anything.
|
||||
|
||||
We will get back to this module in the [next lesson](./Learning-Typeclasses.md). First we need to do a
|
||||
We will get back to this module in the [next lesson](./Beginner-Tutorial-Learning-Typeclasses.md). First we need to do a
|
||||
little detour to understand what a 'class', an 'object' or 'instance' is. These are fundamental
|
||||
things to understand before you can use Evennia efficiently.
|
||||
```{sidebar} OOP
|
||||
|
|
@ -29,18 +29,16 @@ and "what to think about" when creating a multiplayer online text game.
|
|||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Planning-Where-Do-I-Begin.md
|
||||
Game-Planning.md
|
||||
Planning-Some-Useful-Contribs.md
|
||||
Planning-The-Tutorial-Game.md
|
||||
Beginner-Tutorial-Planning-Where-Do-I-Begin.md
|
||||
Beginner-Tutorial-Game-Planning.md
|
||||
Beginner-Tutorial-Planning-The-Tutorial-Game.md
|
||||
```
|
||||
|
||||
## Table of Contents
|
||||
|
||||
```{toctree}
|
||||
|
||||
Planning-Where-Do-I-Begin.md
|
||||
Game-Planning.md
|
||||
Planning-Some-Useful-Contribs.md
|
||||
Planning-The-Tutorial-Game.md
|
||||
Beginner-Tutorial-Planning-Where-Do-I-Begin.md
|
||||
Beginner-Tutorial-Game-Planning.md
|
||||
Beginner-Tutorial-Planning-The-Tutorial-Game.md
|
||||
```
|
||||
|
|
|
|||
|
|
@ -0,0 +1,462 @@
|
|||
# Planning our tutorial game
|
||||
|
||||
Using the general plan from last lesson we'll now establish what kind of game we want to create for this tutorial. We'll call it ... _EvAdventure_.
|
||||
Remembering that we need to keep the scope down, let's establish some parameters.
|
||||
|
||||
- We want EvAdventure be a small game we can play ourselves for fun, but which could in principle be expanded to something more later.
|
||||
- We want to have a clear game-loop, with clear goals.
|
||||
- Let's go with a fantasy theme, it's well understood.
|
||||
- We will use a small, existing tabletop RPG rule set ([Knave](https://www.drivethrurpg.com/product/250888/Knave), more info later)
|
||||
- We want to be able to create and customize a character of our own.
|
||||
- While not roleplay-focused, it should still be possible to socialize and to collaborate.
|
||||
- We don't want to have to rely on a Game master to resolve things, but will rely on code for skill resolution and combat.
|
||||
- We want monsters to fight and NPCs we can talk to. So some sort of AI.
|
||||
- We want some sort of quest system and merchants to buy stuff from.
|
||||
|
||||
|
||||
## Game concept
|
||||
|
||||
With these points in mind, here's a quick blurb for our game:
|
||||
|
||||
_Recently, the nearby village discovered that the old abandoned well contained a dark secret. The bottom of the well led to a previously undiscovered dungeon of ever shifting passages. No one knew why it was there or what its purpose was, but local rumors abound. The first adventurer that went down didn't come back. The second ... brought back a handful of glittering riches._
|
||||
|
||||
_Now the rush is on - there's a dungeon to explore and coin to earn. Knaves, cutthroats, adventurers and maybe even a hero or two are coming from all over the realm to challenge whatever lurks at the bottom of that well._
|
||||
|
||||
_Local merchants and opportunists have seen a chance for profit. A camp of tents has sprung up around the old well, providing food and drink, equipment, entertainment and rumors for a price. It's a festival to enjoy before paying the entrance fee for dropping down the well to find your fate among the shadows below ..._
|
||||
|
||||
Our game will consist of two main game modes - above ground and below. The player starts above ground and is expected to do 'expeditions' into the dark. The design goal is for them to be forced back up again when their health, equipment and luck is about to run out.
|
||||
- Above, in the "dungeon festival", the player can restock and heal up, buy things and do a small set of quests. It's the only place where the characters can sleep and fully heal. They also need to spend coin here to gain XP and levels. This is a place for players to socialize and RP. There is no combat above ground except for an optional spot for non-lethal PvP.
|
||||
- Below is the mysterious dungeon. This is a procedurally generated set of rooms. Players can collaborate if they go down the well together, they will not be able to run into each other otherwise (so this works as an instance). Each room generally presents some challenge (normally a battle). Pushing deeper is more dangerous but can grant greater rewards. While the rooms could in theory go on forever, there should be a boss encounter once a player reaches deep enough.
|
||||
|
||||
Here's an overview of the topside camp for inspiration (quickly thrown together in the free version of [Inkarnate](https://inkarnate.com/)). We'll explore how to break this up into "rooms" (locations) when we get to creating the game world later.
|
||||
|
||||

|
||||
|
||||
For the rest of this lesson we'll answer and reason around the specific questions posed in the previous [Game Planning](./Beginner-Tutorial-Game-Planning.md) lesson.
|
||||
|
||||
## Administration
|
||||
|
||||
### Should your game rules be enforced by coded systems by human game masters?
|
||||
|
||||
Generally, the more work you expect human staffers/GMs to do, the less your code needs to work. To support GMs you'd need to design commands to support GM-specific actions and the type of game-mastering you want them to do. You may need to expand communication channels so you can easily talk to groups people in private and split off gaming groups from each other. RPG rules could be as simple
|
||||
as the GM sitting with the rule books and using a dice-roller for visibility.
|
||||
|
||||
GM:ing is work-intensive however, and even the most skilled and enthusiastic GM can't be awake all hours of the day to serve an international player base. The computer never needs sleep, so having the ability for players to "self-serve" their RP itch when no GMs are around is a good idea even for the most GM-heavy games.
|
||||
|
||||
On the other side of the spectrum are games with no GMs at all; all gameplay are driven either by the computer or by the interactions between players. Such games still need an active staff, but nowhere as much active involvement. Allowing for solo-play with the computer also allows players to have fun when the number of active
|
||||
players is low.
|
||||
|
||||
**EvAdventure Answer:**
|
||||
|
||||
We want EvAdventure to work entirely without depending on human GMs. That said, there'd be nothing stopping a GM from stepping in and run an adventure for some players should they want to.
|
||||
|
||||
### What is the staff hierarchy in your game? Is vanilla Evennia roles enough or do you need something else?
|
||||
|
||||
The default hierarchy is
|
||||
|
||||
- `Player` - regular players
|
||||
- `Player Helper` - can create/edit help entries
|
||||
- `Builder` - can use build commands
|
||||
- `Admin` - can kick and ban accounts
|
||||
- `Developer` - full access, usually also trusted with server access
|
||||
|
||||
There is also the _superuser_, the "owner" of the game you create when you first set up your database. This user
|
||||
goes outside the regular hierarchy and should usually only.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We are okay with keeping the default permission structure for our game.
|
||||
|
||||
### Should players be able to post out-of-characters on channels and via other means like bulletin-boards?
|
||||
|
||||
Evennia's _Channels_ are by default only available between _Accounts_. That is, for players to communicate with each
|
||||
other. By default, the `public` channel is created for general discourse.
|
||||
Channels are logged to a file and when you are coming back to the game you can view the history of a channel in case you missed something.
|
||||
|
||||
> public Hello world!
|
||||
[Public] MyName: Hello world!
|
||||
|
||||
But Channels can also be set up to work between Characters instead of Accounts. This would mean the channels would have an in-game meaning:
|
||||
|
||||
- Members of a guild could be linked telepathically.
|
||||
- Survivors of the apocalypse can communicate over walkie-talkies.
|
||||
- Radio stations you can tune into or have to discover.
|
||||
|
||||
_Bulletin boards_ are a sort of in-game forum where posts are made publicly or privately. Contrary to a channel, the messages are usually stored and are grouped into topics with replies. Evennia has no default bulletin-board system.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
In EvAdventure we will just use the default inter-account channels. We will also not be implementing any bulletin boards; instead the merchant NPCs will act as quest givers.
|
||||
|
||||
## Building
|
||||
|
||||
### How will the world be built?
|
||||
|
||||
There are two main ways to handle this:
|
||||
- Traditionally, from in-game with build-commands: This means builders creating content in their game client. This has the advantage of not requiring Python skills nor server access. This can often be a quite intuitive way to build since you are sort-of walking around in your creation as you build it. However, the developer (you) must make sure to provide build-commands that are flexible enough for builders to be able to create the content you want for your game.
|
||||
- Externally (by batchcmds): Evennia's `batchcmd` takes a text file with Evennia Commands and executes them in sequence. This allows the build process to be repeated and applied quickly to a new database during development.
|
||||
It also allows builders to use proper text-editing tools rather than writing things line-by-line in their clients. The drawback is that for their changes to go live they either need server access or they need to send their batchcode to the game administrator so they can apply the changes. Or use version control.
|
||||
- Externally (with batchcode or custom code): This is the "professional game development" approach. This gives the builders maximum power by creating the content in Python using Evennia primitives. The `batchcode` processor
|
||||
allows Evennia to apply and re-apply build-scripts that are raw Python modules. Again, this would require the builder to have server access or to use version control to share their work with the rest of the development team.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
For EvAdventure, we will build the above-ground part of the game world using batch-scripts. The world below-ground we will build procedurally, using raw code.
|
||||
|
||||
### Can only privileged Builders create things or should regular players also have limited build-capability?
|
||||
|
||||
In some game styles, players have the ability to create objects and even script them. While giving regular users the ability to create objects with in-built commands is easy and safe, actual code-creation (aka _softcode_ ) is not something Evennia supports natively.
|
||||
|
||||
Regular, untrusted users should never be allowed to execute raw Python
|
||||
code (such as what you can do with the `py` command). You can
|
||||
[read more about Evennia's stance on softcode here](../../../Concepts/Soft-Code.md). If you want users to do limited scripting, it's suggested that this is accomplished by adding more powerful build-commands for them to use.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
For our tutorial-game, we will only allow privileged builders and admins to modify the world.
|
||||
|
||||
## Systems
|
||||
|
||||
### Do you base your game off an existing RPG system or make up your own?
|
||||
|
||||
There is a plethora of options out there, and what you choose depends on the game you want. It can be tempting to grab a short free-form ruleset, but remember that the computer does not have any intuitiion or common sense to interpret the rules like a human GM could. Conversely, if you pick a very 'crunchy' game system, with detailed simulation of the real world, remember that you'll need to actually _code_ all those exceptions and tables yourself.
|
||||
|
||||
For speediest development, what you want is a game with a _consolidated_ resolution mechanic - one you can code once and then use in a lot of situations. But you still want enough rules to help telling the computer how various situations should be resolved (combat is the most common system that needs such structure).
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
For this tutorial, we will make use of [Knave](https://www.drivethrurpg.com/product/250888/Knave), a very light [OSR](https://en.wikipedia.org/wiki/Old_School_Renaissance) ruleset by Ben Milton. It's only a few pages long but highly compatible with old-school D&D games. It's consolidates all rules around a few opposed d20 rolls and includes clear rules for combat, inventory, equipment and so on. Since _Knave_ is a tabletop RPG, we will have to do some minor changes here and there to fit it to the computer medium.
|
||||
|
||||
_Knave_ is available under a Creative Commons Attributions 4.0 License, meaning it can be used for derivative work (even commercially). The above link allows you to purchase the PDF and supporting the author. Alternatively you can find unofficial fan releases of the rules [on this page](https://dungeonsandpossums.com/2020/04/some-great-knave-rpg-resources/).
|
||||
|
||||
|
||||
### What are the game mechanics? How do you decide if an action succeeds or fails?
|
||||
|
||||
This follows from the RPG system decided upon in the previous question.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
_Knave_ gives every character a set of six traditional stats: Strength, Intelligence, Dexterity, Constitution, Intelligence, Wisdom and Charisma. Each has a value from +1 to +10. To find its "Defense" value, you add 10.
|
||||
|
||||
You have Strength +1. Your Strength-Defense is 10 + 1 = 11
|
||||
|
||||
To make a check, say an arm-wrestling challenge you roll a twenty-sided die (d20) and add your stat. You have to roll higher than the opponents defense for that stat.
|
||||
|
||||
I have Strength +1, my opponent has a Strength of +2. To beat them in arm wrestling I must roll d20 + 1 and hope to get higher than 12, which is their Strength defense (10 + 2).
|
||||
|
||||
If you attack someone you do the same, except you roll against their `Armor` defense. If you rolled higher, you roll for how much damage you do (depends on your weapon).
|
||||
You can have _advantage_ or _disadvantage_ on a roll. This means rolling 2d20 and picking highest or lowest value.
|
||||
|
||||
In Knave, combat is turn-based. In our implementation we'll also play turn-based, but we'll resolve everything _simultaneously_. This changes _Knave_'s feel quite a bit, but is a case where the computer can do things not practical to do when playing around a table.
|
||||
|
||||
There are also a few tables we'll need to implement. For example, if you lose all health, there's a one-in-six chance you'll die outright. We'll keep this perma-death aspect, but make it very easy to start a new character and jump back in.
|
||||
|
||||
> In this tutorial we will not add opportunities to make use of all of the character stats, making some, like strength, intelligence and dexterity more useful than others. In a full game, one would want to expand so a user can utilize all of their character's strengths.
|
||||
|
||||
### Does the flow of time matter in your game - does night and day change? What about seasons?
|
||||
|
||||
Most commonly, game-time runs faster than real-world time. There are
|
||||
a few advantages with this:
|
||||
|
||||
- Unlike in a single-player game, you can't fast-forward time in a multiplayer game if you are waiting for something, like NPC shops opening.
|
||||
- Healing and other things that we know takes time will go faster while still being reasonably 'realistic'.
|
||||
|
||||
The main drawback is for games with slower roleplay pace. While you are having a thoughtful roleplaying scene over dinner, the game world reports that two days have passed. Having a slower game time than real-time is a less common, but possible solution for such games.
|
||||
|
||||
It is however _not_ recommended to let game-time exactly equal the speed of real time. The reason for this is that people will join your game from all around the world, and they will often only be able to play at particular times of their day. With a game-time drifting relative real-time, everyone will eventually be able to experience both day and night in the game.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
The passage of time will have no impact on our particular game example, so we'll go with Evennia's default, which is that the game-time runs two times faster than real time.
|
||||
|
||||
### Do you want changing, global weather or should weather just be set manually in roleplay?
|
||||
|
||||
A weather system is a good example of a game-global system that affects a subset of game entities (outdoor rooms).
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We'll not change the weather, but will add some random messages to echo through
|
||||
the game world at random intervals just to show the principle.
|
||||
|
||||
### Do you want a coded world-economy or just a simple barter system? Or no formal economy at all?
|
||||
This is a big question and depends on how deep and interconnected the virtual transactions are that are happening in the game. Shop prices could rice and drop due to supply and demand, supply chains could involve crafting and production. One also could consider adding money sinks and manipulate the in-game market to combat inflation.
|
||||
|
||||
The [Barter](../../../Contribs/Contrib-Barter.md) contrib provides a full interface for trading with another player in a safe way.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will not deal with any of this complexity. We will allow for players to buy from npc sellers and players will be able to trade using the normal `give` command.
|
||||
|
||||
### Do you have concepts like reputation and influence?
|
||||
|
||||
These are useful things for a more social-interaction heavy game.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will not include them for this tutorial. Adding the Barter contrib is simple though.
|
||||
|
||||
### Will your characters be known by their name or only by their physical appearance?
|
||||
|
||||
This is a common thing in RP-heavy games. Others will only see you as "The tall woman" until you introduce yourself and they 'recognize' you with a name. Linked to this is the concept of more complex emoting and posing.
|
||||
|
||||
Implementing such a system is not trivial, but the [RPsystem](../../../Contribs/Contrib-RPSystem.md) Evennia contrib offers a ready system with everything needed for free emoting, recognizing people by their appearance and more.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will not use any special RP systems for this tutorial. Adding the RPSystem contrib is a good extra expansion though!
|
||||
|
||||
## Rooms
|
||||
|
||||
### Is a simple room description enough or should the description be able to change?
|
||||
|
||||
Changing room descriptions for day and night, winder and summer is actually quite easy to do, but looks very impressive. We happen to know there is also a contrib that helps with this, so we'll show how to include that.
|
||||
|
||||
There is an [Extended Room](../../../Contribs/Contrib-Extended-Room.md) contrib that adds a Room type that is aware of the time-of-day as well as seasonal variations.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will stick to a normal room in this tutorial and let the world be in a perpetual daylight. Making Rooms into ExtendedRooms is not hard though.
|
||||
|
||||
### Should the room have different statuses?
|
||||
|
||||
One could picture weather making outdoor rooms wet, cold or burnt. In rain, bow strings could get wet and fireballs fizz out. In a hot room, characters could require drinking more water, or even take damage if not finding shelter.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
For the above-ground we need to be able to disable combat all rooms except for the PvP location. We also need to consider how to auto-generate the rooms under ground. So we probably will need some statuses to control that.
|
||||
|
||||
Since each room under ground should present some sort of challenge, we may need a few different room types different from the above-ground Rooms.
|
||||
|
||||
### Can objects be hidden in the room? Can a person hide in the room?
|
||||
|
||||
This ties into if you have hide/stealth mechanics. Maybe you could evesdrop or attack out of hiding.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will not model hiding and stealth. This will be a game of honorable face-to-face conflict.
|
||||
|
||||
## Objects
|
||||
|
||||
### How numerous are your objects? Do you want large loot-lists or are objects just role playing props?
|
||||
|
||||
This also depends on the type of game. In a pure freeform RPG, most objects may be 'imaginary' and just appearing in fiction. If the game is more coded, you want objects with properties that the computer can measure, track and calculate. In many roleplaying-heavy games, you find a mixture of the two, with players imagining items for roleplaying scenes, but only using 'real' objects to resolve conflicts.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will want objects with properties, like weapons and potions and such. Monsters should drop loot even though our list of objects will not be huge in this example game.
|
||||
|
||||
### Is each coin a separate object or do you just store a bank account value?
|
||||
|
||||
The advantage of having multiple items is that it can be more immersive. The drawback is that it's also very fiddly to deal with individual coins, especially if you have to deal with different currencies.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
_Knave_ uses the "copper" as the base coin and so will we. Knave considers the weight of coin and one inventory "slot" can hold 100 coins. So we'll implement a "coin item" to represent many coins.
|
||||
|
||||
### Do multiple similar objects form stack and how are those stacks handled in that case?
|
||||
|
||||
If you drop two identical apples on the ground, Evennia will default to show this in the room as "two apples", but this is just a visual effect - there are still two apple-objects in the room. One could picture instead merging the two into a single object "X nr of apples" when you drop the apples.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will keep Evennia's default.
|
||||
|
||||
### Does an object have weight or volume (so you cannot carry an infinite amount of them)?
|
||||
|
||||
Limiting carrying weight is one way to stop players from hoarding. It also makes it more important for players to pick only the equipment they need. Carrying limits can easily come across as annoying to players though, so one needs to be careful with it.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
_Knave_ limits your inventory to `Constitution + 10` "slots", where most items take up one slot and some large things, like armor, uses two. Small items (like rings) can fit 2-10 per slot and you can fit 100 coins in a slot. This is an important game mechanic to limit players from hoarding. Especially since you need coin to level up.
|
||||
|
||||
### Can objects be broken? Can they be repaired?
|
||||
|
||||
Item breakage is very useful for a game economy; breaking weapons adds tactical considerations (if it's not too common, then it becomes annoying) and repairing things gives work for crafting players.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
In _Knave_, items will break if you make a critical failure on using them (rolls a native 1 on d20). This means they lose a level of `quality` and once at 0, it's unusable. We will not allow players to repair, but we could allow merchants to repair items for a fee.
|
||||
|
||||
### Can you fight with a chair or a flower or must you use a special 'weapon' kind of thing?
|
||||
|
||||
Traditionally, only 'weapons' could be used to fight with. In the past this was a useful
|
||||
simplification, but with Python classes and inheritance, it's not actually more work to just let all items in game work as a weapon in a pinch.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
Since _Knave_ deals with weapon lists and positions where items can be wielded, we will have a separate "Weapon" class for everything you can use for fighting. So, you won't be able to fight with a chair (unless we make it a weapon-inherited chair).
|
||||
|
||||
### Will characters be able to craft new objects?
|
||||
|
||||
Crafting is a common feature in multiplayer games. In code it usually means using a skill-check to combine base ingredients from a fixed recipe in order to create a new item. The classic example is to combine _leather straps_, a _hilt_, a _pommel_ and a _blade_ to make a new _sword_.
|
||||
|
||||
A full-fledged crafting system could require multiple levels of crafting, including having to mine for ore or cut down trees for wood.
|
||||
|
||||
Evennia's [Crafting](../../../Contribs/Contrib-Crafting.md) contrib adds a full crafting system to any game. It's based on [Tags](../../../Components/Tags.md), meaning that pretty much any object can be made usable for crafting, even used in an unexpected way.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
In our case we will not add any crafting in order to limit the scope of our game. Maybe NPCs will be able to repair items - for a cost?
|
||||
|
||||
### Should mobs/NPCs have some sort of AI?
|
||||
|
||||
As a rule, you should not hope to fool anyone into thinking your AI is actually intelligent. The best you will be able to do is to give interesting results and unless you have a side-gig as an AI researcher, users will likely not notice any practical difference between a simple state-machine and you spending a lot of time learning
|
||||
how to train a neural net.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
For this tutorial, we will show how to add a simple state-machine AI for monsters. NPCs will only be shop-keepers and quest-gives so they won't need any real AI to speak of.
|
||||
|
||||
### Are NPCs and mobs different entities? How do they differ?
|
||||
|
||||
"Mobs" or "mobiles" are things that move around. This is traditionally monsters you can fight with, but could also be city guards or the baker going to chat with the neighbor. Back in the day, they were often fundamentally different these days it's often easier to just make NPCs and mobs essentially the same thing.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
In EvAdventure, Monsters and NPCs do very different things, so they will be different classes, sharing some code where possible.
|
||||
|
||||
### _Should there be NPCs giving quests? If so, how do you track Quest status?
|
||||
|
||||
Quests are a staple of many classic RPGs.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will design a simple quest system with some simple conditions for success, like carrying the right item or items back to the quest giver.
|
||||
|
||||
## Characters
|
||||
|
||||
### Can players have more than one Character active at a time or are they allowed to multi-play?
|
||||
|
||||
Since Evennia differentiates between `Sessions` (the client-connection to the game), `Accounts` and `Character`s, it natively supports multi-play. This is controlled by the `MULTISESSION_MODE` setting, which has a value from `0` (default) to `3`.
|
||||
|
||||
- `0`- One Character per Account and one Session per Account. This means that if you login to the same
|
||||
account from another client you'll be disconnected from the first. When creating a new account, a Character
|
||||
will be auto-created with the same name as your Account. This is default mode and mimics legacy code bases
|
||||
which had no separation between Account and Character.
|
||||
- `1` - One Character per Account, multiple Sessions per Account. So you can connect simultaneously from
|
||||
multiple clients and see the same output in all of them.
|
||||
- `2` - Multiple Characters per Account, one Session per Character. This will not auto-create a same-named
|
||||
Character for you, instead you get to create/choose between a number of Characters up to a max limit given by
|
||||
the `MAX_NR_CHARACTERS` setting (default 1). You can play them all simultaneously if you have multiple clients
|
||||
open, but only one client per Character.
|
||||
- `3` - Multiple Characters per Account, Multiple Sessions per Character. This is like mode 2, except players
|
||||
can control each Character from multiple clients, seeing the same output from each Character.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
Due to the nature of _Knave_, characters are squishy and probably short-lived. So it makes little sense to keep a stable of them. We'll use use mode 0 or 1.
|
||||
|
||||
### How does the character-generation work?
|
||||
|
||||
There are a few common ways to do character generation:
|
||||
|
||||
- Rooms. This is the traditional way. Each room's description tells you what command to use to modify your character. When you are done you move to the next room. Only use this if you have another reason for using a room, like having a training dummy to test skills on, for example.
|
||||
- A Menu. The Evennia _EvMenu_ system allows you to code very flexible in-game menus without needing to walk between rooms. You can both have a step-by-step menu (a 'wizard') or allow the user to jump between the
|
||||
steps as they please. This tends to be a lot easier for newcomers to understand since it doesn't require
|
||||
using custom commands they will likely never use again after this.
|
||||
- Questions. A fun way to build a character is to answer a series of questions. This is usually implemented with a sequential menu.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
Knave randomizes almost aspects of the Character generation. We'll use a menu to let the player add their name and sex as well as do the minor re-assignment of stats allowed by the rules.
|
||||
|
||||
### How do you implement different "classes" or "races"?
|
||||
|
||||
The way classes and races work in most RPGs is that they act as static 'templates' that inform which bonuses and special abilities you have. Much of this only comes into play during character generation or when leveling up.
|
||||
|
||||
Often all we need to store on the Character is _which_ class and _which_ race they have; the actual logic can sit in Python code and just be looked up when we need it.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
There are no races and no classes in _Knave_. Every character is a human.
|
||||
|
||||
### If a Character can hide in a room, what skill will decide if they are detected?
|
||||
|
||||
Hiding means a few things.
|
||||
- The Character should not appear in the room's description / character list
|
||||
- Others hould not be able to interact with a hidden character. It'd be weird if you could do `attack <name>`
|
||||
or `look <name>` if the named character is in hiding.
|
||||
- There must be a way for the person to come out of hiding, and probably for others to search or accidentally
|
||||
find the person (probably based on skill checks).
|
||||
- The room will also need to be involved, maybe with some modifier as to how easy it is to hide in the room.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will not be including a hide-mechanic in EvAdventure.
|
||||
|
||||
### What does the skill tree look like? Can a Character gain experience to improve? By killing enemies? Solving quests? By roleplaying?
|
||||
|
||||
Gaining experience points (XP) and improving one's character is a staple of roleplaying games. There are many
|
||||
ways to implement this:
|
||||
- Gaining XP from kills is very common; it's easy to let a monster be 'worth' a certain number of XP and it's easy to tell when you should gain it.
|
||||
- Gaining XP from quests is the same - each quest is 'worth' XP and you get them when completing the test.
|
||||
- Gaining XP from roleplay is harder to define. Different games have tried a lot of different ways to do this:
|
||||
- XP from being online - just being online gains you XP. This inflates player numbers but many players may
|
||||
just be lurking and not be actually playing the game at any given time.
|
||||
- XP from roleplaying scenes - you gain XP according to some algorithm analyzing your emotes for 'quality',
|
||||
how often you post, how long your emotes are etc.
|
||||
- XP from actions - you gain XP when doing things, anything. Maybe your XP is even specific to each action, so
|
||||
you gain XP only for running when you run, XP for your axe skill when you fight with an axe etc.
|
||||
- XP from fails - you only gain XP when failing rolls.
|
||||
- XP from other players - other players can award you XP for good RP.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will use an alternative rule in _Knave_, where Characters gain XP by spending coins they carry back from their adventures. The above-ground merchants will allow you to spend your coins and exchange them for XP 1:1. Each level costs 1000 coins. Every level you have `1d8 * new level` (minimum what you had before + 1) HP, and can raise 3 different ability scores by 1 (max +10). There are no skills in _Knave_, but the principle of increasing them would be the same.
|
||||
|
||||
### May player-characters attack each other (PvP)?
|
||||
|
||||
Deciding this affects the style of your entire game. PvP makes for exciting gameplay but it opens a whole new can of worms when it comes to "fairness". Players will usually accept dying to an overpowered NPC dragon. They will not be as accepting if they perceive another player as being overpowered. PvP means that you
|
||||
have to be very careful to balance the game - all characters does not have to be exactly equal but they should all be viable to play a fun game with.
|
||||
|
||||
PvP does not only mean combat though. Players can compete in all sorts of ways, including gaining influence in a political game or gaining market share when selling their crafted merchandise.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will allow PvP only in one place - a special Dueling location where players can play-fight each other for training and prestige, but not actually get killed. Otherwise no PvP will be allowed. Note that without a full Barter system in place (just regular `give`, it makes it theoretically easier for players to scam one another.
|
||||
|
||||
### What are the penalties of defeat? Permanent death? Quick respawn? Time in prison?
|
||||
|
||||
This is another big decision that strongly affects the mood and style of your game.
|
||||
|
||||
Perma-death means that once your character dies, it's gone and you have to make a new one.
|
||||
|
||||
- It allows for true heroism. If you genuinely risk losing your character of two years to fight the dragon,
|
||||
your triumph is an actual feat.
|
||||
- It limits the old-timer dominance problem. If long-time players dies occationally, it will open things
|
||||
up for newcomers.
|
||||
- It lowers inflation, since the hoarded resources of a dead character can be removed.
|
||||
- It gives capital punishment genuine discouraging power.
|
||||
- It's realistic.
|
||||
|
||||
Perma-death comes with some severe disadvantages however.
|
||||
|
||||
- Many players say they like the _idea_ of permadeath except when it could happen to them.
|
||||
- Some players refuse to take any risks if death is permanent.
|
||||
- It may make players even more reluctant to play conflict-driving 'bad guys'.
|
||||
- Balancing PvP becomes very hard. Fairness and avoiding exploits becomes critical when the outcome
|
||||
is permanent.
|
||||
|
||||
For these reasons, it's very common to do hybrid systems. Some tried variations:
|
||||
|
||||
- NPCs cannot kill you, only other players can.
|
||||
- Death is permanent, but it's difficult to actually die - you are much more likely to end up being severely hurt/incapacitated.
|
||||
- You can pre-pay 'insurance' to magically/technologically avoid actually dying. Only if don't have insurance will
|
||||
you die permanently.
|
||||
- Death just means harsh penalties, not actual death.
|
||||
- When you die you can fight your way back to life from some sort of afterlife.
|
||||
- You'll only die permanently if you as a player explicitly allows it.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
In _Knave_, when you hit 0 HP, you roll on a death table, with a 1/8 chance of immediate death (otherwise you lose
|
||||
points in a random stat). We will offer an "Insurance" that allows you to resurrect if you carry enough coin on you when
|
||||
you die. If not, you are perma-dead and have to create a new character (which is easy and quick since it's mostly
|
||||
randomized).
|
||||
|
||||
## Conclusions
|
||||
|
||||
Going through the questions has helped us get a little bit more of a feel for the game we want to do. There are many, many other things we could ask ourselves, but if we can cover these points we will be a good way towards a complete,
|
||||
playable game!
|
||||
|
||||
In the last of these planning lessons we'll sketch out how these ideas will map to Evennia.
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
# Planning the use of some useful contribs
|
||||
|
||||
Evennia is deliberately bare-bones out of the box. The idea is that you should be as unrestricted as possible
|
||||
in designing your game. This is why you can easily replace the few defaults we have and why we don't try to
|
||||
prescribe any major game systems on you.
|
||||
|
||||
That said, Evennia _does_ offer some more game-opinionated _optional_ stuff. These are referred to as _Contribs_
|
||||
and is an ever-growing treasure trove of code snippets, concepts and even full systems you can pick and choose
|
||||
from to use, tweak or take inspiration from when you make your game.
|
||||
|
||||
The [Contrib overview](../../../Contribs/Contribs-Overview.md) page gives the full list of the current roster of contributions. On
|
||||
this page we will review a few contribs we will make use of for our game. We will do the actual installation
|
||||
of them when we start coding in the next part of this tutorial series. While we will introduce them here, you
|
||||
are wise to read their doc-strings yourself for the details.
|
||||
|
||||
This is the things we know we need:
|
||||
|
||||
- A barter system
|
||||
- Character generation
|
||||
- Some concept of wearing armor
|
||||
- The ability to roll dice
|
||||
- Rooms with awareness of day, night and season
|
||||
- Roleplaying with short-descs, poses and emotes
|
||||
- Quests
|
||||
- Combat (with players and against monsters)
|
||||
|
||||
## Barter contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.game_systems.barter.md)
|
||||
|
||||
Reviewing this contrib suggests that it allows for safe trading between two parties. The basic principle
|
||||
is that the parties puts up the stuff they want to sell and the system will guarantee that these systems are
|
||||
exactly what is being offered. Both sides can modify their offers (bartering) until both mark themselves happy
|
||||
with the deal. Only then the deal is sealed and the objects are exchanged automatically. Interestingly, this
|
||||
works just fine for money too - just put coin objects on one side of the transaction.
|
||||
|
||||
Sue > trade Tom: Hi, I have a necklace to sell; wanna trade for a healing potion?
|
||||
Tom > trade Sue: Hm, I could use a necklace ...
|
||||
<both accepted trade. Start trade>
|
||||
Sue > offer necklace: This necklace is really worth it.
|
||||
Tom > evaluate necklace:
|
||||
<Tom sees necklace stats>
|
||||
Tom > offer ration: I don't have a healing potion, but I'll trade you an iron ration!
|
||||
Sue > Hey, this is a nice necklace, I need more than a ration for it...
|
||||
Tom > offer ration, 10gold: Ok, a ration and 10 gold as well.
|
||||
Sue > accept: Ok, that sounds fair!
|
||||
Tom > accept: Good! Nice doing business with you.
|
||||
<goods change hands automatically. Trade ends>
|
||||
|
||||
Arguably, in a small game you are just fine to just talk to people and use `give` to do the exchange. The
|
||||
barter system guarantees trading safety if you don't trust your counterpart to try to give you the wrong thing or
|
||||
to run away with your money.
|
||||
|
||||
We will use the barter contrib as an optional feature for player-player bartering. More importantly we can
|
||||
add it for NPC shopkeepers and expand it with a little AI, which allows them to potentially trade in other
|
||||
things than boring gold coin.
|
||||
|
||||
## Clothing contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.game_systems.clothing.md)
|
||||
|
||||
This contrib provides a full system primarily aimed at wearing clothes, but it could also work for armor. You wear
|
||||
an object in a particular location and this will then be reflected in your character's description. You can
|
||||
also add roleplaying flavor:
|
||||
|
||||
> wear helmet slightly askew on her head
|
||||
look self
|
||||
Username is wearing a helmet slightly askew on her head.
|
||||
|
||||
By default there are no 'body locations' in this contrib, we will need to expand on it a little to make it useful
|
||||
for things like armor. It's a good contrib to build from though, so that's what we'll do.
|
||||
|
||||
## Dice contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.rpg.dice.md)
|
||||
|
||||
The dice contrib presents a general dice roller to use in game.
|
||||
|
||||
> roll 2d6
|
||||
Roll(s): 2 and 5. Total result is 7.
|
||||
> roll 1d100 + 2
|
||||
Roll(s): 43. Total result is 47
|
||||
> roll 1d20 > 12
|
||||
Roll(s): 7. Total result is 7. This is a failure (by 5)
|
||||
> roll/hidden 1d20 > 12
|
||||
Roll(s): 18. Total result is 17. This is a success (by 6). (not echoed)
|
||||
|
||||
The contrib also has a python function for producing these results in-code. However, while
|
||||
we will emulate rolls for our rule system, we'll do this as simply as possible with Python's `random`
|
||||
module.
|
||||
|
||||
So while this contrib is fun to have around for GMs or for players who want to get a random result
|
||||
or play a game, we will not need it for the core of our game.
|
||||
|
||||
## Extended room contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.grid.extended_room.md)
|
||||
|
||||
This is a custom Room typeclass that changes its description based on time of day and season.
|
||||
|
||||
For example, at night, in wintertime you could show the room as being dark and frost-covered while in daylight
|
||||
at summer it could describe a flowering meadow. The description can also contain special markers, so
|
||||
`<morning> ... </morning>` would include text only visible at morning.
|
||||
|
||||
The extended room also supports _details_, which are things to "look at" in the room without there having
|
||||
to be a separate database object created for it. For example, a player in a church may do `look window` and
|
||||
get a description of the windows without there needing to be an actual `window` object in the room.
|
||||
|
||||
Adding all those extra descriptions can be a lot of work, so they are optional; if not given the room works
|
||||
like a normal room.
|
||||
|
||||
The contrib is simple to add and provides a lot of optional flexibility, so we'll add it to our
|
||||
game, why not!
|
||||
|
||||
## RP-System contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.rpg.rpsystem.md)
|
||||
|
||||
This contrib adds a full roleplaying subsystem to your game. It gives every character a "short-description"
|
||||
(sdesc) that is what people will see when first meeting them. Let's say Tom has an sdesc "A tall man" and
|
||||
Sue has the sdesc "A muscular, blonde woman"
|
||||
|
||||
Tom > look
|
||||
Tom: <room desc> ... You see: A muscular, blonde woman
|
||||
Tom > emote /me smiles to /muscular.
|
||||
Tom: Tom smiles to A muscular, blonde woman.
|
||||
Sue: A tall man smiles to Sue.
|
||||
Tom > emote Leaning forward, /me says, "Well hello, what's yer name?"
|
||||
Tom: Leaning forward, Tom says, "Well hello..."
|
||||
Sue: Leaning forward, A tall man says, "Well hello, what's yer name?"
|
||||
Sue > emote /me grins. "I'm Angelica", she says.
|
||||
Sue: Sue grins. "I'm Angelica", she says.
|
||||
Tom: A muscular, blonde woman grins. "I'm Angelica", she says.
|
||||
Tom > recog muscular Angelica
|
||||
Tom > emote /me nods to /angelica: "I have a message for you ..."
|
||||
Tom: Tom nods to Angelica: "I have a message for you ..."
|
||||
Sue: A tall man nods to Sue: "I have a message for you ..."
|
||||
|
||||
Above, Sue introduces herself as "Angelica" and Tom uses this info to `recoc` her as "Angelica" hereafter. He
|
||||
could have `recoc`-ed her with whatever name he liked - it's only for his own benefit. There is no separate
|
||||
`say`, the spoken words are embedded in the emotes in quotes `"..."`.
|
||||
|
||||
The RPSystem module also includes options for `poses`, which help to establish your position in the room
|
||||
when others look at you.
|
||||
|
||||
Tom > pose stands by the bar, looking bored.
|
||||
Sue > look
|
||||
Sue: <room desc> ... A tall man stands by the bar, looking bored.
|
||||
|
||||
You can also wear a mask to hide your identity; your sdesc will then be changed to the sdesc of the mask,
|
||||
like `a person with a mask`.
|
||||
|
||||
The RPSystem gives a lot of roleplaying power out of the box, so we will add it. There is also a separate
|
||||
[rplanguage](../../../api/evennia.contrib.rpg.rpsystem.md) module that integrates with the spoken words in your emotes and garbles them if you don't understand
|
||||
the language spoken. In order to restrict the scope we will not include languages for the tutorial game.
|
||||
|
||||
|
||||
## Talking NPC contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.tutorials.talking_npc.md)
|
||||
|
||||
This exemplifies an NPC with a menu-driven dialogue tree. We will not use this contrib explicitly, but it's
|
||||
good as inspiration for how we'll do quest-givers later.
|
||||
|
||||
## Traits contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.rpg.traits.md)
|
||||
|
||||
An issue with dealing with roleplaying attributes like strength, dexterity, or skills like hunting, sword etc
|
||||
is how to keep track of the values in the moment. Your strength may temporarily be buffed by a strength-potion.
|
||||
Your swordmanship may be worse because you are encumbered. And when you drink your health potion you must make
|
||||
sure that those +20 health does not bring your health higher than its maximum. All this adds complexity.
|
||||
|
||||
The _Traits_ contrib consists of several types of objects to help track and manage values like this. When
|
||||
installed, the traits are accessed on a new handler `.traits`, for example
|
||||
|
||||
> py self.traits.hp.value
|
||||
100
|
||||
> py self.traits.hp -= 20 # getting hurt
|
||||
> py self.traits.hp.value
|
||||
80
|
||||
> py self.traits.hp.reset() # drink a potion
|
||||
> py self.traits.hp.value
|
||||
100
|
||||
|
||||
A Trait is persistent (it uses an Attribute under the hood) and tracks changes, min/max and other things
|
||||
automatically. They can also be added together in various mathematical operations.
|
||||
|
||||
The contrib introduces three main Trait-classes
|
||||
|
||||
- _Static_ traits for single values like str, dex, things that at most gets a modifier.
|
||||
- _Counters_ is a value that never moves outside a given range, even with modifiers. For example a skill
|
||||
that can at most get a maximum amount of buff. Counters can also easily be _timed_ so that they decrease
|
||||
or increase with a certain rate per second. This could be good for a time-limited curse for example.
|
||||
- _Gauge_ is like a fuel-gauge; it starts at a max value and then empties gradually. This is perfect for
|
||||
things like health, stamina and the like. Gauges can also change with a rate, which works well for the
|
||||
effects of slow poisons and healing both.
|
||||
|
||||
```
|
||||
> py self.traits.hp.value
|
||||
100
|
||||
> py self.traits.hp.rate = -1 # poisoned!
|
||||
> py self.traits.hp.ratetarget = 50 # stop at 50 hp
|
||||
# Wait 30s
|
||||
> py self.traits.hp.value
|
||||
70
|
||||
# Wait another 30s
|
||||
> py self.traits.hp.value
|
||||
50 # stopped at 50
|
||||
> py self.traits.hp.rate = 0 # no more poison
|
||||
> py self.traits.hp.rate = 5 # healing magic!
|
||||
# wait 5s
|
||||
> pyself.traits.hp.value
|
||||
75
|
||||
```
|
||||
|
||||
Traits will be very practical to use for our character sheets.
|
||||
|
||||
## Turnbattle contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.game_systems.turnbattle.md)
|
||||
|
||||
This contrib consists of several implementations of a turn-based combat system, divivided into complexity:
|
||||
|
||||
- basic - initiative and turn order, attacks against defense values, damage.
|
||||
- equip - considers weapons and armor, wielding and weapon accuracy.
|
||||
- items - adds usable items with conditions and status effects
|
||||
- magic - adds spellcasting system using MP.
|
||||
- range - adds abstract positioning and 1D movement to differentiate between melee and ranged attacks.
|
||||
|
||||
The turnbattle system is comprehensive, but it's meant as a base to start from rather than offer
|
||||
a complete system. It's also not built with _Traits_ in mind, so we will need to adjust it for that.
|
||||
|
||||
## Conclusions
|
||||
|
||||
With some contribs selected, we have pieces to build from and don't have to write everything from scratch.
|
||||
We will need Quests and will likely need to do a bunch of work on Combat to adapt the combat contrib
|
||||
to our needs.
|
||||
|
||||
We will now move into actually starting to implement our tutorial game
|
||||
in the next part of this tutorial series. When doing this for yourself, remember to refer
|
||||
back to your planning and adjust it as you learn what works and what does not.
|
||||
|
||||
|
||||
|
|
@ -1,425 +0,0 @@
|
|||
# Planning our tutorial game
|
||||
|
||||
Using the general plan from last lesson we'll now establish what kind of game we want to create for this tutorial.
|
||||
Remembering that we need to keep the scope down, let's establish some parameters.
|
||||
Note that for your own
|
||||
game you don't _need_ to agree/adopt any of these. Many game-types need more or much less than this.
|
||||
But this makes for good, instructive examples.
|
||||
|
||||
- To have something to refer to rather than just saying "our tutorial game" over and over, we'll
|
||||
name it ... _EvAdventure_.
|
||||
- We want EvAdventure be a small game we can play ourselves for fun, but which could in principle be expanded
|
||||
to something more later.
|
||||
- Let's go with a fantasy theme, it's well understood.
|
||||
- We'll use some existing, simple RPG system.
|
||||
- We want to be able to create and customize a character of our own.
|
||||
- We want the tools to roleplay with other players.
|
||||
- We don't want to have to rely on a Game master to resolve things, but will rely on code for skill resolution
|
||||
and combat.
|
||||
- We want monsters to fight and NPCs we can talk to. So some sort of AI.
|
||||
- We want to be able to buy and sell stuff, both with NPCs and other players.
|
||||
- We want some sort of crafting system.
|
||||
- We want some sort of quest system.
|
||||
|
||||
Let's answer the questions from the previous lesson and discuss some of the possibilities.
|
||||
|
||||
## Administration
|
||||
|
||||
### Should your game rules be enforced by coded systems by human game masters?
|
||||
|
||||
Generally, the more work you expect human staffers/GMs to do, the less your code needs to work. To
|
||||
support GMs you'd need to design commands to support GM-specific actions and the type of game-mastering
|
||||
you want them to do. You may need to expand communication channels so you can easily
|
||||
talk to groups people in private and split off gaming groups from each other. RPG rules could be as simple
|
||||
as the GM sitting with the rule books and using a dice-roller for visibility.
|
||||
|
||||
GM:ing is work-intensive however, and even the most skilled and enthusiastic GM can't be awake all hours
|
||||
of the day to serve an international player base. The computer never needs sleep, so having the ability for
|
||||
players to "self-serve" their RP itch when no GMs are around is a good idea even for the most GM-heavy games.
|
||||
|
||||
On the other side of the spectrum are games with no GMs at all; all gameplay are driven either by the computer
|
||||
or by the interactions between players. Such games still need an active staff, but nowhere as much active
|
||||
involvement. Allowing for solo-play with the computer also allows players to have fun when the number of active
|
||||
players is low.
|
||||
|
||||
We want EvAdventure to work entirely without depending on human GMs. That said, there'd be nothing
|
||||
stopping a GM from stepping in and run an adventure for some players should they want to.
|
||||
|
||||
### What is the staff hierarchy in your game? Is vanilla Evennia roles enough or do you need something else?
|
||||
|
||||
The default hierarchy is
|
||||
|
||||
- `Player` - regular players
|
||||
- `Player Helper` - can create/edit help entries
|
||||
- `Builder` - can use build commands
|
||||
- `Admin` - can kick and ban accounts
|
||||
- `Developer` - full access, usually also trusted with server access
|
||||
|
||||
There is also the _superuser_, the "owner" of the game you create when you first set up your database. This user
|
||||
goes outside the regular hierarchy and should usually only.
|
||||
|
||||
We are okay with keeping this structure for our game.
|
||||
|
||||
### Should players be able to post out-of-characters on channels and via other means like bulletin-boards?
|
||||
|
||||
Evennia's _Channels_ are by default only available between _Accounts_. That is, for players to communicate with each
|
||||
other. By default, the `public` channel is created for general discourse.
|
||||
Channels are logged to a file and when you are coming back to the game you can view the history of a channel
|
||||
in case you missed something.
|
||||
|
||||
> public Hello world!
|
||||
[Public] MyName: Hello world!
|
||||
|
||||
But Channels can also be set up to work between Characters instead of Accounts. This would mean the channels
|
||||
would have an in-game meaning:
|
||||
|
||||
- Members of a guild could be linked telepathically.
|
||||
- Survivors of the apocalypse can communicate over walkie-talkies.
|
||||
- Radio stations you can tune into or have to discover.
|
||||
|
||||
_Bulletin boards_ are a sort of in-game forum where posts are made publicly or privately. Contrary to a channel,
|
||||
the messages are usually stored and are grouped into topics with replies. Evennia has no default bulletin-board
|
||||
system.
|
||||
|
||||
In EvAdventure we will just use the default inter-account channels. We will also not be implementing any
|
||||
bulletin boards.
|
||||
|
||||
## Building
|
||||
|
||||
### How will the world be built?
|
||||
|
||||
There are two main ways to handle this:
|
||||
- Traditionally, from in-game with build-commands: This means builders creating content in their game
|
||||
client. This has the advantage of not requiring Python skills nor server access. This can often be a quite
|
||||
intuitive way to build since you are sort-of walking around in your creation as you build it. However, the
|
||||
developer (you) must make sure to provide build-commands that are flexible enough for builders to be able to
|
||||
create the content you want for your game.
|
||||
- Externally (by batchcmds): Evennia's `batchcmd` takes a text file with Evennia Commands and executes them
|
||||
in sequence. This allows the build process to be repeated and applied quickly to a new database during development.
|
||||
It also allows builders to use proper text-editing tools rather than writing things line-by-line in their clients.
|
||||
The drawback is that for their changes to go live they either need server access or they need to send their
|
||||
batchcode to the game administrator so they can apply the changes. Or use version control.
|
||||
- Externally (with batchcode or custom code): This is the "professional game development" approach. This gives the
|
||||
builders maximum power by creating the content in Python using Evennia primitives. The `batchcode` processor
|
||||
allows Evennia to apply and re-apply build-scripts that are raw Python modules. Again, this would require the
|
||||
builder to have server access or to use version control to share their work with the rest of the development team.
|
||||
|
||||
In this tutorial, we will show examples of all these ways, but since we don't have a team of builders we'll
|
||||
build the brunt of things using Evennia's Batchcode system.
|
||||
|
||||
### Can only privileged Builders create things or should regular players also have limited build-capability?
|
||||
|
||||
In some game styles, players have the ability to create objects and even script them. While giving regular users
|
||||
the ability to create objects with in-built commands is easy and safe, actual code-creation (aka _softcode_ ) is
|
||||
not something Evennia supports natively. Regular, untrusted users should never be allowed to execute raw Python
|
||||
code (such as what you can do with the `py` command). You can
|
||||
[read more about Evennia's stance on softcode here](../../../Concepts/Soft-Code.md). If you want users to do limited scripting,
|
||||
it's suggested that this is accomplished by adding more powerful build-commands for them to use.
|
||||
|
||||
For our tutorial-game, we will only allow privileged builders to modify the world. The exception is crafting,
|
||||
which we will limit to repairing broken items by combining them with other repair-related items.
|
||||
|
||||
## Systems
|
||||
|
||||
### Do you base your game off an existing RPG system or make up your own?
|
||||
|
||||
We will make use of [Open Adventure](http://www.geekguild.com/openadventure/), a simple 'old school' RPG-system
|
||||
that is available for free under the Creative Commons license. We'll only use a subset of the rules from
|
||||
the blue "basic" book. For the sake of keeping down the length of this tutorial we will limit what features
|
||||
we will include:
|
||||
|
||||
- Only two 'archetypes' (classes) - Arcanist (wizard) and Warrior, these are examples of two different play
|
||||
styles.
|
||||
- Two races only (dwarves and elves), to show off how to implement races and race bonuses.
|
||||
- No extra features of the races/archetypes such as foci and special feats. While these are good for fleshing
|
||||
out a character, these will work the same as other bonuses and are thus not that instructive.
|
||||
- We will add only a small number of items/weapons from the Open Adventure rulebook to show how it's done.
|
||||
|
||||
### What are the game mechanics? How do you decide if an action succeeds or fails?
|
||||
|
||||
Open Adventure's conflict resolution is based on adding a trait (such as Strength) with a random number in
|
||||
order to beat a target. We will emulate this in code.
|
||||
|
||||
Having a "skill" means getting a bonus to that roll for a more narrow action.
|
||||
Since the computer will need to know exactly what those skills are, we will add them more explicitly than
|
||||
in the rules, but we will only add the minimum to show off the functionality we need.
|
||||
|
||||
### Does the flow of time matter in your game - does night and day change? What about seasons?
|
||||
|
||||
Most commonly, game-time runs faster than real-world time. There are
|
||||
a few advantages with this:
|
||||
|
||||
- Unlike in a single-player game, you can't fast-forward time in a multiplayer game if you are waiting for
|
||||
something, like NPC shops opening.
|
||||
- Healing and other things that we know takes time will go faster while still being reasonably 'realistic'.
|
||||
|
||||
The main drawback is for games with slower roleplay pace. While you are having a thoughtful roleplaying scene
|
||||
over dinner, the game world reports that two days have passed. Having a slower game time than real-time is
|
||||
a less common, but possible solution for such games.
|
||||
|
||||
It is however _not_ recommended to let game-time exactly equal the speed of real time. The reason for this
|
||||
is that people will join your game from all around the world, and they will often only be able to play at
|
||||
particular times of their day. With a game-time drifting relative real-time, everyone will eventually be
|
||||
able to experience both day and night in the game.
|
||||
|
||||
For this tutorial-game we will go with Evennia's default, which is that the game-time runs two times faster
|
||||
than real time.
|
||||
|
||||
### Do you want changing, global weather or should weather just be set manually in roleplay?
|
||||
|
||||
A weather system is a good example of a game-global system that affects a subset of game entities
|
||||
(outdoor rooms). We will not be doing any advanced weather simulation, but we'll show how to do
|
||||
random weather changes happening across the game world.
|
||||
|
||||
### Do you want a coded world-economy or just a simple barter system? Or no formal economy at all?
|
||||
|
||||
We will allow for money and barter/trade between NPCs/Players and Player/Player, but will not care about
|
||||
inflation. A real economic simulation could do things like modify shop prices based on supply and demand.
|
||||
We will not go down that rabbit hole.
|
||||
|
||||
### Do you have concepts like reputation and influence?
|
||||
|
||||
These are useful things for a more social-interaction heavy game. We will not include them for this
|
||||
tutorial however.
|
||||
|
||||
### Will your characters be known by their name or only by their physical appearance?
|
||||
|
||||
This is a common thing in RP-heavy games. Others will only see you as "The tall woman" until you
|
||||
introduce yourself and they 'recognize' you with a name. Linked to this is the concept of more complex
|
||||
emoting and posing.
|
||||
|
||||
Adding such a system from scratch is complex and way beyond the scope of this tutorial. However,
|
||||
there is an existing Evennia contrib that adds all of this functionality and more, so we will
|
||||
include that and explain briefly how it works.
|
||||
|
||||
## Rooms
|
||||
|
||||
### Is a simple room description enough or should the description be able to change?
|
||||
|
||||
Changing room descriptions for day and night, winder and summer is actually quite easy to do, but looks
|
||||
very impressive. We happen to know there is also a contrib that helps with this, so we'll show how to
|
||||
include that.
|
||||
|
||||
### Should the room have different statuses?
|
||||
|
||||
We will have different weather in outdoor rooms, but this will not have any gameplay effect - bow strings
|
||||
will not get wet and fireballs will not fizzle if it rains.
|
||||
|
||||
### Can objects be hidden in the room? Can a person hide in the room?
|
||||
|
||||
We will not model hiding and stealth. This will be a game of honorable face-to-face conflict.
|
||||
|
||||
## Objects
|
||||
|
||||
### How numerous are your objects? Do you want large loot-lists or are objects just role playing props?
|
||||
|
||||
Since we are not going for a pure freeform RPG here, we will want objects with properties, like weapons
|
||||
and potions and such. Monsters should drop loot even though our list of objects will not be huge.
|
||||
|
||||
### Is each coin a separate object or do you just store a bank account value?
|
||||
|
||||
Since we will use bartering, placing coin objects on one side of the barter makes for a simple way to
|
||||
handle payments. So we will use coins as-objects.
|
||||
|
||||
### Do multiple similar objects form stacks and how are those stacks handled in that case?
|
||||
|
||||
Since we'll use coins, it's practical to have them and other items stack together. While Evennia does not
|
||||
do this natively, we will make use of a contrib for this.
|
||||
|
||||
### Does an object have weight or volume (so you cannot carry an infinite amount of them)?
|
||||
|
||||
Limiting carrying weight is one way to stop players from hoarding. It also makes it more important
|
||||
for players to pick only the equipment they need. Carrying limits can easily come across as
|
||||
annoying to players though, so one needs to be careful with it.
|
||||
|
||||
Open Adventure rules include weight limits, so we will include them.
|
||||
|
||||
### Can objects be broken? Can they be repaired?
|
||||
|
||||
Item breakage is very useful for a game economy; breaking weapons adds tactical considerations (if it's not
|
||||
too common, then it becomes annoying) and repairing things gives work for crafting players.
|
||||
|
||||
We wanted a crafting system, so this is what we will limit it to - repairing items using some sort
|
||||
of raw materials.
|
||||
|
||||
### Can you fight with a chair or a flower or must you use a special 'weapon' kind of thing?
|
||||
|
||||
Traditionally, only 'weapons' could be used to fight with. In the past this was a useful
|
||||
simplification, but with Python classes and inheritance, it's not actually more work to just
|
||||
let all items in game work as a weapon in a pinch.
|
||||
|
||||
So for our game we will let a character use any item they want as a weapon. The difference will
|
||||
be that non-weapon items will do less damage and also break and become unusable much quicker.
|
||||
|
||||
### Will characters be able to craft new objects?
|
||||
|
||||
Crafting is a common feature in multiplayer games. In code it usually means using a skill-check
|
||||
to combine base ingredients from a fixed recipe in order to create a new item. The classic
|
||||
example is to combine _leather straps_, a _hilt_, a _pommel_ and a _blade_ to make a new _sword_.
|
||||
A full-fledged crafting system could require multiple levels of crafting, including having to mine
|
||||
for ore or cut down trees for wood.
|
||||
|
||||
In our case we will limit our crafting to repairing broken items. To show how it's done, we will require
|
||||
extra items (a recipe) in order to facilitate the repairs.
|
||||
|
||||
### Should mobs/NPCs have some sort of AI?
|
||||
|
||||
A rule of adding Artificial Intelligence is that with today's technology you should not hope to fool
|
||||
anyone with it anytime soon. Unless you have a side-gig as an AI researcher, users will likely
|
||||
not notice any practical difference between a simple state-machine and you spending a lot of time learning
|
||||
how to train a neural net.
|
||||
|
||||
For this tutorial, we will show how to add a simple state-machine for monsters. NPCs will only be
|
||||
shop-keepers and quest-gives so they won't need any real AI to speak of.
|
||||
|
||||
### Are NPCs and mobs different entities? How do they differ?
|
||||
|
||||
"Mobs" or "mobiles" are things that move around. This is traditionally monsters you can fight with, but could
|
||||
also be city guards or the baker going to chat with the neighbor. Back in the day, they were often fundamentally
|
||||
different these days it's often easier to just make NPCs and mobs essentially the same thing.
|
||||
|
||||
In EvAdventure, both Monsters and NPCs will be the same type of thing; A monster could give you a quest
|
||||
and an NPC might fight you as a mob as well as trade with you.
|
||||
|
||||
### _Should there be NPCs giving quests? If so, how do you track Quest status?
|
||||
|
||||
We will design a simple quest system to track the status of ongoing quests.
|
||||
|
||||
## Characters
|
||||
|
||||
### Can players have more than one Character active at a time or are they allowed to multi-play?
|
||||
|
||||
Since Evennia differentiates between `Sessions` (the client-connection to the game), `Accounts`
|
||||
and `Character`s, it natively supports multi-play. This is controlled by the `MULTISESSION_MODE`
|
||||
setting, which has a value from `0` (default) to `3`.
|
||||
|
||||
- `0`- One Character per Account and one Session per Account. This means that if you login to the same
|
||||
account from another client you'll be disconnected from the first. When creating a new account, a Character
|
||||
will be auto-created with the same name as your Account. This is default mode and mimics legacy code bases
|
||||
which had no separation between Account and Character.
|
||||
- `1` - One Character per Account, multiple Sessions per Account. So you can connect simultaneously from
|
||||
multiple clients and see the same output in all of them.
|
||||
- `2` - Multiple Characters per Account, one Session per Character. This will not auto-create a same-named
|
||||
Character for you, instead you get to create/choose between a number of Characters up to a max limit given by
|
||||
the `MAX_NR_CHARACTERS` setting (default 1). You can play them all simultaneously if you have multiple clients
|
||||
open, but only one client per Character.
|
||||
- `3` - Multiple Characters per Account, Multiple Sessions per Character. This is like mode 2, except players
|
||||
can control each Character from multiple clients, seeing the same output from each Character.
|
||||
|
||||
We will go with a multi-role game, so we will use `MULTISESSION_MODE=3` for this tutorial.
|
||||
|
||||
### How does the character-generation work?
|
||||
|
||||
There are a few common ways to do character generation:
|
||||
|
||||
- Rooms. This is the traditional way. Each room's description tells you what command to use to modify
|
||||
your character. When you are done you move to the next room. Only use this if you have another reason for
|
||||
using a room, like having a training dummy to test skills on, for example.
|
||||
- A Menu. The Evennia _EvMenu_ system allows you to code very flexible in-game menus without needing to walk
|
||||
between rooms. You can both have a step-by-step menu (a 'wizard') or allow the user to jump between the
|
||||
steps as they please. This tends to be a lot easier for newcomers to understand since it doesn't require
|
||||
using custom commands they will likely never use again after this.
|
||||
- Questions. A fun way to build a character is to answer a series of questions. This is usually implemented
|
||||
with a sequential menu.
|
||||
|
||||
For the tutorial we will use a menu to let the user modify each section of their character sheet in any order
|
||||
until they are happy.
|
||||
|
||||
### How do you implement different "classes" or "races"?
|
||||
|
||||
The way classes and races work in most RPGs (as well as in OpenAdventure) is that they act as static 'templates'
|
||||
that inform which bonuses and special abilities you have. This means that all we need to store on the
|
||||
Character is _which_ class and _which_ race they have; the actual logic can sit in Python code and just
|
||||
be looked up when we need it.
|
||||
|
||||
### If a Character can hide in a room, what skill will decide if they are detected?
|
||||
|
||||
Hiding means a few things.
|
||||
- The Character should not appear in the room's description / character list
|
||||
- Others hould not be able to interact with a hidden character. It'd be weird if you could do `attack <name>`
|
||||
or `look <name>` if the named character is in hiding.
|
||||
- There must be a way for the person to come out of hiding, and probably for others to search or accidentally
|
||||
find the person (probably based on skill checks).
|
||||
- The room will also need to be involved, maybe with some modifier as to how easy it is to hide in the room.
|
||||
|
||||
We will _not_ be including a hide-mechanic in EvAdventure though.
|
||||
|
||||
### What does the skill tree look like? Can a Character gain experience to improve? By killing enemies? Solving quests? By roleplaying?
|
||||
|
||||
Gaining experience points (XP) and improving one's character is a staple of roleplaying games. There are many
|
||||
ways to implement this:
|
||||
- Gaining XP from kills is very common; it's easy to let a monster be 'worth' a certain number of XP and it's
|
||||
easy to tell when you should gain it.
|
||||
- Gaining XP from quests is the same - each quest is 'worth' XP and you get them when completing the test.
|
||||
- Gaining XP from roleplay is harder to define. Different games have tried a lot of different ways to do this:
|
||||
- XP from being online - just being online gains you XP. This inflates player numbers but many players may
|
||||
just be lurking and not be actually playing the game at any given time.
|
||||
- XP from roleplaying scenes - you gain XP according to some algorithm analyzing your emotes for 'quality',
|
||||
how often you post, how long your emotes are etc.
|
||||
- XP from actions - you gain XP when doing things, anything. Maybe your XP is even specific to each action, so
|
||||
you gain XP only for running when you run, XP for your axe skill when you fight with an axe etc.
|
||||
- XP from fails - you only gain XP when failing rolls.
|
||||
- XP from other players - other players can award you XP for good RP.
|
||||
|
||||
For EvAdventure we will use Open Adventure's rules for XP, which will be driven by kills and quest successes.
|
||||
|
||||
### May player-characters attack each other (PvP)?
|
||||
|
||||
Deciding this affects the style of your entire game. PvP makes for exciting gameplay but it opens a whole new
|
||||
can of worms when it comes to "fairness". Players will usually accept dying to an overpowered NPC dragon. They
|
||||
will not be as accepting if they perceive another player is perceived as being overpowered. PvP means that you
|
||||
have to be very careful to balance the game - all characters does not have to be exactly equal but they should
|
||||
all be viable to play a fun game with. PvP does not only mean combat though. Players can compete in all sorts of ways, including gaining influence in
|
||||
a political game or gaining market share when selling their crafted merchandise.
|
||||
|
||||
For the EvAdventure we will support both Player-vs-environment combat and turn-based PvP. We will allow players
|
||||
to barter with each other (so potentially scam others?) but that's the extent of it. We will focus on showing
|
||||
off techniques and will not focus on making a balanced game.
|
||||
|
||||
### What are the penalties of defeat? Permanent death? Quick respawn? Time in prison?
|
||||
|
||||
This is another big decision that strongly affects the mood and style of your game.
|
||||
|
||||
Perma-death means that once your character dies, it's gone and you have to make a new one.
|
||||
|
||||
- It allows for true heroism. If you genuinely risk losing your character of two years to fight the dragon,
|
||||
your triumph is an actual feat.
|
||||
- It limits the old-timer dominance problem. If long-time players dies occationally, it will open things
|
||||
up for newcomers.
|
||||
- It lowers inflation, since the hoarded resources of a dead character can be removed.
|
||||
- It gives capital punishment genuine discouraging power.
|
||||
- It's realistic.
|
||||
|
||||
Perma-death comes with some severe disadvantages however.
|
||||
|
||||
- It's impopular. Many players will just not play a game where they risk losing their beloved character
|
||||
just like that.
|
||||
- Many players say they like the _idea_ of permadeath except when it could happen to them.
|
||||
- It can limit roleplaying freedom and make people refuse to take any risks.
|
||||
- It may make players even more reluctant to play conflict-driving 'bad guys'.
|
||||
- Game balance is much, much more important when results are "final". This escalates the severity of 'unfairness'
|
||||
a hundred-fold. Things like bugs or exploits can also lead to much more server effects.
|
||||
|
||||
For these reasons, it's very common to do hybrid systems. Some tried variations:
|
||||
|
||||
- NPCs cannot kill you, only other players can.
|
||||
- Death is permanent, but it's difficult to actually die - you are much more likely to end up being severely
|
||||
hurt/incapacitated.
|
||||
- You can pre-pay 'insurance' to magically/technologically avoid actually dying. Only if don't have insurance will
|
||||
you die permanently.
|
||||
- Death just means harsh penalties, not actual death.
|
||||
- When you die you can fight your way back to life from some sort of afterlife.
|
||||
- You'll only die permanently if you as a player explicitly allows it.
|
||||
|
||||
For our tutorial-game we will not be messing with perma-death; instead your defeat will mean you will re-spawn
|
||||
back at your home location with a fraction of your health.
|
||||
|
||||
## Conclusions
|
||||
|
||||
Going through the questions has helped us get a little bit more of a feel for the game we want to do. There are
|
||||
many other things we could ask ourselves, but if we can cover these points we will be a good way towards a complete,
|
||||
playable game!
|
||||
|
||||
Before starting to code in earnest a good coder should always do an inventory of all the stuff they _don't_ need
|
||||
to code themselves. So in the next lesson we will check out what help we have from Evennia's _contribs_.
|
||||
|
||||
|
|
@ -0,0 +1,414 @@
|
|||
# Player Characters
|
||||
|
||||
In the [previous lesson about rules and dice rolling](./Beginner-Tutorial-Rules.md) we made some
|
||||
assumptions about the "Player Character" entity:
|
||||
|
||||
- It should store Abilities on itself as `character.strength`, `character.constitution` etc.
|
||||
- It should have a `.heal(amount)` method.
|
||||
|
||||
So we have some guidelines of how it should look! A Character is a database entity with values that
|
||||
should be able to be changed over time. It makes sense to base it off Evennia's
|
||||
[DefaultCharacter Typeclass](../../../Components/Typeclasses.md). The Character class is like a 'character sheet' in a tabletop
|
||||
RPG, it will hold everything relevant to that PC.
|
||||
|
||||
## Inheritance structure
|
||||
|
||||
Player Characters (PCs) are not the only "living" things in our world. We also have _NPCs_
|
||||
(like shopkeepers and other friendlies) as well as _monsters_ (mobs) that can attack us.
|
||||
|
||||
In code, there are a few ways we could structure this. If NPCs/monsters were just special cases of PCs,
|
||||
we could use a class inheritance like this:
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class EvAdventureCharacter(DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureNPC(EvAdventureCharacter):
|
||||
# more stuff
|
||||
|
||||
class EvAdventureMob(EvAdventureNPC):
|
||||
# more stuff
|
||||
```
|
||||
|
||||
All code we put on the `Character` class would now be inherited to `NPC` and `Mob` automatically.
|
||||
|
||||
However, in _Knave_, NPCs and particularly monsters are _not_ using the same rules as PCs - they are
|
||||
simplified to use a Hit-Die (HD) concept. So while still character-like, NPCs should be separate from
|
||||
PCs like this:
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class EvAdventureCharacter(DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureNPC(DefaultCharacter):
|
||||
# separate stuff
|
||||
|
||||
class EvAdventureMob(EvadventureNPC):
|
||||
# more separate stuff
|
||||
```
|
||||
|
||||
Nevertheless, there are some things that _should_ be common for all 'living things':
|
||||
|
||||
- All can take damage.
|
||||
- All can die.
|
||||
- All can heal
|
||||
- All can hold and lose coins
|
||||
- All can loot their fallen foes.
|
||||
- All can get looted when defeated.
|
||||
|
||||
We don't want to code this separately for every class but we no longer have a common parent
|
||||
class to put it on. So instead we'll use the concept of a _mixin_ class:
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class LivingMixin:
|
||||
# stuff common for all living things
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureNPC(LivingMixin, DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureMob(LivingMixin, EvadventureNPC):
|
||||
# more stuff
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
In [evennia/contrib/tutorials/evadventure/characters.py](evennia.contrib.tutorials.evadventure.characters)
|
||||
is an example of a character class structure.
|
||||
```
|
||||
Above, the `LivingMixin` class cannot work on its own - it just 'patches' the other classes with some
|
||||
extra functionality all living things should be able to do. This is an example of
|
||||
_multiple inheritance_. It's useful to know about, but one should not over-do multiple inheritance
|
||||
since it can also get confusing to follow the code.
|
||||
|
||||
## Living mixin class
|
||||
|
||||
> Create a new module `mygame/evadventure/characters.py`
|
||||
|
||||
Let's get some useful common methods all living things should have in our game.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/characters.py
|
||||
|
||||
from .rules import dice
|
||||
|
||||
class LivingMixin:
|
||||
|
||||
# makes it easy for mobs to know to attack PCs
|
||||
is_pc = False
|
||||
|
||||
def heal(self, hp):
|
||||
"""
|
||||
Heal hp amount of health, not allowing to exceed our max hp
|
||||
|
||||
"""
|
||||
damage = self.hp_max - self.hp
|
||||
healed = min(damage, hp)
|
||||
self.hp += healed
|
||||
|
||||
self.msg("You heal for {healed} HP.")
|
||||
|
||||
def at_pay(self, amount):
|
||||
"""When paying coins, make sure to never detract more than we have"""
|
||||
amount = min(amount, self.coins)
|
||||
self.coins -= amount
|
||||
return amount
|
||||
|
||||
def at_damage(self, damage, attacker=None):
|
||||
"""Called when attacked and taking damage."""
|
||||
self.hp -= damage
|
||||
|
||||
def at_defeat(self):
|
||||
"""Called when defeated. By default this means death."""
|
||||
self.at_death()
|
||||
|
||||
def at_death(self):
|
||||
"""Called when this thing dies."""
|
||||
# this will mean different things for different living things
|
||||
pass
|
||||
|
||||
def at_do_loot(self, looted):
|
||||
"""Called when looting another entity"""
|
||||
looted.at_looted(self)
|
||||
|
||||
def at_looted(self, looter):
|
||||
"""Called when looted by another entity"""
|
||||
|
||||
# default to stealing some coins
|
||||
max_steal = dice.roll("1d10")
|
||||
stolen = self.at_pay(max_steal)
|
||||
looter.coins += stolen
|
||||
|
||||
```
|
||||
Most of these are empty since they will behave differently for characters and npcs. But having them
|
||||
in the mixin means we can expect these methods to be available for all living things.
|
||||
|
||||
|
||||
## Character class
|
||||
|
||||
We will now start making the basic Character class, based on what we need from _Knave_.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/characters.py
|
||||
|
||||
from evennia import DefaultCharacter, AttributeProperty
|
||||
from .rules import dice
|
||||
|
||||
class LivingMixin:
|
||||
# ...
|
||||
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
"""
|
||||
A character to use for EvAdventure.
|
||||
"""
|
||||
is_pc = True
|
||||
|
||||
strength = AttributeProperty(1)
|
||||
dexterity = AttributeProperty(1)
|
||||
constitution = AttributeProperty(1)
|
||||
intelligence = AttributeProperty(1)
|
||||
wisdom = AttributeProperty(1)
|
||||
charisma = AttributeProperty(1)
|
||||
|
||||
hp = AttributeProperty(8)
|
||||
hp_max = AttributeProperty(8)
|
||||
|
||||
level = AttributeProperty(1)
|
||||
xp = AttributeProperty(0)
|
||||
coins = AttributeProperty(0)
|
||||
|
||||
def at_defeat(self):
|
||||
"""Characters roll on the death table"""
|
||||
if self.location.allow_death:
|
||||
# this allow rooms to have non-lethal battles
|
||||
dice.roll_death(self)
|
||||
else:
|
||||
self.location.msg_contents(
|
||||
"$You() $conj(collapse) in a heap, alive but beaten.",
|
||||
from_obj=self)
|
||||
self.heal(self.hp_max)
|
||||
|
||||
def at_death(self):
|
||||
"""We rolled 'dead' on the death table."""
|
||||
self.location.msg_contents(
|
||||
"$You() collapse in a heap, embraced by death.",
|
||||
from_obj=self)
|
||||
# TODO - go back into chargen to make a new character!
|
||||
```
|
||||
|
||||
We make an assumption about our rooms here - that they have a property `.allow_death`. We need
|
||||
to make a note to actually add such a property to rooms later!
|
||||
|
||||
In our `Character` class we implement all attributes we want to simulate from the _Knave_ ruleset.
|
||||
The `AttributeProperty` is one way to add an Attribute in a field-like way; these will be accessible
|
||||
on every character in several ways:
|
||||
|
||||
- As `character.strength`
|
||||
- As `character.db.strength`
|
||||
- As `character.attributes.get("strength")`
|
||||
|
||||
See [Attributes](../../../Components/Attributes.md) for seeing how Attributes work.
|
||||
|
||||
Unlike in base _Knave_, we store `coins` as a separate Attribute rather than as items in the inventory,
|
||||
this makes it easier to handle barter and trading later.
|
||||
|
||||
We implement the Player Character versions of `at_defeat` and `at_death`. We also make use of `.heal()`
|
||||
from the `LivingMixin` class.
|
||||
|
||||
### Funcparser inlines
|
||||
|
||||
This piece of code is worth some more explanation:
|
||||
|
||||
```python
|
||||
self.location.msg_contents(
|
||||
"$You() $conj(collapse) in a heap, alive but beaten.",
|
||||
from_obj=self)
|
||||
```
|
||||
|
||||
Remember that `self` is the Character instance here. So `self.location.msg_contents` means "send a
|
||||
message to everything inside my current location". In other words, send a message to everyone
|
||||
in the same place as the character.
|
||||
|
||||
The `$You() $conj(collapse)` are [FuncParser inlines](../../../Components/FuncParser.md). These are functions that
|
||||
execute
|
||||
in the string. The resulting string may look different for different audiences. The `$You()` inline
|
||||
function will use `from_obj` to figure out who 'you' are and either show your name or 'You'.
|
||||
The `$conj()` (verb conjugator) will tweak the (English) verb to match.
|
||||
|
||||
- You will see: `"You collapse in a heap, alive but beaten."`
|
||||
- Others in the room will see: `"Thomas collapses in a heap, alive but beaten."`
|
||||
|
||||
Note how `$conj()` chose `collapse/collapses` to make the sentences grammatically correct.
|
||||
|
||||
### Backtracking
|
||||
|
||||
We make our first use of the `rules.dice` roller to roll on the death table! As you may recall, in the
|
||||
previous lesson, we didn't know just what to do when rolling 'dead' on this table. Now we know - we
|
||||
should be calling `at_death` on the character. So let's add that where we had TODOs before:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/rules.py
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def roll_death(self, character):
|
||||
ability_name = self.roll_random_table("1d8", death_table)
|
||||
|
||||
if ability_name == "dead":
|
||||
# kill the character!
|
||||
character.at_death() # <------ TODO no more
|
||||
else:
|
||||
# ...
|
||||
|
||||
if current_ability < -10:
|
||||
# kill the character!
|
||||
character.at_death() # <------- TODO no more
|
||||
else:
|
||||
# ...
|
||||
```
|
||||
|
||||
## Connecting the Character with Evennia
|
||||
|
||||
You can easily make yourself an `EvAdventureCharacter` in-game by using the
|
||||
`type` command:
|
||||
|
||||
type self = evadventure.characters.EvAdventureCharacter
|
||||
|
||||
You can now do `examine self` to check your type updated.
|
||||
|
||||
If you want _all_ new Characters to be of this type you need to tell Evennia about it. Evennia
|
||||
uses a global setting `BASE_CHARACTER_TYPECLASS` to know which typeclass to use when creating
|
||||
Characters (when logging in, for example). This defaults to `typeclasses.characters.Character` (that is,
|
||||
the `Character` class in `mygame/typeclasses/characters.py`).
|
||||
|
||||
There are thus two ways to weave your new Character class into Evennia:
|
||||
|
||||
1. Change `mygame/server/conf/settings.py` and add `BASE_CHARACTER_CLASS = "evadventure.characters.EvAdventureCharacter"`.
|
||||
2. Or, change `typeclasses.characters.Character` to inherit from `EvAdventureCharacter`.
|
||||
|
||||
You must always reload the server for changes like this to take effect.
|
||||
|
||||
```{important}
|
||||
In this tutorial we are making all changes in a folder `mygame/evadventure/`. This means we can isolate
|
||||
our code but means we need to do some extra steps to tie the character (and other objects) into Evennia.
|
||||
For your own game it would be just fine to start editing `mygame/typeclasses/characters.py` directly
|
||||
instead.
|
||||
```
|
||||
|
||||
|
||||
## Unit Testing
|
||||
|
||||
> Create a new module `mygame/evadventure/tests/test_characters.py`
|
||||
|
||||
For testing, we just need to create a new EvAdventure character and check
|
||||
that calling the methods on it doesn't error out.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/tests/test_characters.py
|
||||
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
||||
from ..characters import EvAdventureCharacter
|
||||
|
||||
class TestCharacters(BaseEvenniaTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.character = create.create_object(EvAdventureCharacter, key="testchar")
|
||||
|
||||
def test_heal(self):
|
||||
self.character.hp = 0
|
||||
self.character.hp_max = 8
|
||||
|
||||
self.character.heal(1)
|
||||
self.assertEqual(self.character.hp, 1)
|
||||
# make sure we can't heal more than max
|
||||
self.character.heal(100)
|
||||
self.assertEqual(self.character.hp, 8)
|
||||
|
||||
def test_at_pay(self):
|
||||
self.character.coins = 100
|
||||
|
||||
result = self.character.at_pay(60)
|
||||
self.assertEqual(result, 60)
|
||||
self.assertEqual(self.character.coins, 40)
|
||||
|
||||
# can't get more coins than we have
|
||||
result = self.character.at_pay(100)
|
||||
self.assertEqual(result, 40)
|
||||
self.assertEqual(self.character.coins, 0)
|
||||
|
||||
# tests for other methods ...
|
||||
|
||||
```
|
||||
If you followed the previous lessons, these tests should look familiar. Consider adding
|
||||
tests for other methods as practice. Refer to previous lessons for details.
|
||||
|
||||
For running the tests you do:
|
||||
|
||||
evennia test --settings settings.py .evadventure.tests.test_character
|
||||
|
||||
|
||||
## About races and classes
|
||||
|
||||
_Knave_ doesn't have any D&D-style _classes_ (like Thief, Fighter etc). It also does not bother with
|
||||
_races_ (like dwarves, elves etc). This makes the tutorial shorter, but you may ask yourself how you'd
|
||||
add these functions.
|
||||
|
||||
In the framework we have sketched out for _Knave_, it would be simple - you'd add your race/class as
|
||||
an Attribute on your Character:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/characters.py
|
||||
|
||||
from evennia import DefaultCharacter, AttributeProperty
|
||||
# ...
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
|
||||
# ...
|
||||
|
||||
charclass = AttributeProperty("Fighter")
|
||||
charrace = AttributeProperty("Human")
|
||||
|
||||
```
|
||||
We use `charclass` rather than `class` here, because `class` is a reserved Python keyword. Naming
|
||||
`race` as `charrace` thus matches in style.
|
||||
|
||||
We'd then need to expand our [rules module](./Beginner-Tutorial-Rules.md) (and later
|
||||
[character generation](./Beginner-Tutorial-Chargen.md) to check and include what these classes mean.
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
|
||||
With the `EvAdventureCharacter` class in place, we have a better understanding of how our PCs will look
|
||||
like under _Knave_.
|
||||
|
||||
For now, we only have bits and pieces and haven't been testing this code in-game. But if you want
|
||||
you can swap yourself into `EvAdventureCharacter` right now. Log into your game and run
|
||||
the command
|
||||
|
||||
type self = evadventure.characters.EvAdventureCharacter
|
||||
|
||||
If all went well, `ex self` will now show your typeclass as being `EvAdventureCharacter`.
|
||||
Check out your strength with
|
||||
|
||||
py self.strength = 3
|
||||
|
||||
```{important}
|
||||
When doing `ex self` you will _not_ see all your Abilities listed yet. That's because
|
||||
Attributes added with `AttributeProperty` are not available until they have been accessed at
|
||||
least once. So once you set (or look at) `.strength` above, `strength` will show in `examine` from
|
||||
then on.
|
||||
```
|
||||
|
|
@ -0,0 +1,711 @@
|
|||
# Character Generation
|
||||
|
||||
In previous lessons we have established how a character looks. Now we need to give the player a
|
||||
chance to create one.
|
||||
|
||||
## How it will work
|
||||
|
||||
A fresh Evennia install will automatically create a new Character with the same name as your
|
||||
Account when you log in. This is quick and simple and mimics older MUD styles. You could picture
|
||||
doing this, and then customizing the Character in-place.
|
||||
|
||||
We will be a little more sophisticated though. We want the user to be able to create a character
|
||||
using a menu when they log in.
|
||||
|
||||
We do this by editing `mygame/server/conf/settings.py` and adding the line
|
||||
|
||||
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = False
|
||||
|
||||
When doing this, connecting with the game with a new account will land you in "OOC" mode. The
|
||||
ooc-version of `look` (sitting in the Account cmdset) will show a list of available characters
|
||||
if you have any. You can also enter `charcreate` to make a new character. The `charcreate` is a
|
||||
simple command coming with Evennia that just lets you make a new character with a given name and
|
||||
description. We will later modify that to kick off our chargen. For now we'll just keep in mind
|
||||
that's how we'll start off the menu.
|
||||
|
||||
In _Knave_, most of the character-generation is random. This means this tutorial can be pretty
|
||||
compact while still showing the basic idea. What we will create is a menu looking like this:
|
||||
|
||||
|
||||
```
|
||||
Silas
|
||||
|
||||
STR +1
|
||||
DEX +2
|
||||
CON +1
|
||||
INT +3
|
||||
WIS +1
|
||||
CHA +2
|
||||
|
||||
You are lanky with a sunken face and filthy hair, breathy speech, and foreign clothing.
|
||||
You were a herbalist, but you were pursued and ended up a knave. You are honest but also
|
||||
suspicious. You are of the neutral alignment.
|
||||
|
||||
Your belongings:
|
||||
Brigandine armor, ration, ration, sword, torch, torch, torch, torch, torch,
|
||||
tinderbox, chisel, whistle
|
||||
|
||||
----------------------------------------------------------------------------------------
|
||||
1. Change your name
|
||||
2. Swap two of your ability scores (once)
|
||||
3. Accept and create character
|
||||
```
|
||||
|
||||
If you select 1, you get a new menu node:
|
||||
|
||||
```
|
||||
Your current name is Silas. Enter a new name or leave empty to abort.
|
||||
-----------------------------------------------------------------------------------------
|
||||
```
|
||||
You can now enter a new name. When pressing return you'll get back to the first menu node
|
||||
showing your character, now with the new name.
|
||||
|
||||
If you select 2, you go to another menu node:
|
||||
|
||||
```
|
||||
Your current abilities:
|
||||
|
||||
STR +1
|
||||
DEX +2
|
||||
CON +1
|
||||
INT +3
|
||||
WIS +1
|
||||
CHA +2
|
||||
|
||||
You can swap the values of two abilities around.
|
||||
You can only do this once, so choose carefully!
|
||||
|
||||
To swap the values of e.g. STR and INT, write 'STR INT'. Empty to abort.
|
||||
------------------------------------------------------------------------------------------
|
||||
```
|
||||
If you enter `WIS CHA` here, WIS will become `+2` and `CHA` `+1`. You will then again go back
|
||||
to the main node to see your new character, but this time the option to swap will no longer be
|
||||
available (you can only do it once).
|
||||
|
||||
If you finally select the `Accept and create character` option, the character will be created
|
||||
and you'll leave the menu;
|
||||
|
||||
Character was created!
|
||||
|
||||
## Random tables
|
||||
|
||||
```{sidebar}
|
||||
Full Knave random tables are found in
|
||||
[evennia/contrib/tutorials/evadventure/random_tables.py](evennia.contrib.tutorials.evadventure.random_tables).
|
||||
```
|
||||
|
||||
> Make a new module `mygame/evadventure/random_tables.py`.
|
||||
|
||||
Since most of _Knave_'s character generation is random we will need to roll on random tables
|
||||
from the _Knave_ rulebook. While we added the ability to roll on a random table back in the
|
||||
[Rules Tutorial](./Beginner-Tutorial-Rules.md), we haven't added the relevant tables yet.
|
||||
|
||||
```
|
||||
# in mygame/evadventure/random_tables.py
|
||||
|
||||
chargen_tables = {
|
||||
"physique": [
|
||||
"athletic", "brawny", "corpulent", "delicate", "gaunt", "hulking", "lanky",
|
||||
"ripped", "rugged", "scrawny", "short", "sinewy", "slender", "flabby",
|
||||
"statuesque", "stout", "tiny", "towering", "willowy", "wiry",
|
||||
],
|
||||
"face": [
|
||||
"bloated", "blunt", "bony", # ...
|
||||
], # ...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The tables are just copied from the _Knave_ rules. We group the aspects in a dict
|
||||
`character_generation` to separate chargen-only tables from other random tables we'll also
|
||||
keep in here.
|
||||
|
||||
## Storing state of the menu
|
||||
|
||||
```{sidebar}
|
||||
There is a full implementation of the chargen in
|
||||
[evennia/contrib/tutorials/evadventure/chargen.py](evennia.contrib.tutorials.evadventure.chargen).
|
||||
```
|
||||
> create a new module `mygame/evadventure/chargen.py`.
|
||||
|
||||
During character generation we will need an entity to store/retain the changes, like a
|
||||
'temporary character sheet'.
|
||||
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
from .random_tables import chargen_tables
|
||||
from .rules import dice
|
||||
|
||||
class TemporaryCharacterSheet:
|
||||
|
||||
def _random_ability(self):
|
||||
return min(dice.roll("1d6"), dice.roll("1d6"), dice.roll("1d6"))
|
||||
|
||||
def __init__(self):
|
||||
self.ability_changes = 0 # how many times we tried swap abilities
|
||||
|
||||
# name will likely be modified later
|
||||
self.name = dice.roll_random_table("1d282", chargen_tables["name"])
|
||||
|
||||
# base attribute values
|
||||
self.strength = self._random_ability()
|
||||
self.dexterity = self._random_ability()
|
||||
self.constitution = self._random_ability()
|
||||
self.intelligence = self._random_ability()
|
||||
self.wisdom = self._random_ability()
|
||||
self.charisma = self._random_ability()
|
||||
|
||||
# physical attributes (only for rp purposes)
|
||||
physique = dice.roll_random_table("1d20", chargen_tables["physique"])
|
||||
face = dice.roll_random_table("1d20", chargen_tables["face"])
|
||||
skin = dice.roll_random_table("1d20", chargen_tables["skin"])
|
||||
hair = dice.roll_random_table("1d20", chargen_tables["hair"])
|
||||
clothing = dice.roll_random_table("1d20", chargen_tables["clothing"])
|
||||
speech = dice.roll_random_table("1d20", chargen_tables["speech"])
|
||||
virtue = dice.roll_random_table("1d20", chargen_tables["virtue"])
|
||||
vice = dice.roll_random_table("1d20", chargen_tables["vice"])
|
||||
background = dice.roll_random_table("1d20", chargen_tables["background"])
|
||||
misfortune = dice.roll_random_table("1d20", chargen_tables["misfortune"])
|
||||
alignment = dice.roll_random_table("1d20", chargen_tables["alignment"])
|
||||
|
||||
self.desc = (
|
||||
f"You are {physique} with a {face} face, {skin} skin, {hair} hair, {speech} speech,"
|
||||
f" and {clothing} clothing. You were a {background.title()}, but you were"
|
||||
f" {misfortune} and ended up a knave. You are {virtue} but also {vice}. You are of the"
|
||||
f" {alignment} alignment."
|
||||
)
|
||||
|
||||
#
|
||||
self.hp_max = max(5, dice.roll("1d8"))
|
||||
self.hp = self.hp_max
|
||||
self.xp = 0
|
||||
self.level = 1
|
||||
|
||||
# random equipment
|
||||
self.armor = dice.roll_random_table("1d20", chargen_tables["armor"])
|
||||
|
||||
_helmet_and_shield = dice.roll_random_table("1d20", chargen_tables["helmets and shields"])
|
||||
self.helmet = "helmet" if "helmet" in _helmet_and_shield else "none"
|
||||
self.shield = "shield" if "shield" in _helmet_and_shield else "none"
|
||||
|
||||
self.weapon = dice.roll_random_table("1d20", chargen_tables["starting weapon"])
|
||||
|
||||
self.backpack = [
|
||||
"ration",
|
||||
"ration",
|
||||
dice.roll_random_table("1d20", chargen_tables["dungeoning gear"]),
|
||||
dice.roll_random_table("1d20", chargen_tables["dungeoning gear"]),
|
||||
dice.roll_random_table("1d20", chargen_tables["general gear 1"]),
|
||||
dice.roll_random_table("1d20", chargen_tables["general gear 2"]),
|
||||
]
|
||||
```
|
||||
|
||||
Here we have followed the _Knave_ rulebook to randomize abilities, description and equipment.
|
||||
The `dice.roll()` and `dice.roll_random_table` methods now become very useful! Everything here
|
||||
should be easy to follow.
|
||||
|
||||
The main difference from baseline _Knave_ is that we make a table of "starting weapon" (in Knave
|
||||
you can pick whatever you like).
|
||||
|
||||
We also initialize `.ability_changes = 0`. Knave only allows us to swap the values of two
|
||||
Abilities _once_. We will use this to know if it has been done or not.
|
||||
|
||||
### Showing the sheet
|
||||
|
||||
Now that we have our temporary character sheet, we should make it easy to visualize it.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
_TEMP_SHEET = """
|
||||
{name}
|
||||
|
||||
STR +{strength}
|
||||
DEX +{dexterity}
|
||||
CON +{constitution}
|
||||
INT +{intelligence}
|
||||
WIS +{wisdom}
|
||||
CHA +{charisma}
|
||||
|
||||
{description}
|
||||
|
||||
Your belongings:
|
||||
{equipment}
|
||||
"""
|
||||
|
||||
class TemporaryCharacterSheet:
|
||||
|
||||
# ...
|
||||
|
||||
def show_sheet(self):
|
||||
equipment = (
|
||||
str(item)
|
||||
for item in [self.armor, self.helmet, self.shield, self.weapon] + self.backpack
|
||||
if item
|
||||
)
|
||||
|
||||
return _TEMP_SHEET.format(
|
||||
name=self.name,
|
||||
strength=self.strength,
|
||||
dexterity=self.dexterity,
|
||||
constitution=self.constitution,
|
||||
intelligence=self.intelligence,
|
||||
wisdom=self.wisdom,
|
||||
charisma=self.charisma,
|
||||
description=self.desc,
|
||||
equipment=", ".join(equipment),
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
The new `show_sheet` method collect the data from the temporary sheet and return it in a pretty
|
||||
form. Making a 'template' string like `_TEMP_SHEET` makes it easier to change things later if you want
|
||||
to change how things look.
|
||||
|
||||
### Apply character
|
||||
|
||||
Once we are happy with our character, we need to actually create it with the stats we chose.
|
||||
This is a bit more involved.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
# ...
|
||||
|
||||
from .characters import EvAdventureCharacter
|
||||
from evennia import create_object
|
||||
from evennia.prototypes.spawner import spawn
|
||||
|
||||
|
||||
class TemporaryCharacterSheet:
|
||||
|
||||
# ...
|
||||
|
||||
def apply(self):
|
||||
# create character object with given abilities
|
||||
new_character = create_object(
|
||||
EvAdventureCharacter,
|
||||
key=self.name,
|
||||
attrs=(
|
||||
("strength", self.strength),
|
||||
("dexterity", self.dexterity),
|
||||
("constitution", self.constitution),
|
||||
("intelligence", self.intelligence),
|
||||
("wisdom", self.wisdom),
|
||||
("charisma", self.wisdom),
|
||||
("hp", self.hp),
|
||||
("hp_max", self.hp_max),
|
||||
("desc", self.desc),
|
||||
),
|
||||
)
|
||||
# spawn equipment (will require prototypes created before it works)
|
||||
if self.weapon:
|
||||
weapon = spawn(self.weapon)
|
||||
new_character.equipment.move(weapon)
|
||||
if self.shield:
|
||||
shield = spawn(self.shield)
|
||||
new_character.equipment.move(shield)
|
||||
if self.armor:
|
||||
armor = spawn(self.armor)
|
||||
new_character.equipment.move(armor)
|
||||
if self.helmet:
|
||||
helmet = spawn(self.helmet)
|
||||
new_character.equipment.move(helmet)
|
||||
|
||||
for item in self.backpack:
|
||||
item = spawn(item)
|
||||
new_character.equipment.store(item)
|
||||
|
||||
return new_character
|
||||
```
|
||||
|
||||
We use `create_object` to create a new `EvAdventureCharacter`. We feed it with all relevant data
|
||||
from the temporary character sheet. This is when these become an actual character.
|
||||
|
||||
```{sidebar}
|
||||
A prototype is basically a `dict` describing how the object should be created. Since
|
||||
it's just a piece of code, it can stored in a Python module and used to quickly _spawn_ (create)
|
||||
things from those prototypes.
|
||||
```
|
||||
|
||||
Each piece of equipment is an object in in its own right. We will here assume that all game
|
||||
items are defined as [Prototypes](../../../Components/Prototypes.md) keyed to its name, such as "sword", "brigandine
|
||||
armor" etc.
|
||||
|
||||
We haven't actually created those prototypes yet, so for now we'll need to assume they are there.
|
||||
Once a piece of equipment has been spawned, we make sure to move it into the `EquipmentHandler` we
|
||||
created in the [Equipment lesson](./Beginner-Tutorial-Equipment.md).
|
||||
|
||||
|
||||
## Initializing EvMenu
|
||||
|
||||
Evennia comes with a full menu-generation system based on [Command sets](../../../Components/Command-Sets.md), called
|
||||
[EvMenu](../../../Components/EvMenu.md).
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
from evennia import EvMenu
|
||||
|
||||
# ...
|
||||
|
||||
# chargen menu
|
||||
|
||||
|
||||
# this goes to the bottom of the module
|
||||
|
||||
def start_chargen(caller, session=None):
|
||||
"""
|
||||
This is a start point for spinning up the chargen from a command later.
|
||||
|
||||
"""
|
||||
|
||||
menutree = {} # TODO!
|
||||
|
||||
# this generates all random components of the character
|
||||
tmp_character = TemporaryCharacterSheet()
|
||||
|
||||
EvMenu(caller, menutree, session=session, tmp_character=tmp_character)
|
||||
|
||||
```
|
||||
|
||||
This first function is what we will call from elsewhere (for example from a custom `charcreate`
|
||||
command) to kick the menu into gear.
|
||||
|
||||
It takes the `caller` (the one to want to start the menu) and a `session` argument. The latter will help
|
||||
track just which client-connection we are using (depending on Evennia settings, you could be
|
||||
connecting with multiple clients).
|
||||
|
||||
We create a `TemporaryCharacterSheet` and call `.generate()` to make a random character. We then
|
||||
feed all this into `EvMenu`.
|
||||
|
||||
The moment this happens, the user will be in the menu, there are no further steps needed.
|
||||
|
||||
The `menutree` is what we'll create next. It describes which menu 'nodes' are available to jump
|
||||
between.
|
||||
|
||||
## Main Node: Choosing what to do
|
||||
|
||||
This is the first menu node. It will act as a central hub, from which one can choose different
|
||||
actions.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
# ...
|
||||
|
||||
# at the end of the module, but before the `start_chargen` function
|
||||
|
||||
def node_chargen(caller, raw_string, **kwargs):
|
||||
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
|
||||
text = tmp_character.show_sheet()
|
||||
|
||||
options = [
|
||||
{
|
||||
"desc": "Change your name",
|
||||
"goto": ("node_change_name", kwargs)
|
||||
}
|
||||
]
|
||||
if tmp_character.ability_changes <= 0:
|
||||
options.append(
|
||||
{
|
||||
"desc": "Swap two of your ability scores (once)",
|
||||
"goto": ("node_swap_abilities", kwargs),
|
||||
}
|
||||
)
|
||||
options.append(
|
||||
{
|
||||
"desc": "Accept and create character",
|
||||
"goto": ("node_apply_character", kwargs)
|
||||
},
|
||||
)
|
||||
|
||||
return text, options
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
A lot to unpack here! In Evennia, it's convention to name your node-functions `node_*`. While
|
||||
not required, it helps you track what is a node and not.
|
||||
|
||||
Every menu-node, should accept `caller, raw_string, **kwargs` as arguments. Here `caller` is the
|
||||
`caller` you passed into the `EvMenu` call. `raw_string` is the input given by the user in order
|
||||
to _get to this node_, so currently empty. The `**kwargs` are all extra keyword arguments passed
|
||||
into `EvMenu`. They can also be passed between nodes. In this case, we passed the
|
||||
keyword `tmp_character` to `EvMenu`. We now have the temporary character sheet available in the
|
||||
node!
|
||||
|
||||
An `EvMenu` node must always return two things - `text` and `options`. The `text` is what will
|
||||
show to the user when looking at this node. The `options` are, well, what options should be
|
||||
presented to move on from here to some other place.
|
||||
|
||||
For the text, we simply get a pretty-print of the temporary character sheet. A single option is
|
||||
defined as a `dict` like this:
|
||||
|
||||
```python
|
||||
{
|
||||
"key": ("name". "alias1", "alias2", ...), # if skipped, auto-show a number
|
||||
"desc": "text to describe what happens when selecting option",.
|
||||
"goto": ("name of node or a callable", kwargs_to_pass_into_next_node_or_callable)
|
||||
}
|
||||
```
|
||||
|
||||
Multiple option-dicts are returned in a list or tuple. The `goto` option-key is important to
|
||||
understand. The job of this is to either point directly to another node (by giving its name), or
|
||||
by pointing to a Python callable (like a function) _that then returns that name_. You can also
|
||||
pass kwargs (as a dict). This will be made available as `**kwargs` in the callable or next node.
|
||||
|
||||
While an option can have a `key`, you can also skip it to just get a running number.
|
||||
|
||||
In our `node_chargen` node, we point to three nodes by name: `node_change_name`,
|
||||
`node_swap_abilities`, and `node_apply_character`. We also make sure to pass along `kwargs`
|
||||
to each node, since that contains our temporary character sheet.
|
||||
|
||||
The middle of these options only appear if we haven't already switched two abilities around - to
|
||||
know this, we check the `.ability_changes` property to make sure it's still 0.
|
||||
|
||||
|
||||
## Node: Changing your name
|
||||
|
||||
This is where you end up if you opted to change your name in `node_chargen`.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
# ...
|
||||
|
||||
# after previous node
|
||||
|
||||
def _update_name(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Used by node_change_name below to check what user
|
||||
entered and update the name if appropriate.
|
||||
|
||||
"""
|
||||
if raw_string:
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
tmp_character.name = raw_string.lower().capitalize()
|
||||
|
||||
return "node_chargen", kwargs
|
||||
|
||||
|
||||
def node_change_name(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Change the random name of the character.
|
||||
|
||||
"""
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
|
||||
text = (
|
||||
f"Your current name is |w{tmp_character.name}|n. "
|
||||
"Enter a new name or leave empty to abort."
|
||||
)
|
||||
|
||||
options = {
|
||||
"key": "_default",
|
||||
"goto": (_update_name, kwargs)
|
||||
}
|
||||
|
||||
return text, options
|
||||
```
|
||||
|
||||
There are two functions here - the menu node itself (`node_change_name`) and a
|
||||
helper _goto_function_ (`_update_name`) to handle the user's input.
|
||||
|
||||
For the (single) option, we use a special `key` named `_default`. This makes this option
|
||||
a catch-all: If the user enters something that does not match any other option, this is
|
||||
the option that will be used.
|
||||
Since we have no other options here, we will always use this option no matter what the user enters.
|
||||
|
||||
Also note that the `goto` part of the option points to the `_update_name` callable rather than to
|
||||
the name of a node. It's important we keep passing `kwargs` along to it!
|
||||
|
||||
When a user writes anything at this node, the `_update_name` callable will be called. This has
|
||||
the same arguments as a node, but it is _not_ a node - we will only use it to _figure out_ which
|
||||
node to go to next.
|
||||
|
||||
In `_update_name` we now have a use for the `raw_string` argument - this is what was written by
|
||||
the user on the previous node, remember? This is now either an empty string (meaning to ignore
|
||||
it) or the new name of the character.
|
||||
|
||||
A goto-function like `_update_name` must return the name of the next node to use. It can also
|
||||
optionally return the `kwargs` to pass into that node - we want to always do this, so we don't
|
||||
loose our temporary character sheet. Here we will always go back to the `node_chargen`.
|
||||
|
||||
> Hint: If returning `None` from a goto-callable, you will always return to the last node you
|
||||
> were at.
|
||||
|
||||
## Node: Swapping Abilities around
|
||||
|
||||
You get here by selecting the second option from the `node_chargen` node.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
# ...
|
||||
|
||||
# after previous node
|
||||
|
||||
_ABILITIES = {
|
||||
"STR": "strength",
|
||||
"DEX": "dexterity",
|
||||
"CON": "constitution",
|
||||
"INT": "intelligence",
|
||||
"WIS": "wisdom",
|
||||
"CHA": "charisma",
|
||||
}
|
||||
|
||||
|
||||
def _swap_abilities(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Used by node_swap_abilities to parse the user's input and swap ability
|
||||
values.
|
||||
|
||||
"""
|
||||
if raw_string:
|
||||
abi1, *abi2 = raw_string.split(" ", 1)
|
||||
if not abi2:
|
||||
caller.msg("That doesn't look right.")
|
||||
return None, kwargs
|
||||
abi2 = abi2[0]
|
||||
abi1, abi2 = abi1.upper().strip(), abi2.upper().strip()
|
||||
if abi1 not in _ABILITIES or abi2 not in _ABILITIES:
|
||||
caller.msg("Not a familiar set of abilites.")
|
||||
return None, kwargs
|
||||
|
||||
# looks okay = swap values. We need to convert STR to strength etc
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
abi1 = _ABILITIES[abi1]
|
||||
abi2 = _ABILITIES[abi2]
|
||||
abival1 = getattr(tmp_character, abi1)
|
||||
abival2 = getattr(tmp_character, abi2)
|
||||
|
||||
setattr(tmp_character, abi1, abival2)
|
||||
setattr(tmp_character, abi2, abival1)
|
||||
|
||||
tmp_character.ability_changes += 1
|
||||
|
||||
return "node_chargen", kwargs
|
||||
|
||||
|
||||
def node_swap_abilities(caller, raw_string, **kwargs):
|
||||
"""
|
||||
One is allowed to swap the values of two abilities around, once.
|
||||
|
||||
"""
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
|
||||
text = f"""
|
||||
Your current abilities:
|
||||
|
||||
STR +{tmp_character.strength}
|
||||
DEX +{tmp_character.dexterity}
|
||||
CON +{tmp_character.constitution}
|
||||
INT +{tmp_character.intelligence}
|
||||
WIS +{tmp_character.wisdom}
|
||||
CHA +{tmp_character.charisma}
|
||||
|
||||
You can swap the values of two abilities around.
|
||||
You can only do this once, so choose carefully!
|
||||
|
||||
To swap the values of e.g. STR and INT, write |wSTR INT|n. Empty to abort.
|
||||
"""
|
||||
|
||||
options = {"key": "_default", "goto": (_swap_abilities, kwargs)}
|
||||
|
||||
return text, options
|
||||
```
|
||||
|
||||
This is more code, but the logic is the same - we have a node (`node_swap_abilities`) and
|
||||
and a goto-callable helper (`_swap_abilities`). We catch everything the user writes on the
|
||||
node (such as `WIS CON`) and feed it into the helper.
|
||||
|
||||
In `_swap_abilities`, we need to analyze the `raw_string` from the user to see what they
|
||||
want to do.
|
||||
|
||||
Most code in the helper is validating the user didn't enter nonsense. If they did,
|
||||
we use `caller.msg()` to tell them and then return `None, kwargs`, which re-runs the same node (the
|
||||
name-selection) all over again.
|
||||
|
||||
Since we want users to be able to write "CON" instead of the longer "constitution", we need a
|
||||
mapping `_ABILITIES` to easily convert between the two (it's stored as `consitution` on the
|
||||
temporary character sheet). Once we know which abilities they want to swap, we do so and tick up
|
||||
the `.ability_changes` counter. This means this option will no longer be available from the main
|
||||
node.
|
||||
|
||||
Finally, we return to `node_chargen` again.
|
||||
|
||||
## Node: Creating the Character
|
||||
|
||||
We get here from the main node by opting to finish chargen.
|
||||
|
||||
```python
|
||||
node_apply_character(caller, raw_string, **kwargs):
|
||||
"""
|
||||
End chargen and create the character. We will also puppet it.
|
||||
|
||||
"""
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
new_character = tmp_character.apply(caller)
|
||||
|
||||
caller.account.db._playable_characters = [new_character]
|
||||
|
||||
text = "Character created!"
|
||||
|
||||
return text, None
|
||||
```
|
||||
When entering the node, we will take the Temporary character sheet and use its `.appy` method to
|
||||
create a new Character with all equipment.
|
||||
|
||||
This is what is called an _end node_, because it returns `None` instead of options. After this,
|
||||
the menu will exit. We will be back to the default character selection screen. The characters
|
||||
found on that screen are the ones listed in the `_playable_characters` Attribute, so we need to
|
||||
also the new character to it.
|
||||
|
||||
|
||||
## Tying the nodes together
|
||||
|
||||
```python
|
||||
def start_chargen(caller, session=None):
|
||||
"""
|
||||
This is a start point for spinning up the chargen from a command later.
|
||||
|
||||
"""
|
||||
menutree = { # <----- can now add this!
|
||||
"node_chargen": node_chargen,
|
||||
"node_change_name": node_change_name,
|
||||
"node_swap_abilities": node_swap_abilities,
|
||||
"node_apply_character": node_apply_character
|
||||
}
|
||||
|
||||
# this generates all random components of the character
|
||||
tmp_character = TemporaryCharacterSheet()
|
||||
tmp_character.generate()
|
||||
|
||||
EvMenu(caller, menutree, session=session,
|
||||
startnode="node_chargen", # <-----
|
||||
tmp_character=tmp_character)
|
||||
|
||||
```
|
||||
|
||||
Now that we have all the nodes, we add them to the `menutree` we left empty before. We only add
|
||||
the nodes, _not_ the goto-helpers! The keys we set in the `menutree` dictionary are the names we
|
||||
should use to point to nodes from inside the menu (and we did).
|
||||
|
||||
We also add a keyword argument `startnode` pointing to the `node_chargen` node. This tells EvMenu
|
||||
to first jump into that node when the menu is starting up.
|
||||
|
||||
## Conclusions
|
||||
|
||||
This lesson taught us how to use `EvMenu` to make an interactive character generator. In an RPG
|
||||
more complex than _Knave_, the menu would be bigger and more intricate, but the same principles
|
||||
apply.
|
||||
|
||||
Together with the previous lessons we have now fished most of the basics around player
|
||||
characters - how they store their stats, handle their equipment and how to create them.
|
||||
|
||||
In the next lesson we'll address how EvAdventure _Rooms_ work.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# In-game Commands
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Dynamically generated Dungeon
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -0,0 +1,599 @@
|
|||
# Handling Equipment
|
||||
|
||||
In _Knave_, you have a certain number of inventory "slots". The amount of slots is given by `CON + 10`.
|
||||
All items (except coins) have a `size`, indicating how many slots it uses. You can't carry more items
|
||||
than you have slot-space for. Also items wielded or worn count towards the slots.
|
||||
|
||||
We still need to track what the character is using however: What weapon they have readied affects the damage
|
||||
they can do. The shield, helmet and armor they use affects their defense.
|
||||
|
||||
We have already set up the possible 'wear/wield locations' when we defined our Objects
|
||||
[in the previous lesson](./Beginner-Tutorial-Objects.md). This is what we have in `enums.py`:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/enums.py
|
||||
|
||||
# ...
|
||||
|
||||
class WieldLocation(Enum):
|
||||
|
||||
BACKPACK = "backpack"
|
||||
WEAPON_HAND = "weapon_hand"
|
||||
SHIELD_HAND = "shield_hand"
|
||||
TWO_HANDS = "two_handed_weapons"
|
||||
BODY = "body" # armor
|
||||
HEAD = "head" # helmets
|
||||
```
|
||||
|
||||
Basically, all the weapon/armor locations are exclusive - you can only have one item in each (or none).
|
||||
The BACKPACK is special - it contains any number of items (up to the maximum slot usage).
|
||||
|
||||
## EquipmentHandler that saves
|
||||
|
||||
> Create a new module `mygame/evadventure/equipment.py`.
|
||||
|
||||
```{sidebar}
|
||||
If you want to understand more about behind how Evennia uses handlers, there is a
|
||||
[dedicated tutorial](../../Tutorial-Persistent-Handler.md) talking about the principle.
|
||||
```
|
||||
In default Evennia, everything you pick up will end up "inside" your character object (that is, have
|
||||
you as its `.location`). This is called your _inventory_ and has no limit. We will keep 'moving items into us'
|
||||
when we pick them up, but we will add more functionality using an _Equipment handler_.
|
||||
|
||||
A handler is (for our purposes) an object that sits "on" another entity, containing functionality
|
||||
for doing one specific thing (managing equipment, in our case).
|
||||
|
||||
This is the start of our handler:
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation
|
||||
|
||||
class EquipmentHandler:
|
||||
save_attribute = "inventory_slots"
|
||||
|
||||
def __init__(self, obj):
|
||||
# here obj is the character we store the handler on
|
||||
self.obj = obj
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
"""Load our data from an Attribute on `self.obj`"""
|
||||
self.slots = self.obj.attributes.get(
|
||||
self.save_attribute,
|
||||
category="inventory",
|
||||
default={
|
||||
WieldLocation.WEAPON_HAND: None,
|
||||
WieldLocation.SHIELD_HAND: None,
|
||||
WieldLocation.TWO_HANDS: None,
|
||||
WieldLocation.BODY: None,
|
||||
WieldLocation.HEAD: None,
|
||||
WieldLocation.BACKPACK: []
|
||||
}
|
||||
)
|
||||
|
||||
def _save(self):
|
||||
"""Save our data back to the same Attribute"""
|
||||
self.obj.attributes.add(self.save_attribute, self.slots, category="inventory")
|
||||
```
|
||||
|
||||
This is a compact and functional little handler. Before analyzing how it works, this is how
|
||||
we will add it to the Character:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/characters.py
|
||||
|
||||
# ...
|
||||
|
||||
from evennia.utils.utils import lazy_property
|
||||
from .equipment import EquipmentHandler
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
|
||||
# ...
|
||||
|
||||
@lazy_property
|
||||
def equipment(self):
|
||||
return EquipmentHandler(self)
|
||||
```
|
||||
|
||||
After reloading the server, the equipment-handler will now be accessible on character-instances as
|
||||
|
||||
character.equipment
|
||||
|
||||
The `@lazy_property` works such that it will not load the handler until someone actually tries to
|
||||
fetch it with `character.equipment`. When that
|
||||
happens, we start up the handler and feed it `self` (the `Character` instance itself). This is what
|
||||
enters `__init__` as `.obj` in the `EquipmentHandler` code above.
|
||||
|
||||
So we now have a handler on the character, and the handler has a back-reference to the character it sits
|
||||
on.
|
||||
|
||||
Since the handler itself is just a regular Python object, we need to use the `Character` to store
|
||||
our data - our _Knave_ "slots". We must save them to the database, because we want the server to remember
|
||||
them even after reloading.
|
||||
|
||||
Using `self.obj.attributes.add()` and `.get()` we save the data to the Character in a specially named
|
||||
[Attribute](../../../Components/Attributes.md). Since we use a `category`, we are unlikely to collide with
|
||||
other Attributes.
|
||||
|
||||
Our storage structure is a `dict` with keys after our available `WieldLocation` enums. Each can only
|
||||
have one item except `WieldLocation.BACKPACK`, which is a list.
|
||||
|
||||
## Connecting the EquipmentHandler
|
||||
|
||||
Whenever an object leaves from one location to the next, Evennia will call a set of _hooks_ (methods) on the
|
||||
object that moves, on the source-location and on its destination. This is the same for all moving things -
|
||||
whether it's a character moving between rooms or an item being dropping from your hand to the ground.
|
||||
|
||||
We need to tie our new `EquipmentHandler` into this system. By reading the doc page on [Objects](../../../Components/Objects.md),
|
||||
or looking at the [DefaultObject.move_to](evennia.objects.objects.DefaultObject.move_to) docstring, we'll
|
||||
find out what hooks Evennia will call. Here `self` is the object being moved from
|
||||
`source_location` to `destination`:
|
||||
|
||||
|
||||
1. `self.at_pre_move(destination)` (abort if return False)
|
||||
2. `source_location.at_pre_object_leave(self, destination)` (abort if return False)
|
||||
3. `destination.at_pre_object_receive(self, source_location)` (abort if return False)
|
||||
4. `source_location.at_object_leave(self, destination)`
|
||||
5. `self.announce_move_from(destination)`
|
||||
6. (move happens here)
|
||||
7. `self.announce_move_to(source_location)`
|
||||
8. `destination.at_object_receive(self, source_location)`
|
||||
9. `self.at_post_move(source_location)`
|
||||
|
||||
All of these hooks can be overridden to customize movement behavior. In this case we are interested in
|
||||
controlling how items 'enter' and 'leave' our character - being 'inside' the character is the same as
|
||||
them 'carrying' it. We have three good hook-candidates to use for this.
|
||||
|
||||
- `.at_pre_object_receive` - used to check if you can actually pick something up, or if your equipment-store is full.
|
||||
- `.at_object_receive` - used to add the item to the equipmenthandler
|
||||
- `.at_object_leave` - used to remove the item from the equipmenthandler
|
||||
|
||||
You could also picture using `.at_pre_object_leave` to restrict dropping (cursed?) items, but
|
||||
we will skip that for this tutorial.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/character.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
|
||||
# ...
|
||||
|
||||
def at_pre_object_receive(self, moved_object, source_location, **kwargs):
|
||||
"""Called by Evennia before object arrives 'in' this character (that is,
|
||||
if they pick up something). If it returns False, move is aborted.
|
||||
|
||||
"""
|
||||
return self.equipment.validate_slot_usage(moved_object)
|
||||
|
||||
def at_object_receive(self, moved_object, source_location, **kwargs):
|
||||
"""
|
||||
Called by Evennia when an object arrives 'in' the character.
|
||||
|
||||
"""
|
||||
self.equipment.add(moved_object)
|
||||
|
||||
def at_object_leave(self, moved_object, destination, **kwargs):
|
||||
"""
|
||||
Called by Evennia when object leaves the Character.
|
||||
|
||||
"""
|
||||
self.equipment.remove(moved_object)
|
||||
```
|
||||
|
||||
Above we have assumed the `EquipmentHandler` (`.equipment`) has methods `.validate_slot_usage`,
|
||||
`.add` and `.remove`. But we haven't actually added them yet - we just put some reasonable names! Before
|
||||
we can use this, we need to go actually adding those methods.
|
||||
|
||||
## Expanding the Equipmenthandler
|
||||
|
||||
## `.validate_slot_usage`
|
||||
|
||||
Let's start with implementing the first method we came up with above, `validate_slot_usage`:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation, Ability
|
||||
|
||||
class EquipmentError(TypeError):
|
||||
"""All types of equipment-errors"""
|
||||
pass
|
||||
|
||||
class EquipmentHandler:
|
||||
|
||||
# ...
|
||||
|
||||
@property
|
||||
def max_slots(self):
|
||||
"""Max amount of slots, based on CON defense (CON + 10)"""
|
||||
return getattr(self.obj, Ability.CON.value, 1) + 10
|
||||
|
||||
def count_slots(self):
|
||||
"""Count current slot usage"""
|
||||
slots = self.slots
|
||||
wield_usage = sum(
|
||||
getattr(slotobj, "size", 0) or 0
|
||||
for slot, slotobj in slots.items()
|
||||
if slot is not WieldLocation.BACKPACK
|
||||
)
|
||||
backpack_usage = sum(
|
||||
getattr(slotobj, "size", 0) or 0 for slotobj in slots[WieldLocation.BACKPACK]
|
||||
)
|
||||
return wield_usage + backpack_usage
|
||||
|
||||
def validate_slot_usage(self, obj):
|
||||
"""
|
||||
Check if obj can fit in equipment, based on its size.
|
||||
|
||||
"""
|
||||
if not inherits_from(obj, EvAdventureObject):
|
||||
# in case we mix with non-evadventure objects
|
||||
raise EquipmentError(f"{obj.key} is not something that can be equipped.")
|
||||
|
||||
size = obj.size
|
||||
max_slots = self.max_slots
|
||||
current_slot_usage = self.count_slots()
|
||||
return current_slot_usage + size <= max_slots:
|
||||
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
The `@property` decorator turns a method into a property so you don't need to 'call' it.
|
||||
That is, you can access `.max_slots` instead of `.max_slots()`. In this case, it's just a
|
||||
little less to type.
|
||||
```
|
||||
We add two helpers - the `max_slots` _property_ and `count_slots`, a method that calculate the current
|
||||
slots being in use. Let's figure out how they work.
|
||||
|
||||
### `.max_slots`
|
||||
|
||||
For `max_slots`, remember that `.obj` on the handler is a back-reference to the `EvAdventureCharacter` we
|
||||
put this handler on. `getattr` is a Python method for retrieving a named property on an object.
|
||||
The `Enum` `Ability.CON.value` is the string `Constitution` (check out the
|
||||
[first Utility and Enums tutorial](./Beginner-Tutorial-Utilities.md) if you don't recall).
|
||||
|
||||
So to be clear,
|
||||
|
||||
```python
|
||||
getattr(self.obj, Ability.CON.value) + 10
|
||||
```
|
||||
is the same as writing
|
||||
|
||||
```python
|
||||
getattr(your_character, "Constitution") + 10
|
||||
```
|
||||
|
||||
which is the same as doing something like this:
|
||||
|
||||
```python
|
||||
your_character.Constitution + 10
|
||||
```
|
||||
|
||||
In our code we write `getattr(self.obj, Ability.CON.value, 1)` - that extra `1` means that if there
|
||||
should happen to _not_ be a property "Constitution" on `self.obj`, we should not error out but just
|
||||
return 1.
|
||||
|
||||
|
||||
### `.count_slots`
|
||||
|
||||
In this helper we use two Python tools - the `sum()` function and a
|
||||
[list comprehension](https://www.w3schools.com/python/python_lists_comprehension.asp). The former
|
||||
simply adds the values of any iterable together. The latter is a more efficient way to create a list:
|
||||
|
||||
new_list = [item for item in some_iterable if condition]
|
||||
all_above_5 = [num for num in range(10) if num > 5] # [6, 7, 8, 9]
|
||||
all_below_5 = [num for num in range(10) if num < 5] # [0, 1, 2, 3, 4]
|
||||
|
||||
To make it easier to understand, try reading the last line above as "for every number in the range 0-9,
|
||||
pick all with a value below 5 and make a list of them". You can also embed such comprehensions
|
||||
directly in a function call like `sum()` without using `[]` around it.
|
||||
|
||||
In `count_slots` we have this code:
|
||||
|
||||
```python
|
||||
wield_usage = sum(
|
||||
getattr(slotobj, "size", 0)
|
||||
for slot, slotobj in slots.items()
|
||||
if slot is not WieldLocation.BACKPACK
|
||||
)
|
||||
```
|
||||
|
||||
We should be able to follow all except `slots.items()`. Since `slots` is a `dict`, we can use `.items()`
|
||||
to get a sequence of `(key, value)` pairs. We store these in `slot` and `slotobj`. So the above can
|
||||
be understood as "for every `slot` and `slotobj`-pair in `slots`, check which slot location it is.
|
||||
If it is _not_ in the backpack, get its size and add it to the list. Sum over all these
|
||||
sizes".
|
||||
|
||||
A less compact but maybe more readonable way to write this would be:
|
||||
|
||||
```python
|
||||
backpack_item_sizes = []
|
||||
for slot, slotobj in slots.items():
|
||||
if slot is not WieldLocation.BACKPACK:
|
||||
size = getattr(slotobj, "size", 0)
|
||||
backpack_item_sizes.append(size)
|
||||
wield_usage = sum(backpack_item_sizes)
|
||||
```
|
||||
|
||||
The same is done for the items actually in the BACKPACK slot. The total sizes are added
|
||||
together.
|
||||
|
||||
### Validating slots
|
||||
|
||||
With these helpers in place, `validate_slot_usage` now becomes simple. We use `max_slots` to see how much we can carry.
|
||||
We then get how many slots we are already using (with `count_slots`) and see if our new `obj`'s size
|
||||
would be too much for us.
|
||||
|
||||
## `.add` and `.remove`
|
||||
|
||||
We will make it so `.add` puts something in the `BACKPACK` location and `remove` drops it, wherever
|
||||
it is (even if it was in your hands).
|
||||
|
||||
```python
|
||||
# mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EquipmentHandler:
|
||||
|
||||
# ...
|
||||
|
||||
def add(self, obj):
|
||||
"""
|
||||
Put something in the backpack.
|
||||
"""
|
||||
self.validate_slot_usage(obj)
|
||||
self.slots[WieldLocation.BACKPACK].append(obj)
|
||||
self._save()
|
||||
|
||||
def remove(self, slot):
|
||||
"""
|
||||
Remove contents of a particular slot, for
|
||||
example `equipment.remove(WieldLocation.SHIELD_HAND)`
|
||||
"""
|
||||
slots = self.slots
|
||||
ret = []
|
||||
if slot is WieldLocation.BACKPACK:
|
||||
# empty entire backpack!
|
||||
ret.extend(slots[slot])
|
||||
slots[slot] = []
|
||||
else:
|
||||
ret.append(slots[slot])
|
||||
slots[slot] = None
|
||||
if ret:
|
||||
self._save()
|
||||
return ret
|
||||
```
|
||||
|
||||
Both of these should be straight forward to follow. In `.add`, we make use of `validate_slot_usage` to
|
||||
double-check we can actually fit the thing, then we add the item to the backpack.
|
||||
|
||||
In `.delete`, we allow emptying by `WieldLocation` - we figure out what slot it is and return
|
||||
the item within (if any). If we gave `BACKPACK` as the slot, we empty the backpack and
|
||||
return all items.
|
||||
|
||||
Whenever we change the equipment loadout we must make sure to `._save()` the result, or it will
|
||||
be lost after a server reload.
|
||||
|
||||
## Moving things around
|
||||
|
||||
With the help of `.remove()` and `.add()` we can get things in and out of the `BACKPACK` equipment
|
||||
location. We also need to grab stuff from the backpack and wield or wear it. We add a `.move` method
|
||||
on the `EquipmentHandler` to do this:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EquipmentHandler:
|
||||
|
||||
# ...
|
||||
|
||||
def move(self, obj):
|
||||
"""Move object from backpack to its intended `inventory_use_slot`."""
|
||||
|
||||
# make sure to remove from equipment/backpack first, to avoid double-adding
|
||||
self.remove(obj)
|
||||
|
||||
slots = self.slots
|
||||
use_slot = getattr(obj, "inventory_use_slot", WieldLocation.BACKPACK)
|
||||
|
||||
to_backpack = []
|
||||
if use_slot is WieldLocation.TWO_HANDS:
|
||||
# two-handed weapons can't co-exist with weapon/shield-hand used items
|
||||
to_backpack = [slots[WieldLocation.WEAPON_HAND], slots[WieldLocation.SHIELD_HAND]]
|
||||
slots[WieldLocation.WEAPON_HAND] = slots[WieldLocation.SHIELD_HAND] = None
|
||||
slots[use_slot] = obj
|
||||
elif use_slot in (WieldLocation.WEAPON_HAND, WieldLocation.SHIELD_HAND):
|
||||
# can't keep a two-handed weapon if adding a one-handed weapon or shield
|
||||
to_backpack = [slots[WieldLocation.TWO_HANDS]]
|
||||
slots[WieldLocation.TWO_HANDS] = None
|
||||
slots[use_slot] = obj
|
||||
elif use_slot is WieldLocation.BACKPACK:
|
||||
# it belongs in backpack, so goes back to it
|
||||
to_backpack = [obj]
|
||||
else:
|
||||
# for others (body, head), just replace whatever's there
|
||||
replaced = [obj]
|
||||
slots[use_slot] = obj
|
||||
|
||||
for to_backpack_obj in to_backpack:
|
||||
# put stuff in backpack
|
||||
slots[use_slot].append(to_backpack_obj)
|
||||
|
||||
# store new state
|
||||
self._save()
|
||||
```
|
||||
|
||||
Here we remember that every `EvAdventureObject` has an `inventory_use_slot` property that tells us where
|
||||
it goes. So we just need to move the object to that slot, replacing whatever is in that place
|
||||
from before. Anything we replace goes back to the backpack.
|
||||
|
||||
## Get everything
|
||||
|
||||
In order to visualize our inventory, we need some method to get everything we are carrying.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EquipmentHandler:
|
||||
|
||||
# ...
|
||||
|
||||
def all(self):
|
||||
"""
|
||||
Get all objects in inventory, regardless of location.
|
||||
"""
|
||||
slots = self.slots
|
||||
lst = [
|
||||
(slots[WieldLocation.WEAPON_HAND], WieldLocation.WEAPON_HAND),
|
||||
(slots[WieldLocation.SHIELD_HAND], WieldLocation.SHIELD_HAND),
|
||||
(slots[WieldLocation.TWO_HANDS], WieldLocation.TWO_HANDS),
|
||||
(slots[WieldLocation.BODY], WieldLocation.BODY),
|
||||
(slots[WieldLocation.HEAD], WieldLocation.HEAD),
|
||||
] + [(item, WieldLocation.BACKPACK) for item in slots[WieldLocation.BACKPACK]]
|
||||
return lst
|
||||
```
|
||||
|
||||
Here we get all the equipment locations and add their contents together into a list of tuples
|
||||
`[(item, WieldLocation), ...]`. This is convenient for display.
|
||||
|
||||
## Weapon and armor
|
||||
|
||||
It's convenient to have the `EquipmentHandler` easily tell you what weapon is currently wielded
|
||||
and what _armor_ level all worn equipment provides. Otherwise you'd need to figure out what item is
|
||||
in which wield-slot and to add up armor slots manually every time you need to know.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/equipment.py
|
||||
|
||||
from .objects import WeaponEmptyHand
|
||||
from .enums import WieldLocation, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EquipmentHandler:
|
||||
|
||||
# ...
|
||||
|
||||
@property
|
||||
def armor(self):
|
||||
slots = self.slots
|
||||
return sum(
|
||||
(
|
||||
# armor is listed using its defense, so we remove 10 from it
|
||||
# (11 is base no-armor value in Knave)
|
||||
getattr(slots[WieldLocation.BODY], "armor", 1),
|
||||
# shields and helmets are listed by their bonus to armor
|
||||
getattr(slots[WieldLocation.SHIELD_HAND], "armor", 0),
|
||||
getattr(slots[WieldLocation.HEAD], "armor", 0),
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def weapon(self):
|
||||
# first checks two-handed wield, then one-handed; the two
|
||||
# should never appear simultaneously anyhow (checked in `move` method).
|
||||
slots = self.slots
|
||||
weapon = slots[WieldLocation.TWO_HANDS]
|
||||
if not weapon:
|
||||
weapon = slots[WieldLocation.WEAPON_HAND]
|
||||
if not weapon:
|
||||
weapon = WeaponEmptyHand()
|
||||
return weapon
|
||||
|
||||
```
|
||||
|
||||
In the `.armor()` method we get the item (if any) out of each relevant wield-slot (body, shield, head),
|
||||
and grab their `armor` Attribute. We then `sum()` them all up.
|
||||
|
||||
In `.weapon()`, we simply check which of the possible weapon slots (weapon-hand or two-hands) have
|
||||
something in them. If not we fall back to the 'fake' weapon `WeaponEmptyHand` which is just a 'dummy'
|
||||
object that represents your bare hands with damage and all.
|
||||
(created in [The Object tutorial](./Beginner-Tutorial-Objects.md#your-bare-hands) earlier).
|
||||
|
||||
|
||||
## Extra credits
|
||||
|
||||
This covers the basic functionality of the equipment handler. There are other useful methods that
|
||||
can be added:
|
||||
|
||||
- Given an item, figure out which equipment slot it is currently in
|
||||
- Make a string representing the current loadout
|
||||
- Get everything in the backpack (only)
|
||||
- Get all wieldable items (weapons, shields) from backpack
|
||||
- Get all usable items (items with a use-location of `BACKPACK`) from the backpack
|
||||
|
||||
Experiment with adding those. A full example is found in
|
||||
[evennia/contrib/tutorials/evadventure/equipment.py](evennia.contrib.tutorials.evadventure.equipment).
|
||||
|
||||
## Unit Testing
|
||||
|
||||
> Create a new module `mygame/evadventure/tests/test_equipment.py`.
|
||||
|
||||
```{sidebar}
|
||||
See [evennia/contrib/tutorials/evadventure/tests/test_equipment.py](evennia.contrib.tutorials.evadventure.tests.test_equipment)
|
||||
for a finished testing example.
|
||||
```
|
||||
|
||||
To test the `EquipmentHandler`, easiest is create an `EvAdventureCharacter` (this should by now
|
||||
have `EquipmentHandler` available on itself as `.equipment`) and a few test objects; then test
|
||||
passing these into the handler's methods.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/tests/test_equipment.py
|
||||
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
||||
from ..objects import EvAdventureRoom
|
||||
from ..enums import WieldLocation
|
||||
|
||||
class TestEquipment(BaseEvenniaTest):
|
||||
|
||||
def setUp(self):
|
||||
self.character = create.create_object(EvAdventureCharacter, key='testchar')
|
||||
self.helmet = create.create_object(EvAdventureHelmet, key="helmet")
|
||||
self.weapon = create.create_object(EvAdventureWeapon, key="weapon")
|
||||
|
||||
def test_add_remove):
|
||||
self.character.equipment.add(self.helmet)
|
||||
self.assertEqual(
|
||||
self.character.equipment.slots[WieldLocation.BACKPACK],
|
||||
[self.helmet]
|
||||
)
|
||||
self.character.equipment.remove(self.helmet)
|
||||
self.assertEqual(self.character.equipment.slots[WieldLocation.BACKPACK], [])
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
_Handlers_ are useful for grouping functionality together. Now that we spent our time making the
|
||||
`EquipmentHandler`, we shouldn't need to worry about item-slots anymore - the handler 'handles' all
|
||||
the details for us. As long as we call its methods, the details can be forgotten about.
|
||||
|
||||
We also learned to use _hooks_ to tie _Knave_'s custom equipment handling into Evennia.
|
||||
|
||||
With `Characters`, `Objects` and now `Equipment` in place, we should be able to move on to character
|
||||
generation - where players get to make their own character!
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Non-Player-Characters (NPCs)
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -0,0 +1,360 @@
|
|||
# In-game Objects and items
|
||||
|
||||
In the previous lesson we established what a 'Character' is in our game. Before we continue
|
||||
we also need to have a notion what an 'item' or 'object' is.
|
||||
|
||||
Looking at _Knave_'s item lists, we can get some ideas of what we need to track:
|
||||
|
||||
- `size` - this is how many 'slots' the item uses in the character's inventory.
|
||||
- `value` - a base value if we want to sell or buy the item.
|
||||
- `inventory_use_slot` - some items can be worn or wielded. For example, a helmet needs to be
|
||||
worn on the head and a shield in the shield hand. Some items can't be used this way at all, but
|
||||
only belong in the backpack.
|
||||
- `obj_type` - Which 'type' of item this is.
|
||||
|
||||
|
||||
## New Enums
|
||||
|
||||
We added a few enumberations for Abilities back in the [Utilities tutorial](./Beginner-Tutorial-Utilities.md).
|
||||
Before we continue, let's expand with enums for use-slots and object types.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/enums.py
|
||||
|
||||
# ...
|
||||
|
||||
class WieldLocation(Enum):
|
||||
|
||||
BACKPACK = "backpack"
|
||||
WEAPON_HAND = "weapon_hand"
|
||||
SHIELD_HAND = "shield_hand"
|
||||
TWO_HANDS = "two_handed_weapons"
|
||||
BODY = "body" # armor
|
||||
HEAD = "head" # helmets
|
||||
|
||||
class ObjType(Enum):
|
||||
|
||||
WEAPON = "weapon"
|
||||
ARMOR = "armor"
|
||||
SHIELD = "shield"
|
||||
HELMET = "helmet"
|
||||
CONSUMABLE = "consumable"
|
||||
GEAR = "gear"
|
||||
MAGIC = "magic"
|
||||
QUEST = "quest"
|
||||
TREASURE = "treasure"
|
||||
```
|
||||
|
||||
Once we have these enums, we will use them for referencing things.
|
||||
|
||||
## The base object
|
||||
|
||||
> Create a new module `mygame/evadventure/objects.py`
|
||||
|
||||
```{sidebar}
|
||||
[evennia/contrib/tutorials/evadventure/objects.py](evennia.contrib.tutorials.evadventure.objects) has
|
||||
a full set of objects implemented.
|
||||
```
|
||||
<div style="clear: right;"></div>
|
||||
|
||||
We will make a base `EvAdventureObject` class off Evennia's standard `DefaultObject`. We will then add
|
||||
child classes to represent the relevant types:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
from evennia import AttributeProperty, DefaultObject
|
||||
from evennia.utils.utils import make_iter
|
||||
from .utils import get_obj_stats
|
||||
from .enums import WieldLocation, ObjType
|
||||
|
||||
|
||||
class EvAdventureObject(DefaultObject):
|
||||
"""
|
||||
Base for all evadventure objects.
|
||||
|
||||
"""
|
||||
inventory_use_slot = WieldLocation.BACKPACK
|
||||
size = AttributeProperty(1, autocreate=False)
|
||||
value = AttributeProperty(0, autocreate=False)
|
||||
|
||||
# this can be either a single type or a list of types (for objects able to be
|
||||
# act as multiple). This is used to tag this object during creation.
|
||||
obj_type = ObjType.GEAR
|
||||
|
||||
def at_object_creation(self):
|
||||
"""Called when this object is first created. We convert the .obj_type
|
||||
property to a database tag."""
|
||||
|
||||
for obj_type in make_iter(self.obj_type):
|
||||
self.tags.add(self.obj_type.value, category="obj_type")
|
||||
|
||||
def get_help(self):
|
||||
"""Get any help text for this item"""
|
||||
return "No help for this item"
|
||||
```
|
||||
|
||||
### Using Attributes or not
|
||||
|
||||
In theory, `size` and `value` does not change and _could_ also be just set as a regular Python
|
||||
property on the class:
|
||||
|
||||
```python
|
||||
class EvAdventureObject(DefaultObject):
|
||||
inventory_use_slot = WieldLocation.BACKPACK
|
||||
size = 1
|
||||
value = 0
|
||||
```
|
||||
|
||||
The problem with this is that if we want to make a new object of `size 3` and `value 20`, we have to
|
||||
make a new class for it. We can't change it on the fly because the change would only be in memory and
|
||||
be lost on next server reload.
|
||||
|
||||
Because we use `AttributeProperties`, we can set `size` and `value` to whatever we like when we
|
||||
create the object (or later), and the Attributes will remember our changes to that object indefinitely.
|
||||
|
||||
To make this a little more efficient, we use `autocreate=False`. Normally when you create a
|
||||
new object with defined `AttributeProperties`, a matching `Attribute` is immediately created at
|
||||
the same time. So normally, the object would be created along with two Attributes `size` and `value`.
|
||||
With `autocreate=False`, no Attribute will be created _unless the default is changed_. That is, as
|
||||
long as your object has `size=1` no database `Attribute` will be created at all. This saves time and
|
||||
resources when creating large number of objects.
|
||||
|
||||
The drawback is that since no Attribute is created you can't refer to it
|
||||
with `obj.db.size` or `obj.attributes.get("size")` _unless you change its default_. You also can't query
|
||||
the database for all objects with `size=1`, since most objects would not yet have an in-database
|
||||
`size` Attribute to search for.
|
||||
|
||||
In our case, we'll only refer to these properties as `obj.size` etc, and have no need to find
|
||||
all objects of a particular size. So we should be safe.
|
||||
|
||||
### Creating tags in `at_object_creation`
|
||||
|
||||
The `at_object_creation` is a method Evennia calls on every child of `DefaultObject` whenever it is
|
||||
first created.
|
||||
|
||||
We do a tricky thing here, converting our `.obj_type` to one or more [Tags](../../../Components/Tags.md). Tagging the
|
||||
object like this means you can later efficiently find all objects of a given type (or combination of
|
||||
types) with Evennia's search functions:
|
||||
|
||||
```python
|
||||
from .enums import ObjType
|
||||
from evennia.utils import search
|
||||
|
||||
# get all shields in the game
|
||||
all_shields = search.search_object_by_tag(ObjType.SHIELD.value, category="obj_type")
|
||||
```
|
||||
|
||||
We allow `.obj_type` to be given as a single value or a list of values. We use `make_iter` from the
|
||||
evennia utility library to make sure we don't balk at either. This means you could have a Shield that
|
||||
is also Magical, for example.
|
||||
|
||||
## Other object types
|
||||
|
||||
Some of the other object types are very simple so far.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
from evennia import AttributeProperty, DefaultObject
|
||||
from .enums import ObjType
|
||||
|
||||
class EvAdventureObject(DefaultObject):
|
||||
# ...
|
||||
|
||||
|
||||
class EvAdventureQuestObject(EvAdventureObject):
|
||||
"""Quest objects should usually not be possible to sell or trade."""
|
||||
obj_type = ObjType.QUEST
|
||||
|
||||
class EvAdventureTreasure(EvAdventureObject):
|
||||
"""Treasure is usually just for selling for coin"""
|
||||
obj_type = ObjType.TREASURE
|
||||
value = AttributeProperty(100, autocreate=False)
|
||||
|
||||
```
|
||||
|
||||
## Consumables
|
||||
|
||||
A 'consumable' is an item that has a certain number of 'uses'. Once fully consumed, it can't be used
|
||||
anymore. An example would be a health potion.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureConsumable(EvAdventureObject):
|
||||
"""An item that can be used up"""
|
||||
|
||||
obj_type = ObjType.CONSUMABLE
|
||||
value = AttributeProperty(0.25, autocreate=False)
|
||||
uses = AttributeProperty(1, autocreate=False)
|
||||
|
||||
def at_pre_use(self, user, *args, **kwargs):
|
||||
"""Called before using. If returning False, abort use."""
|
||||
return uses > 0
|
||||
|
||||
def at_use(self, user, *args, **kwargs):
|
||||
"""Called when using the item"""
|
||||
pass
|
||||
|
||||
def at_post_use(self. user, *args, **kwargs):
|
||||
"""Called after using the item"""
|
||||
# detract a usage, deleting the item if used up.
|
||||
self.uses -= 1
|
||||
if self.uses <= 0:
|
||||
user.msg(f"{self.key} was used up.")
|
||||
self.delete()
|
||||
```
|
||||
|
||||
What exactly each consumable does will vary - we will need to implement children of this class
|
||||
later, overriding `at_use` with different effects.
|
||||
|
||||
## Weapons
|
||||
|
||||
All weapons need properties that describe how efficient they are in battle.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
from .enums import WieldLocation, ObjType, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureWeapon(EvAdventureObject):
|
||||
"""Base class for all weapons"""
|
||||
|
||||
obj_type = ObjType.WEAPON
|
||||
inventory_use_slot = AttributeProperty(WieldLocation.WEAPON_HAND, autocreate=False)
|
||||
quality = AttributeProperty(3, autocreate=False)
|
||||
|
||||
attack_type = AttibuteProperty(Ability.STR, autocreate=False)
|
||||
defend_type = AttibuteProperty(Ability.ARMOR, autocreate=False)
|
||||
|
||||
damage_roll = AttibuteProperty("1d6", autocreate=False)
|
||||
```
|
||||
|
||||
The `quality` is something we need to track in _Knave_. When getting critical failures on attacks,
|
||||
a weapon's quality will go down. When it reaches 0, it will break.
|
||||
|
||||
The attack/defend type tracks how we resolve attacks with the weapon, like `roll + STR vs ARMOR + 10`.
|
||||
|
||||
## Magic
|
||||
|
||||
In _Knave_, anyone can use magic if they are wielding a rune stone (our name for spell books) in both
|
||||
hands. You can only use a rune stone once per rest. So a rune stone is an example of a 'magical weapon'
|
||||
that is also a 'consumable' of sorts.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
# ...
|
||||
class EvAdventureConsumable(EvAdventureObject):
|
||||
# ...
|
||||
|
||||
class EvAdventureWeapon(EvAdventureObject):
|
||||
# ...
|
||||
|
||||
class EvAdventureRuneStone(EvAdventureWeapon, EvAdventureConsumable):
|
||||
"""Base for all magical rune stones"""
|
||||
|
||||
obj_type = (ObjType.WEAPON, ObjType.MAGIC)
|
||||
inventory_use_slot = WieldLocation.TWO_HANDS # always two hands for magic
|
||||
quality = AttributeProperty(3, autocreate=False)
|
||||
|
||||
attack_type = AttibuteProperty(Ability.INT, autocreate=False)
|
||||
defend_type = AttibuteProperty(Ability.DEX, autocreate=False)
|
||||
|
||||
damage_roll = AttibuteProperty("1d8", autocreate=False)
|
||||
|
||||
def at_post_use(self, user, *args, **kwargs):
|
||||
"""Called after usage/spell was cast"""
|
||||
self.uses -= 1
|
||||
# we don't delete the rune stone here, but
|
||||
# it must be reset on next rest.
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the rune stone (normally after rest)"""
|
||||
self.uses = 1
|
||||
```
|
||||
|
||||
We make the rune stone a mix of weapon and consumable. Note that we don't have to add `.uses`
|
||||
again, it's inherited from `EvAdventureConsumable` parent. The `at_pre_use` and `at_use` methods
|
||||
are also inherited; we only override `at_post_use` since we don't want the runestone to be deleted
|
||||
when it runs out of uses.
|
||||
|
||||
We add a little convenience method `refresh` - we should call this when the character rests, to
|
||||
make the runestone active again.
|
||||
|
||||
Exactly what rune stones _do_ will be implemented in the `at_use` methods of subclasses to this
|
||||
base class. Since magic in _Knave_ tends to be pretty custom, it makes sense that it will lead to a lot
|
||||
of custom code.
|
||||
|
||||
|
||||
## Armor
|
||||
|
||||
Armor, shields and helmets increase the `ARMOR` stat of the character. In _Knave_, what is stored is the
|
||||
defense value of the armor (values 11-20). We will instead store the 'armor bonus' (1-10). As we know,
|
||||
defending is always `bonus + 10`, so the result will be the same - this means
|
||||
we can use `Ability.ARMOR` as any other defensive ability without worrying about a special case.
|
||||
|
||||
``
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureAmor(EvAdventureObject):
|
||||
obj_type = ObjType.ARMOR
|
||||
inventory_use_slot = WieldLocation.BODY
|
||||
|
||||
armor = AttributeProperty(1, autocreate=False)
|
||||
quality = AttributeProperty(3, autocreate=False)
|
||||
|
||||
|
||||
class EvAdventureShield(EvAdventureArmor):
|
||||
obj_type = ObjType.SHIELD
|
||||
inventory_use_slot = WieldLocation.SHIELD_HAND
|
||||
|
||||
|
||||
class EvAdventureHelmet(EvAdventureArmor):
|
||||
obj_type = ObjType.HELMET
|
||||
inventory_use_slot = WieldLocation.HEAD
|
||||
```
|
||||
|
||||
## Your Bare hands
|
||||
|
||||
This is a 'dummy' object that is not stored in the database. We will use this in the upcoming
|
||||
[Equipment tutorial lesson](./Beginner-Tutorial-Equipment.md) to represent when you have 'nothing'
|
||||
in your hands. This way we don't need to add any special case for this.
|
||||
|
||||
```python
|
||||
class WeaponEmptyHand:
|
||||
obj_type = ObjType.WEAPON
|
||||
key = "Empty Fists"
|
||||
inventory_use_slot = WieldLocation.WEAPON_HAND
|
||||
attack_type = Ability.STR
|
||||
defense_type = Ability.ARMOR
|
||||
damage_roll = "1d4"
|
||||
quality = 100000 # let's assume fists are always available ...
|
||||
|
||||
def __repr__(self):
|
||||
return "<WeaponEmptyHand>"
|
||||
```
|
||||
|
||||
## Testing and Extra credits
|
||||
|
||||
Remember the `get_obj_stats` function from the [Utility Tutorial](./Beginner-Tutorial-Utilities.md) earlier?
|
||||
We had to use dummy-values since we didn't yet know how we would store properties on Objects in the game.
|
||||
|
||||
Well, we just figured out all we need! You can go back and update `get_obj_stats` to properly read the data
|
||||
from the object it receives.
|
||||
|
||||
When you change this function you must also update the related unit test - so your existing test becomes a
|
||||
nice way to test your new Objects as well! Add more tests showing the output of feeding different object-types
|
||||
to `get_obj_stats`.
|
||||
|
||||
Try it out yourself. If you need help, a finished utility example is found in [evennia/contrib/tutorials/evadventure/utils.py](get_obj_stats).
|
||||
|
|
@ -1,5 +1,11 @@
|
|||
# Part 3: How we get there
|
||||
|
||||
```{warning}
|
||||
The tutorial game is under development and is not yet complete, nor tested. Use the existing
|
||||
lessons as inspiration and to help get you going, but don't expect out-of-the-box perfection
|
||||
from it at this time.
|
||||
```
|
||||
|
||||
```{eval-rst}
|
||||
.. sidebar:: Beginner Tutorial Parts
|
||||
|
||||
|
|
@ -17,47 +23,59 @@
|
|||
Taking our new game online and let players try it out
|
||||
```
|
||||
|
||||
In part three of the Evennia Beginner tutorial we will go through the creation of several key parts of our tutorial
|
||||
game _EvAdventure_. This is a pretty big part with plenty of examples.
|
||||
In part three of the Evennia Beginner tutorial we will go through the actual creation of
|
||||
our tutorial game _EvAdventure_, based on the [Knave](https://www.drivethrurpg.com/product/250888/Knave)
|
||||
RPG ruleset.
|
||||
|
||||
If you followed the previous parts of this tutorial you will have some notions about Python and where to find
|
||||
and make use of things in Evennia. We also have a good idea of the type of game we want.
|
||||
Even if this is not the game-style you are interested in, following along will give you a lot of experience
|
||||
with using Evennia. This be of much use when doing your own thing later.
|
||||
This is a big part. You'll be seeing a lot of code and there are plenty of lessons to go through.
|
||||
Take your time!
|
||||
|
||||
If you followed the previous parts of this tutorial you will have some notions about Python and where to
|
||||
find and make use of things in Evennia. We also have a good idea of the type of game we will
|
||||
create.
|
||||
|
||||
Even if this is not the game-style you are interested in, following along will give you a lot
|
||||
of experience using Evennia and be really helpful for doing your own thing later!
|
||||
|
||||
Fully coded examples of all code we make in this part can be found in the
|
||||
[evennia/contrib/tutorials/evadventure](evennia.contrib.tutorials.evadventure) package.
|
||||
|
||||
## Lessons
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Implementing-a-game-rule-system
|
||||
Turn-based-Combat-System
|
||||
A-Sittable-Object
|
||||
|
||||
Beginner-Tutorial-Utilities
|
||||
Beginner-Tutorial-Rules
|
||||
Beginner-Tutorial-Characters
|
||||
Beginner-Tutorial-Objects
|
||||
Beginner-Tutorial-Equipment
|
||||
Beginner-Tutorial-Chargen
|
||||
Beginner-Tutorial-Rooms
|
||||
Beginner-Tutorial-NPCs
|
||||
Beginner-Tutorial-Turnbased-Combat
|
||||
Beginner-Tutorial-Quests
|
||||
Beginner-Tutorial-Shops
|
||||
Beginner-Tutorial-Dungeon
|
||||
Beginner-Tutorial-Commands
|
||||
```
|
||||
1. [Changing settings](../../../Unimplemented.md)
|
||||
1. [Applying contribs](../../../Unimplemented.md)
|
||||
1. [Creating a rule module](../../../Unimplemented.md)
|
||||
1. [Tweaking the base Typeclasses](../../../Unimplemented.md)
|
||||
1. [Character creation menu](../../../Unimplemented.md)
|
||||
1. [Wearing armor and wielding weapons](../../../Unimplemented.md)
|
||||
1. [Two types of combat](../../../Unimplemented.md)
|
||||
1. [Monsters and AI](../../../Unimplemented.md)
|
||||
1. [Questing and rewards](../../../Unimplemented.md)
|
||||
1. [Overview of Tech demo](../../../Unimplemented.md)
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Implementing-a-game-rule-system
|
||||
Turn-Based-Combat-System
|
||||
A-Sittable-Object
|
||||
Beginner-Tutorial-Utilities
|
||||
Beginner-Tutorial-Rules
|
||||
Beginner-Tutorial-Characters
|
||||
Beginner-Tutorial-Objects
|
||||
Beginner-Tutorial-Equipment
|
||||
Beginner-Tutorial-Chargen
|
||||
Beginner-Tutorial-Rooms
|
||||
Beginner-Tutorial-NPCs
|
||||
Beginner-Tutorial-Turnbased-Combat
|
||||
Beginner-Tutorial-Quests
|
||||
Beginner-Tutorial-Shops
|
||||
Beginner-Tutorial-Dungeon
|
||||
Beginner-Tutorial-Commands
|
||||
```
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
# Game Quests
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# In-game Rooms
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -0,0 +1,633 @@
|
|||
# Rules and dice rolling
|
||||
|
||||
In _EvAdventure_ we have decided to use the [Knave](https://www.drivethrurpg.com/product/250888/Knave)
|
||||
RPG ruleset. This is commercial, but released under Creative Commons 4.0, meaning it's okay to share and
|
||||
adapt _Knave_ for any purpose, even commercially. If you don't want to buy it but still follow
|
||||
along, you can find a [free fan-version here](http://abominablefancy.blogspot.com/2018/10/knaves-fancypants.html).
|
||||
|
||||
## Summary of _Knave_ rules
|
||||
|
||||
Knave, being inspired by early Dungeons & Dragons, is very simple.
|
||||
|
||||
- It uses six Ability bonuses
|
||||
_Strength_ (STR), _Dexterity_ (DEX), _Constitution_ (CON), _Intelligence_ (INT), _Wisdom_ (WIS)
|
||||
and _Charisma_ (CHA). These are rated from `+1` to `+10`.
|
||||
- Rolls are made with a twenty-sided die (`1d20`), usually adding a suitable Ability bonus to the roll.
|
||||
- If you roll _with advantage_, you roll `2d20` and pick the
|
||||
_highest_ value, If you roll _with disadvantage_, you roll `2d20` and pick the _lowest_.
|
||||
- Rolling a natural `1` is a _critical failure_. A natural `20` is a _critical success_. Rolling such
|
||||
in combat means your weapon or armor loses quality, which will eventually destroy it.
|
||||
- A _saving throw_ (trying to succeed against the environment) means making a roll to beat `15` (always).
|
||||
So if you are lifting a heavy stone and have `STR +2`, you'd roll `1d20 + 2` and hope the result
|
||||
is higher than `15`.
|
||||
- An _opposed saving throw_ means beating the enemy's suitable Ability 'defense', which is always their
|
||||
`Ability bonus + 10`. So if you have `STR +1` and are arm wrestling someone with `STR +2`, you roll
|
||||
`1d20 + 1` and hope to roll higher than `2 + 10 = 12`.
|
||||
- A special bonus is `Armor`, `+1` is unarmored, additional armor is given by equipment. Melee attacks
|
||||
test `STR` versus the `Armor` defense value while ranged attacks uses `WIS` vs `Armor`.
|
||||
- _Knave_ has no skills or classes. Everyone can use all items and using magic means having a special
|
||||
'rune stone' in your hands; one spell per stone and day.
|
||||
- A character has `CON + 10` carry 'slots'. Most normal items uses one slot, armor and large weapons uses
|
||||
two or three.
|
||||
- Healing is random, `1d8 + CON` health healed after food and sleep.
|
||||
- Monster difficulty is listed by hy many 1d8 HP they have; this is called their "hit die" or HD. If
|
||||
needing to test Abilities, monsters have HD bonus in every Ability.
|
||||
- Monsters have a _morale rating_. When things go bad, they have a chance to panic and flee if
|
||||
rolling `2d6` over their morale rating.
|
||||
- All Characters in _Knave_ are mostly randomly generated. HP is `<level>d8` but we give every
|
||||
new character max HP to start.
|
||||
- _Knave_ also have random tables, such as for starting equipment and to see if dying when
|
||||
hitting 0. Death, if it happens, is permanent.
|
||||
|
||||
|
||||
## Making a rule module
|
||||
|
||||
> Create a new module mygame/evadventure/rules.py
|
||||
|
||||
```{sidebar}
|
||||
A complete version of the rule module is found in
|
||||
[evennia/contrib/tutorials/evadventure/rules.py](evennia.contrib.tutorials.evadventure.rules).
|
||||
```
|
||||
There are three broad sets of rules for most RPGS:
|
||||
|
||||
- Character generation rules, often only used during character creation
|
||||
- Regular gameplay rules - rolling dice and resolving game situations
|
||||
- Character improvement - getting and spending experience to improve the character
|
||||
|
||||
We want our `rules` module to cover as many aspeects of what we'd otherwise would have to look up
|
||||
in a rulebook.
|
||||
|
||||
|
||||
## Rolling dice
|
||||
|
||||
We will start by making a dice roller. Let's group all of our dice rolling into a structure like this
|
||||
(not functional code yet):
|
||||
|
||||
```python
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
def roll(...):
|
||||
# get result of one generic roll, for any type and number of dice
|
||||
|
||||
def roll_with_advantage_or_disadvantage(...)
|
||||
# get result of normal d20 roll, with advantage/disadvantage (or not)
|
||||
|
||||
def saving_throw(...):
|
||||
# do a saving throw against a specific target number
|
||||
|
||||
def opposed_saving_throw(...):
|
||||
# do an opposed saving throw against a target's defense
|
||||
|
||||
def roll_random_table(...):
|
||||
# make a roll against a random table (loaded elsewere)
|
||||
|
||||
def morale_check(...):
|
||||
# roll a 2d6 morale check for a target
|
||||
|
||||
def heal_from_rest(...):
|
||||
# heal 1d8 when resting+eating, but not more than max value.
|
||||
|
||||
def roll_death(...):
|
||||
# roll to determine penalty when hitting 0 HP.
|
||||
|
||||
|
||||
dice = EvAdventureRollEngine()
|
||||
|
||||
```
|
||||
```{sidebar}
|
||||
This groups all dice-related code into one 'container' that is easy to import. But it's mostly a matter
|
||||
of taste. You _could_ also break up the class' methods into normal functions at the top-level of the
|
||||
module if you wanted.
|
||||
```
|
||||
|
||||
This structure (called a _singleton_) means we group all dice rolls into one class that we then initiate
|
||||
into a variable `dice` at the end of the module. This means that we can do the following from other
|
||||
modules:
|
||||
|
||||
```python
|
||||
from .rules import dice
|
||||
|
||||
dice.roll("1d8")
|
||||
```
|
||||
|
||||
### Generic dice roller
|
||||
|
||||
We want to be able to do `roll("1d20")` and get a random result back from the roll.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
from random import randint
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
def roll(self, roll_string):
|
||||
"""
|
||||
Roll XdY dice, where X is the number of dice
|
||||
and Y the number of sides per die.
|
||||
|
||||
Args:
|
||||
roll_string (str): A dice string on the form XdY.
|
||||
Returns:
|
||||
int: The result of the roll.
|
||||
|
||||
"""
|
||||
|
||||
# split the XdY input on the 'd' one time
|
||||
number, diesize = roll_string.split("d", 1)
|
||||
|
||||
# convert from string to integers
|
||||
number = int(number)
|
||||
diesize = int(diesize)
|
||||
|
||||
# make the roll
|
||||
return sum(randint(1, diesize) for _ in range(number))
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
For this tutorial we have opted to not use any contribs, so we create
|
||||
our own dice roller. But normally you could instead use the [dice](../../../Contribs/Contrib-Dice.md) contrib for this.
|
||||
We'll point out possible helpful contribs in sidebars as we proceed.
|
||||
```
|
||||
|
||||
The `randint` standard Python library module produces a random integer
|
||||
in a specific range. The line
|
||||
|
||||
```python
|
||||
sum(randint(1, diesize) for _ in range(number))
|
||||
```
|
||||
works like this:
|
||||
|
||||
- For a certain `number` of times ...
|
||||
- ... create a random integer between `1` and `diesize` ...
|
||||
- ... and `sum` all those integers together.
|
||||
|
||||
You could write the same thing less compactly like this:
|
||||
|
||||
```python
|
||||
rolls = []
|
||||
for _ in range(number):
|
||||
random_result = randint(1, diesize)
|
||||
rolls.append(random_result)
|
||||
return sum(rolls)
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
Note that `range` generates a value `0...number-1`. We use `_` in the `for` loop to
|
||||
indicate we don't really care what this value is - we just want to repeat the loop
|
||||
a certain amount of times.
|
||||
```
|
||||
|
||||
We don't ever expect end users to call this method; if we did, we would have to validate the inputs
|
||||
much more - We would have to make sure that `number` or `diesize` are valid inputs and not
|
||||
crazy big so the loop takes forever!
|
||||
|
||||
### Rolling with advantage
|
||||
|
||||
Now that we have the generic roller, we can start using it to do a more complex roll.
|
||||
|
||||
```
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
def roll(roll_string):
|
||||
# ...
|
||||
|
||||
def roll_with_advantage_or_disadvantage(self, advantage=False, disadvantage=False):
|
||||
|
||||
if not (advantage or disadvantage) or (advantage and disadvantage):
|
||||
# normal roll - advantage/disadvantage not set or they cancel
|
||||
# each other out
|
||||
return self.roll("1d20")
|
||||
elif advantage:
|
||||
# highest of two d20 rolls
|
||||
return max(self.roll("1d20"), self.roll("1d20"))
|
||||
else:
|
||||
# disadvantage - lowest of two d20 rolls
|
||||
return min(self.roll("1d20"), self.roll("1d20"))
|
||||
```
|
||||
|
||||
The `min()` and `max()` functions are standard Python fare for getting the biggest/smallest
|
||||
of two arguments.
|
||||
|
||||
### Saving throws
|
||||
|
||||
We want the saving throw to itself figure out if it succeeded or not. This means it needs to know
|
||||
the Ability bonus (like STR `+1`). It would be convenient if we could just pass the entity
|
||||
doing the saving throw to this method, tell it what type of save was needed, and then
|
||||
have it figure things out:
|
||||
|
||||
```python
|
||||
result, quality = dice.saving_throw(character, Ability.STR)
|
||||
```
|
||||
The return will be a boolean `True/False` if they pass, as well as a `quality` that tells us if
|
||||
a perfect fail/success was rolled or not.
|
||||
|
||||
To make the saving throw method this clever, we need to think some more about how we want to store our
|
||||
data on the character.
|
||||
|
||||
For our purposes it sounds reasonable that we will be using [Attributes](../../../Components/Attributes.md) for storing
|
||||
the Ability scores. To make it easy, we will name them the same as the
|
||||
[Enum values](./Beginner-Tutorial-Utilities.md#enums) we set up in the previous lesson. So if we have
|
||||
an enum `STR = "strength"`, we want to store the Ability on the character as an Attribute `strength`.
|
||||
|
||||
From the Attribute documentation, we can see that we can use `AttributeProperty` to make it so the
|
||||
Attribute is available as `character.strength`, and this is what we will do.
|
||||
|
||||
So, in short, we'll create the saving throws method with the assumption that we will be able to do
|
||||
`character.strength`, `character.constitution`, `character.charisma` etc to get the relevant Abilities.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
# ...
|
||||
from .enums import Ability
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
def roll(...)
|
||||
# ...
|
||||
|
||||
def roll_with_advantage_or_disadvantage(...)
|
||||
# ...
|
||||
|
||||
def saving_throw(self, character, bonus_type=Ability.STR, target=15,
|
||||
advantage=False, disadvantage=False):
|
||||
"""
|
||||
Do a saving throw, trying to beat a target.
|
||||
|
||||
Args:
|
||||
character (Character): A character (assumed to have Ability bonuses
|
||||
stored on itself as Attributes).
|
||||
bonus_type (Ability): A valid Ability bonus enum.
|
||||
target (int): The target number to beat. Always 15 in Knave.
|
||||
advantage (bool): If character has advantage on this roll.
|
||||
disadvantage (bool): If character has disadvantage on this roll.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple (bool, Ability), showing if the throw succeeded and
|
||||
the quality is one of None or Ability.CRITICAL_FAILURE/SUCCESS
|
||||
|
||||
"""
|
||||
|
||||
# make a roll
|
||||
dice_roll = self.roll_with_advantage_or_disadvantage(advantage, disadvantage)
|
||||
|
||||
# figure out if we had critical failure/success
|
||||
quality = None
|
||||
if dice_roll == 1:
|
||||
quality = Ability.CRITICAL_FAILURE
|
||||
elif dice_roll == 20:
|
||||
quality = Ability.CRITICAL_SUCCESS
|
||||
|
||||
# figure out bonus
|
||||
bonus = getattr(character, bonus_type.value, 1)
|
||||
|
||||
# return a tuple (bool, quality)
|
||||
return (dice_roll + bonus) > target, quality
|
||||
```
|
||||
|
||||
The `getattr(obj, attrname, default)` function is a very useful Python tool for getting an attribute
|
||||
off an object and getting a default value if the attribute is not defined.
|
||||
|
||||
### Opposed saving throw
|
||||
|
||||
With the building pieces we already created, this method is simple. Remember that the defense you have
|
||||
to beat is always the relevant bonus + 10 in _Knave_. So if the enemy defends with `STR +3`, you must
|
||||
roll higher than `13`.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
from .enums import Ability
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
def roll(...):
|
||||
# ...
|
||||
|
||||
def roll_with_advantage_or_disadvantage(...):
|
||||
# ...
|
||||
|
||||
def saving_throw(...):
|
||||
# ...
|
||||
|
||||
def opposed_saving_throw(self, attacker, defender,
|
||||
attack_type=Ability.STR, defense_type=Ability.ARMOR,
|
||||
advantage=False, disadvantage=False):
|
||||
defender_defense = getattr(defender, defense_type.value, 1) + 10
|
||||
result, quality = self.saving_throw(attacker, bonus_type=attack_type,
|
||||
target=defender_defense,
|
||||
advantage=advantave, disadvantage=disadvantage)
|
||||
|
||||
return result, quality
|
||||
```
|
||||
|
||||
### Morale check
|
||||
|
||||
We will make the assumption that the `morale` value is available from the creature simply as
|
||||
`monster.morale` - we need to remember to make this so later!
|
||||
|
||||
In _Knave_, a creature have roll with `2d6` equal or under its morale to not flee or surrender
|
||||
when things go south. The standard morale value is 9.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def morale_check(self, defender):
|
||||
return self.roll("2d6") <= getattr(defender, "morale", 9)
|
||||
|
||||
```
|
||||
|
||||
### Roll for Healing
|
||||
|
||||
To be able to handle healing, we need to make some more assumptions about how we store
|
||||
health on game entities. We will need `hp_max` (the total amount of available HP) and `hp`
|
||||
(the current health value). We again assume these will be available as `obj.hp` and `obj.hp_max`.
|
||||
|
||||
According to the rules, after consuming a ration and having a full night's sleep, a character regains
|
||||
`1d8 + CON` HP.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
from .enums import Ability
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def heal_from_rest(self, character):
|
||||
"""
|
||||
A night's rest retains 1d8 + CON HP
|
||||
|
||||
"""
|
||||
con_bonus = getattr(character, Ability.CON.value, 1)
|
||||
character.heal(self.roll("1d8") + con_bonus)
|
||||
```
|
||||
|
||||
We make another assumption here - that `character.heal()` is a thing. We tell this function how
|
||||
much the character should heal, and it will do so, making sure to not heal more than its max
|
||||
number of HPs
|
||||
|
||||
> Knowing what is available on the character and what rule rolls we need is a bit of a chicken-and-egg
|
||||
> problem. We will make sure to implement the matching _Character_ class next lesson.
|
||||
|
||||
|
||||
### Rolling on a table
|
||||
|
||||
We occasionally need to roll on a 'table' - a selection of choices. There are two main table-types
|
||||
we need to support:
|
||||
|
||||
Simply one element per row of the table (same odds to get each result).
|
||||
|
||||
| Result |
|
||||
|:------:|
|
||||
| item1 |
|
||||
| item2 |
|
||||
| item3 |
|
||||
| item4 |
|
||||
|
||||
This we will simply represent as a plain list
|
||||
|
||||
```python
|
||||
["item1", "item2", "item3", "item4"]
|
||||
```
|
||||
|
||||
Ranges per item (varying odds per result):
|
||||
|
||||
| Range | Result |
|
||||
|:-----:|:------:|
|
||||
| 1-5 | item1 |
|
||||
| 6-15 | item2 |
|
||||
| 16-19 | item3 |
|
||||
| 20 | item4 |
|
||||
|
||||
This we will represent as a list of tuples:
|
||||
|
||||
```python
|
||||
[("1-5", "item1"), ("6-15", "item2"), ("16-19", "item4"), ("20", "item5")]
|
||||
```
|
||||
|
||||
We also need to know what die to roll to get a result on the table (it may not always
|
||||
be obvious, and in some games you could be asked to roll a lower dice to only get
|
||||
early table results, for example).
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
from random import randint, choice
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def roll_random_table(self, dieroll, table_choices):
|
||||
"""
|
||||
Args:
|
||||
dieroll (str): A die roll string, like "1d20".
|
||||
table_choices (iterable): A list of either single elements or
|
||||
of tuples.
|
||||
Returns:
|
||||
Any: A random result from the given list of choices.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If rolling dice giving results outside the table.
|
||||
|
||||
"""
|
||||
roll_result = self.roll(dieroll)
|
||||
|
||||
if isinstance(table_choices[0], (tuple, list)):
|
||||
# the first element is a tuple/list; treat as on the form [("1-5", "item"),...]
|
||||
for (valrange, choice) in table_choices:
|
||||
minval, *maxval = valrange.split("-", 1)
|
||||
minval = abs(int(minval))
|
||||
maxval = abs(int(maxval[0]) if maxval else minval)
|
||||
|
||||
if minval <= roll_result <= maxval:
|
||||
return choice
|
||||
|
||||
# if we get here we must have set a dieroll producing a value
|
||||
# outside of the table boundaries - raise error
|
||||
raise RuntimeError("roll_random_table: Invalid die roll")
|
||||
else:
|
||||
# a simple regular list
|
||||
roll_result = max(1, min(len(table_choices), roll_result))
|
||||
return table_choices[roll_result - 1]
|
||||
```
|
||||
Check that you understand what this does.
|
||||
|
||||
This may be confusing:
|
||||
```python
|
||||
minval, *maxval = valrange.split("-", 1)
|
||||
minval = abs(int(minval))
|
||||
maxval = abs(int(maxval[0]) if maxval else minval)
|
||||
```
|
||||
|
||||
If `valrange` is the string `1-5`, then `valrange.split("-", 1)` would result in a tuple `("1", "5")`.
|
||||
But if the string was in fact just `"20"` (possible for a single entry in an RPG table), this would
|
||||
lead to an error since it would only split out a single element - and we expected two.
|
||||
|
||||
By using `*maxval` (with the `*`), `maxval` is told to expect _0 or more_ elements in a tuple.
|
||||
So the result for `1-5` will be `("1", ("5",))` and for `20` it will become `("20", ())`. In the line
|
||||
|
||||
```python
|
||||
maxval = abs(int(maxval[0]) if maxval else minval)
|
||||
```
|
||||
|
||||
we check if `maxval` actually has a value `("5",)` or if its empty `()`. The result is either
|
||||
`"5"` or the value of `minval`.
|
||||
|
||||
|
||||
### Roll for death
|
||||
|
||||
While original Knave suggests hitting 0 HP means insta-death, we will grab the optional "death table"
|
||||
from the "prettified" Knave's optional rules to make it a little less punishing. We also changed the
|
||||
result of `2` to 'dead' since we don't simulate 'dismemberment' in this tutorial:
|
||||
|
||||
| Roll | Result | -1d4 Loss of Ability |
|
||||
|:---: |:--------:|:--------------------:|
|
||||
| 1-2 | dead | -
|
||||
| 3 | weakened | STR |
|
||||
|4 | unsteady | DEX |
|
||||
| 5 | sickly | CON |
|
||||
| 6 | addled | INT |
|
||||
| 7 | rattled | WIS |
|
||||
| 8 | disfigured | CHA |
|
||||
|
||||
All the non-dead values map to a loss of 1d4 in one of the six Abilities (but you get HP back).
|
||||
We need to map back to this from the above table. One also cannot have less than -10 Ability bonus,
|
||||
if you do, you die too.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
death_table = (
|
||||
("1-2", "dead"),
|
||||
("3": "strength",
|
||||
("4": "dexterity"),
|
||||
("5": "constitution"),
|
||||
("6": "intelligence"),
|
||||
("7": "wisdom"),
|
||||
("8": "charisma"),
|
||||
)
|
||||
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def roll_random_table(...)
|
||||
# ...
|
||||
|
||||
def roll_death(self, character):
|
||||
ability_name = self.roll_random_table("1d8", death_table)
|
||||
|
||||
if ability_name == "dead":
|
||||
# TODO - kill the character!
|
||||
pass
|
||||
else:
|
||||
loss = self.roll("1d4")
|
||||
|
||||
current_ability = getattr(character, ability_name)
|
||||
current_ability -= loss
|
||||
|
||||
if current_ability < -10:
|
||||
# TODO - kill the character!
|
||||
pass
|
||||
else:
|
||||
# refresh 1d4 health, but suffer 1d4 ability loss
|
||||
self.heal(character, self.roll("1d4")
|
||||
setattr(character, ability_name, current_ability)
|
||||
|
||||
character.msg(
|
||||
"You survive your brush with death, and while you recover "
|
||||
f"some health, you permanently lose {loss} {ability_name} instead."
|
||||
)
|
||||
|
||||
dice = EvAdventureRollEngine()
|
||||
```
|
||||
|
||||
Here we roll on the 'death table' from the rules to see what happens. We give the character
|
||||
a message if they survive, to let them know what happened.
|
||||
|
||||
We don't yet know what 'killing the character' technically means, so we mark this as `TODO` and
|
||||
return to it in a later lesson. We just know that we need to do _something_ here to kill off the
|
||||
character!
|
||||
|
||||
## Testing
|
||||
|
||||
> Make a new module `mygame/evadventure/tests/test_rules.py`
|
||||
|
||||
Testing the `rules` module will also showcase some very useful tools when testing.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/tests/test_rules.py
|
||||
|
||||
from unittest.mock import patch
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
from .. import rules
|
||||
|
||||
class TestEvAdventureRuleEngine(BaseEvenniaTest):
|
||||
|
||||
def setUp(self):
|
||||
"""Called before every test method"""
|
||||
super().setUp()
|
||||
self.roll_engine = rules.EvAdventureRollEngine()
|
||||
|
||||
@patch("evadventure.rules.randint")
|
||||
def test_roll(self, mock_randint):
|
||||
mock_randint.return_value = 4
|
||||
self.assertEqual(self.roll_engine.roll("1d6", 4)
|
||||
self.assertEqual(self.roll_engine.roll("2d6", 2 * 4)
|
||||
|
||||
# test of the other rule methods below ...
|
||||
```
|
||||
|
||||
As before, run the specific test with
|
||||
|
||||
evennia test --settings settings.py .evadventure.tests.test_rules
|
||||
|
||||
### Mocking and patching
|
||||
|
||||
```{sidebar}
|
||||
In [evennia/contrib/tutorials/evadventure/tests/test_rules.py](evennia.contrib.tutorials.evadventure.tests.test_rules)
|
||||
has a complete example of rule testing.
|
||||
```
|
||||
The `setUp` method is a special method of the testing class. It will be run before every
|
||||
test method. We use `super().setUp()` to make sure the parent class' version of this method
|
||||
always fire. Then we create a fresh `EvAdventureRollEngine` we can test with.
|
||||
|
||||
In our test, we import `patch` from the `unittest.mock` library. This is a very useful tool for testing.
|
||||
Normally the `randint` function we imported in `rules` will return a random value. That's very hard to
|
||||
test for, since the value will be different every test.
|
||||
|
||||
With `@patch` (this is called a _decorator_), we temporarily replace `rules.randint` with a 'mock' - a
|
||||
dummy entity. This mock is passed into the testing method. We then take this `mock_randint` and set
|
||||
`.return_value = 4` on it.
|
||||
|
||||
Adding `return_value` to the mock means that every time this mock is called, it will return 4. For the
|
||||
duration of the test we can now check with `self.assertEqual` that our `roll` method always returns a
|
||||
result as-if the random result was 4.
|
||||
|
||||
There are [many resources for understanding mock](https://realpython.com/python-mock-library/), refer to
|
||||
them for further help.
|
||||
|
||||
> The `EvAdventureRollEngine` have many methods to test. We leave this as an extra exercise!
|
||||
|
||||
## Summary
|
||||
|
||||
This concludes all the core rule mechanics of _Knave_ - the rules used during play. We noticed here
|
||||
that we are going to soon need to establish how our _Character_ actually stores data. So we will
|
||||
address that next.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# In-game Shops
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Turn-based combat
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
# Code structure and Utilities
|
||||
|
||||
In this lesson we will set up the file structure for _EvAdventure_. We will make some
|
||||
utilities that will be useful later. We will also learn how to write _tests_.
|
||||
|
||||
## Folder structure
|
||||
|
||||
Create a new folder under your `mygame` folder, named `evadventure`. Inside it, create
|
||||
another folder `tests/` and make sure to put empty `__init__.py` files in both. This turns both
|
||||
folders into packages Python understands to import from.
|
||||
|
||||
```
|
||||
mygame/
|
||||
commands/
|
||||
evadventure/ <---
|
||||
__init__.py <---
|
||||
tests/ <---
|
||||
__init__.py <---
|
||||
__init__.py
|
||||
README.md
|
||||
server/
|
||||
typeclasses/
|
||||
web/
|
||||
world/
|
||||
|
||||
```
|
||||
|
||||
Importing anything from inside this folder from anywhere else under `mygame` will be done by
|
||||
|
||||
```python
|
||||
# from anywhere in mygame/
|
||||
from evadventure.yourmodulename import whatever
|
||||
```
|
||||
|
||||
This is the 'absolute path` type of import.
|
||||
|
||||
Between two modules both in `evadventure/`, you can use a 'relative' import with `.`:
|
||||
|
||||
```python
|
||||
# from a module inside mygame/evadventure
|
||||
from .yourmodulename import whatever
|
||||
```
|
||||
|
||||
From e.g. inside `mygame/evadventure/tests/` you can import from one level above using `..`:
|
||||
|
||||
```python
|
||||
# from mygame/evadventure/tests/
|
||||
from ..yourmodulename import whatever
|
||||
```
|
||||
|
||||
|
||||
## Enums
|
||||
|
||||
```{sidebar}
|
||||
A full example of the enum module is found in
|
||||
[evennia/contrib/tutorials/evadventure/enums.py](evennia.contrib.tutorials.evadventure.enums).
|
||||
```
|
||||
Create a new file `mygame/evadventure/enums.py`.
|
||||
|
||||
An [enum](https://docs.python.org/3/library/enum.html) (enumeration) is a way to establish constants
|
||||
in Python. Best is to show an example:
|
||||
|
||||
```python
|
||||
# in a file mygame/evadventure/enums.py
|
||||
|
||||
from enum import Enum
|
||||
|
||||
class Ability(Enum):
|
||||
|
||||
STR = "strength"
|
||||
|
||||
```
|
||||
|
||||
You access an enum like this:
|
||||
|
||||
```
|
||||
# from another module in mygame/evadventure
|
||||
|
||||
from .enums import Ability
|
||||
|
||||
Ability.STR # the enum itself
|
||||
Ability.STR.value # this is the string "strength"
|
||||
|
||||
```
|
||||
|
||||
Having enums is recommended practice. With them set up, it means we can make sure to refer to the
|
||||
same thing every time. Having all enums in one place also means you have a good overview of the
|
||||
constants you are dealing with.
|
||||
|
||||
The alternative would be to for example pass around a string `"constitution"`. If you mis-spell
|
||||
this (`"consitution"`), you would not necessarily know it right away - the error would happen later
|
||||
when the string is not recognized. If you make a typo getting `Ability.COM` instead of `Ability.CON`,
|
||||
Python will immediately raise an error since this enum is not recognized.
|
||||
|
||||
With enums you can also do nice direct comparisons like `if ability is Ability.WIS: <do stuff>`.
|
||||
|
||||
Note that the `Ability.STR` enum does not have the actual _value_ of e.g. your Strength.
|
||||
It's just a fixed label for the Strength ability.
|
||||
|
||||
Here is the `enum.py` module needed for _Knave_. It covers the basic aspects of
|
||||
rule systems we need to track (check out the _Knave_ rules. If you use another rule system you'll
|
||||
likely gradually expand on your enums as you figure out what you'll need).
|
||||
|
||||
```python
|
||||
# mygame/evadventure/enums.py
|
||||
|
||||
class Ability(Enum):
|
||||
"""
|
||||
The six base ability-bonuses and other
|
||||
abilities
|
||||
|
||||
"""
|
||||
|
||||
STR = "strength"
|
||||
DEX = "dexterity"
|
||||
CON = "constitution"
|
||||
INT = "intelligence"
|
||||
WIS = "wisdom"
|
||||
CHA = "charisma"
|
||||
|
||||
ARMOR = "armor"
|
||||
|
||||
CRITICAL_FAILURE = "critical_failure"
|
||||
CRITICAL_SUCCESS = "critical_success"
|
||||
|
||||
ALLEGIANCE_HOSTILE = "hostile"
|
||||
ALLEGIANCE_NEUTRAL = "neutral"
|
||||
ALLEGIANCE_FRIENDLY = "friendly"
|
||||
|
||||
|
||||
```
|
||||
|
||||
Here the `Ability` class holds basic properties of a character sheet.
|
||||
|
||||
|
||||
## Utility module
|
||||
|
||||
> Create a new module `mygame/evadventure/utils.py`
|
||||
|
||||
```{sidebar}
|
||||
An example of the utility module is found in
|
||||
[evennia/contrib/tutorials/evadventure/utils.py](evennia.contrib.tutorials.evadventure.utils)
|
||||
```
|
||||
|
||||
This is for general functions we may need from all over. In this case we only picture one utility,
|
||||
a function that produces a pretty display of any object we pass to it.
|
||||
|
||||
This is an example of the string we want to see:
|
||||
|
||||
```
|
||||
Chipped Sword
|
||||
Value: ~10 coins [wielded in Weapon hand]
|
||||
|
||||
A simple sword used by mercenaries all over
|
||||
the world.
|
||||
|
||||
Slots: 1, Used from: weapon hand
|
||||
Quality: 3, Uses: None
|
||||
Attacks using strength against armor.
|
||||
Damage roll: 1d6
|
||||
```
|
||||
|
||||
Here's the start of how the function could look:
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/utils.py
|
||||
|
||||
_OBJ_STATS = """
|
||||
|c{key}|n
|
||||
Value: ~|y{value}|n coins{carried}
|
||||
|
||||
{desc}
|
||||
|
||||
Slots: |w{size}|n, Used from: |w{use_slot_name}|n
|
||||
Quality: |w{quality}|n, Uses: |wuses|n
|
||||
Attacks using |w{attack_type_name}|n against |w{defense_type_name}|n
|
||||
Damage roll: |w{damage_roll}|n
|
||||
""".strip()
|
||||
|
||||
|
||||
def get_obj_stats(obj, owner=None):
|
||||
"""
|
||||
Get a string of stats about the object.
|
||||
|
||||
Args:
|
||||
obj (Object): The object to get stats for.
|
||||
owner (Object): The one currently owning/carrying `obj`, if any. Can be
|
||||
used to show e.g. where they are wielding it.
|
||||
Returns:
|
||||
str: A nice info string to display about the object.
|
||||
|
||||
"""
|
||||
return _OBJ_STATS.format(
|
||||
key=obj.key,
|
||||
value=10,
|
||||
carried="[Not carried]",
|
||||
desc=obj.db.desc,
|
||||
size=1,
|
||||
quality=3,
|
||||
uses="infinite"
|
||||
use_slot_name="backpack",
|
||||
attack_type_name="strength"
|
||||
defense_type_name="armor"
|
||||
damage_roll="1d6"
|
||||
)
|
||||
```
|
||||
Here we set up the string template with place holders for where every piece of info should go.
|
||||
Study this string so you understand what it does. The `|c`, `|y`, `|w` and `|n` markers are
|
||||
[Evennia color markup](../../../Concepts/Colors.md) for making the text cyan, yellow, white and neutral-color respectively.
|
||||
|
||||
We can guess some things, such that `obj.key` is the name of the object, and that `obj.db.desc` will
|
||||
hold its description (this is how it is in default Evennia).
|
||||
|
||||
But so far we have not established how to get any of the other properties like `size` or `attack_type`.
|
||||
So we just set them to dummy values. We'll need to get back to this when we have more code in place!
|
||||
|
||||
## Testing
|
||||
|
||||
```{important}
|
||||
It's useful for any game dev to know how to effectively test their code. So we'll try to include a
|
||||
*Testing* section at the end of each of the implementation lessons to follow. Writing tests for your code
|
||||
is optional but highly recommended; it can feel a little cumbersome at first, but you'll thank yourself later.
|
||||
```
|
||||
|
||||
> create a new module `mygame/evadventure/tests/test_utils.py`
|
||||
|
||||
How do you know if you made a typo in the code above? You could _manually_ test it by reloading your
|
||||
Evennia server and do the following from in-game:
|
||||
|
||||
py from evadventure.utils import get_obj_stats;print(get_obj_stats(self))
|
||||
|
||||
You should get back a nice string about yourself! If that works, great! But you'll need to remember
|
||||
doing that test when you change this code later.
|
||||
|
||||
```{sidebar}
|
||||
In [evennia/contrib/tutorials/evadventure/tests/test_utils.py](evennia.contrib.tutorials.
|
||||
evadventure.tests.test_utils)
|
||||
is an example of the testing module. To dive deeper into unit testing in Evennia, see the
|
||||
[Unit testing](../../../Coding/Unit-Testing.md) documentation.
|
||||
```
|
||||
|
||||
A _unit test_ allows you to set up automated testing of code. Once you've written your test you
|
||||
can run it over and over and make sure later changes to your code didn't break things.
|
||||
|
||||
In this particular case, we _expect_ to later have to update the test when `get_obj_stats` becomes more
|
||||
complete and returns more reasonable data.
|
||||
|
||||
Evennia comes with extensive functionality to help you test your code. Here's a module for
|
||||
testing `get_obj_stats`.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/tests/test_utils.py
|
||||
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
||||
from ..import utils
|
||||
|
||||
class TestUtils(BaseEvenniaTest):
|
||||
def test_get_obj_stats(self):
|
||||
# make a simple object to test with
|
||||
obj = create.create_object(
|
||||
key="testobj",
|
||||
attributes=(("desc", "A test object"),)
|
||||
)
|
||||
# run it through the function
|
||||
result = utils.get_obj_stats(obj)
|
||||
# check that the result is what we expected
|
||||
self.assertEqual(
|
||||
result,
|
||||
"""
|
||||
|ctestobj|n
|
||||
Value: ~|y10|n coins
|
||||
|
||||
A test object
|
||||
|
||||
Slots: |w1|n, Used from: |wbackpack|n
|
||||
Quality: |w3|n, Uses: |winfinite|n
|
||||
Attacks using |wstrength|n against |warmor|n
|
||||
Damage roll: |w1d6|n
|
||||
""".strip()
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
What happens here is that we create a new test-class `TestUtils` that inherits from `BaseEvenniaTest`.
|
||||
This inheritance is what makes this a testing class.
|
||||
|
||||
We can have any number of methods on this class. To have a method recognized as one containing
|
||||
code to test, its name _must_ start with `test_`. We have one - `test_get_obj_stats`.
|
||||
|
||||
In this method we create a dummy `obj` and gives it a `key` "testobj". Note how we add the
|
||||
`desc` [Attribute](../../../Components/Attributes.md) directly in the `create_object` call by specifying the attribute as a
|
||||
tuple `(name, value)`!
|
||||
|
||||
We then get the result of passing this dummy-object through `get_obj_stats` we imported earlier.
|
||||
|
||||
The `assertEqual` method is available on all testing classes and checks that the `result` is equal
|
||||
to the string we specify. If they are the same, the test _passes_, otherwise it _fails_ and we
|
||||
need to investigate what went wrong.
|
||||
|
||||
### Running your test
|
||||
|
||||
To run your test you need to stand inside your `mygame` folder and execute the following command:
|
||||
|
||||
evennia test --settings settings.py .evadventure.tests
|
||||
|
||||
This will run all your `evadventure` tests (if you had more of them). To only run your utility tests
|
||||
you could do
|
||||
|
||||
evennia test --settings settings.py .evadventure.tests.test_utils
|
||||
|
||||
If all goes well, you should get an `OK` back. Otherwise you need to check the failure, maybe
|
||||
your return string doesn't quite match what you expected.
|
||||
|
||||
## Summary
|
||||
|
||||
It's very important to understand how you import code between modules in Python, so if this is still
|
||||
confusing to you, it's worth to read up on this more.
|
||||
|
||||
That said, many newcomers are confused with how to begin, so by creating the folder structure, some
|
||||
small modules and even making your first unit test, you are off to a great start!
|
||||
|
|
@ -28,7 +28,7 @@ Character [Command Set](../Components/Command-Sets.md)?
|
|||
|
||||
**A:** Go to `mygame/commands/default_cmdsets.py`. Find the `CharacterCmdSet` class. It has one
|
||||
method named `at_cmdset_creation`. At the end of that method, add the following line:
|
||||
`self.remove(default_cmds.CmdGet())`. See the [Adding Commands Tutorial](Beginner-Tutorial/Part1/Adding-Commands.md)
|
||||
`self.remove(default_cmds.CmdGet())`. See the [Adding Commands Tutorial](Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md)
|
||||
for more info.
|
||||
|
||||
## Preventing character from moving based on a condition
|
||||
|
|
@ -156,7 +156,7 @@ class CmdWerewolf(Command):
|
|||
def func(self):
|
||||
# ...
|
||||
```
|
||||
Add this to the [default cmdset as usual](Beginner-Tutorial/Part1/Adding-Commands.md). The `is_full_moon` [lock
|
||||
Add this to the [default cmdset as usual](Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md). The `is_full_moon` [lock
|
||||
function](../Components/Locks.md#lock-functions) does not yet exist. We must create that:
|
||||
|
||||
```python
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ told us that we couldn't go there.
|
|||
## Adding default error commands
|
||||
|
||||
The way to do this is to give Evennia an _alternative_ Command to use when no Exit-Command is found
|
||||
in the room. See [Adding Commands](Beginner-Tutorial/Part1/Adding-Commands.md) for more info about the
|
||||
in the room. See [Adding Commands](Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md) for more info about the
|
||||
process of adding new Commands to Evennia.
|
||||
|
||||
In this example all we'll do is echo an error message.
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ for-roleplaying-sessions) that can be of interest.
|
|||
An important aspect of making things more familiar for *Players* is adding new and tweaking existing
|
||||
commands. How this is done is covered by the [Tutorial on adding new commands](Adding-Command-
|
||||
Tutorial). You may also find it useful to shop through the `evennia/contrib/` folder. The
|
||||
[Tutorial world](Beginner-Tutorial/Part1/Tutorial-World.md) is a small single-player quest you can try (it’s not very MUSH-
|
||||
[Tutorial world](Beginner-Tutorial/Part1/Beginner-Tutorial-Tutorial-World.md) is a small single-player quest you can try (it’s not very MUSH-
|
||||
like but it does show many Evennia concepts in action). Beyond that there are [many more tutorials](./Howtos-Overview.md)
|
||||
to try out. If you feel you want a more visual overview you can also look at
|
||||
[Evennia in pictures](https://evennia.blogspot.se/2016/05/evennia-in-pictures.html).
|
||||
|
|
|
|||
|
|
@ -686,7 +686,7 @@ implemented.
|
|||
## Rooms
|
||||
|
||||
Evennia comes with rooms out of the box, so no extra work needed. A GM will automatically have all
|
||||
needed building commands available. A fuller go-through is found in the [Building tutorial](Beginner-Tutorial/Part1/Building-Quickstart.md).
|
||||
needed building commands available. A fuller go-through is found in the [Building tutorial](Beginner-Tutorial/Part1/Beginner-Tutorial-Building-Quickstart.md).
|
||||
Here are some useful highlights:
|
||||
|
||||
* `@dig roomname;alias = exit_there;alias, exit_back;alias` - this is the basic command for digging
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ Tutorial-Vehicles.md
|
|||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Tutorial-Persistent-Handler.md
|
||||
Gametime-Tutorial.md
|
||||
Help-System-Tutorial.md
|
||||
Mass-and-weight-for-objects.md
|
||||
|
|
@ -88,4 +89,18 @@ Evennia-for-roleplaying-sessions.md
|
|||
Evennia-for-Diku-Users.md
|
||||
Evennia-for-MUSH-Users.md
|
||||
Tutorial-for-basic-MUSH-like-game.md
|
||||
```
|
||||
```
|
||||
|
||||
## Old tutorials
|
||||
|
||||
These will be replaced by the Beginner Tutorial, but remain here until that is complete.
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Implementing-a-game-rule-system.md
|
||||
Turn-based-Combat-System.md
|
||||
A-Sittable-Object.md
|
||||
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -45,12 +45,12 @@ makes it easier to change and update things in one place later.
|
|||
values for Health, a list of skills etc, store those things on the Character - don't store how to
|
||||
roll or change them.
|
||||
- Next is to determine just how you want to store things on your Objects and Characters. You can
|
||||
choose to either store things as individual [Attributes](../../../Components/Attributes.md), like `character.db.STR=34` and
|
||||
choose to either store things as individual [Attributes](../Components/Attributes.md), like `character.db.STR=34` and
|
||||
`character.db.Hunting_skill=20`. But you could also use some custom storage method, like a
|
||||
dictionary `character.db.skills = {"Hunting":34, "Fishing":20, ...}`. A much more fancy solution is
|
||||
to look at the Ainneve [Trait
|
||||
handler](https://github.com/evennia/ainneve/blob/master/world/traits.py). Finally you could even go
|
||||
with a [custom django model](../../../Concepts/New-Models.md). Which is the better depends on your game and the
|
||||
with a [custom django model](../Concepts/New-Models.md). Which is the better depends on your game and the
|
||||
complexity of your system.
|
||||
- Make a clear [API](https://en.wikipedia.org/wiki/Application_programming_interface) into your
|
||||
rules. That is, make methods/functions that you feed with, say, your Character and which skill you
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
|
||||
This tutorial will elaborate on the many ways one can parse command arguments. The first step after
|
||||
[adding a command](Beginner-Tutorial/Part1/Adding-Commands.md) usually is to parse its arguments. There are lots of
|
||||
[adding a command](Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md) usually is to parse its arguments. There are lots of
|
||||
ways to do it, but some are indeed better than others and this tutorial will try to present them.
|
||||
|
||||
If you're a Python beginner, this tutorial might help you a lot. If you're already familiar with
|
||||
|
|
@ -652,7 +652,7 @@ about... what is this `"book"`?
|
|||
|
||||
To get an object from a string, we perform an Evennia search. Evennia provides a `search` method on
|
||||
all typeclassed objects (you will most likely use the one on characters or accounts). This method
|
||||
supports a very wide array of arguments and has [its own tutorial](Beginner-Tutorial/Part1/Searching-Things.md).
|
||||
supports a very wide array of arguments and has [its own tutorial](Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.md).
|
||||
Some examples of useful cases follow:
|
||||
|
||||
### Local searches
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ In this section we will try to create an actual "map" object that an account can
|
|||
at.
|
||||
|
||||
Evennia offers a range of [default commands](../Components/Default-Commands.md) for
|
||||
[creating objects and rooms in-game](Beginner-Tutorial/Part1/Building-Quickstart.md). While readily accessible, these commands are made to do very
|
||||
[creating objects and rooms in-game](Beginner-Tutorial/Part1/Beginner-Tutorial-Building-Quickstart.md). While readily accessible, these commands are made to do very
|
||||
specific, restricted things and will thus not offer as much flexibility to experiment (for an
|
||||
advanced exception see [the FuncParser](../Components/FuncParser.md)). Additionally, entering long
|
||||
descriptions and properties over and over in the game client can become tedious; especially when
|
||||
|
|
@ -413,4 +413,4 @@ easily new game defining features can be added to Evennia.
|
|||
You can easily build from this tutorial by expanding the map and creating more rooms to explore. Why
|
||||
not add more features to your game by trying other tutorials: [Add weather to your world](Weather-
|
||||
Tutorial), [fill your world with NPC's](./Tutorial-Aggressive-NPCs.md) or
|
||||
[implement a combat system](Beginner-Tutorial/Part3/Turn-based-Combat-System.md).
|
||||
[implement a combat system](./Turn-based-Combat-System.md).
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ allows for emoting as part of combat which is an advantage for roleplay-heavy ga
|
|||
To implement a freeform combat system all you need is a dice roller and a roleplaying rulebook. See
|
||||
[contrib/dice.py](https://github.com/evennia/evennia/blob/master/evennia/contrib/dice.py) for an
|
||||
example dice roller. To implement at twitch-based system you basically need a few combat
|
||||
[commands](../../../Components/Commands.md), possibly ones with a [cooldown](../../Command-Cooldown.md). You also need a [game rule
|
||||
[commands](../Components/Commands.md), possibly ones with a [cooldown](./Command-Cooldown.md). You also need a [game rule
|
||||
module](./Implementing-a-game-rule-system.md) that makes use of it. We will focus on the turn-based
|
||||
variety here.
|
||||
|
||||
|
|
@ -61,22 +61,22 @@ reported. A new turn then begins.
|
|||
|
||||
For creating the combat system we will need the following components:
|
||||
|
||||
- A combat handler. This is the main mechanic of the system. This is a [Script](../../../Components/Scripts.md) object
|
||||
- A combat handler. This is the main mechanic of the system. This is a [Script](../Components/Scripts.md) object
|
||||
created for each combat. It is not assigned to a specific object but is shared by the combating
|
||||
characters and handles all the combat information. Since Scripts are database entities it also means
|
||||
that the combat will not be affected by a server reload.
|
||||
- A combat [command set](../../../Components/Command-Sets.md) with the relevant commands needed for combat, such as the
|
||||
- A combat [command set](../Components/Command-Sets.md) with the relevant commands needed for combat, such as the
|
||||
various attack/defend options and the `flee/disengage` command to leave the combat mode.
|
||||
- A rule resolution system. The basics of making such a module is described in the [rule system
|
||||
tutorial](./Implementing-a-game-rule-system.md). We will only sketch such a module here for our end-turn
|
||||
combat resolution.
|
||||
- An `attack` [command](../../../Components/Commands.md) for initiating the combat mode. This is added to the default
|
||||
- An `attack` [command](../Components/Commands.md) for initiating the combat mode. This is added to the default
|
||||
command set. It will create the combat handler and add the character(s) to it. It will also assign
|
||||
the combat command set to the characters.
|
||||
|
||||
## The combat handler
|
||||
|
||||
The _combat handler_ is implemented as a stand-alone [Script](../../../Components/Scripts.md). This Script is created when
|
||||
The _combat handler_ is implemented as a stand-alone [Script](../Components/Scripts.md). This Script is created when
|
||||
the first Character decides to attack another and is deleted when no one is fighting any more. Each
|
||||
handler represents one instance of combat and one combat only. Each instance of combat can hold any
|
||||
number of characters but each character can only be part of one combat at a time (a player would
|
||||
|
|
@ -89,7 +89,7 @@ don't use this very much here this might allow the combat commands on the charac
|
|||
update the combat handler state directly.
|
||||
|
||||
_Note: Another way to implement a combat handler would be to use a normal Python object and handle
|
||||
time-keeping with the [TickerHandler](../../../Components/TickerHandler.md). This would require either adding custom hook
|
||||
time-keeping with the [TickerHandler](../Components/TickerHandler.md). This would require either adding custom hook
|
||||
methods on the character or to implement a custom child of the TickerHandler class to track turns.
|
||||
Whereas the TickerHandler is easy to use, a Script offers more power in this case._
|
||||
|
||||
|
|
@ -507,7 +507,7 @@ class CmdAttack(Command):
|
|||
```
|
||||
|
||||
The `attack` command will not go into the combat cmdset but rather into the default cmdset. See e.g.
|
||||
the [Adding Command Tutorial](../Part1/Adding-Commands.md) if you are unsure about how to do this.
|
||||
the [Adding Command Tutorial](Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md) if you are unsure about how to do this.
|
||||
|
||||
## Expanding the example
|
||||
|
||||
235
docs/source/Howtos/Tutorial-Persistent-Handler.md
Normal file
235
docs/source/Howtos/Tutorial-Persistent-Handler.md
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
# Making a Persistent object Handler
|
||||
|
||||
A _handler_ is a convenient way to group functionality on an object. This allows you to logically
|
||||
group all actions related to that thing in one place. This tutorial expemplifies how to make your
|
||||
own handlers and make sure data you store in them survives a reload.
|
||||
|
||||
For example, when you do `obj.attributes.get("key")` or `obj.tags.add('tagname')` you are evoking
|
||||
handlers stored as `.attributes` and `tags` on the `obj`. On these handlers are methods (`get()`
|
||||
and `add()` in this example).
|
||||
|
||||
## Base Handler example
|
||||
|
||||
Here is a base way to set up an on-object handler:
|
||||
|
||||
```python
|
||||
|
||||
from evennia import DefaultObject, create_object
|
||||
from evennia.utils.utils import lazy_property
|
||||
|
||||
class NameChanger:
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
|
||||
def add_to_key(self, suffix):
|
||||
self.obj.key = f"self.obj.key_{suffix}"
|
||||
|
||||
# make a test object
|
||||
class MyObject(DefaultObject):
|
||||
@lazy_property:
|
||||
def namechange(self):
|
||||
return NameChanger(self)
|
||||
|
||||
|
||||
obj = create_object(MyObject, key="test")
|
||||
print(obj.key)
|
||||
>>> "test"
|
||||
obj.namechange.add_to_key("extra")
|
||||
print(obj.key)
|
||||
>>> "test_extra"
|
||||
```
|
||||
|
||||
What happens here is that we make a new class `NameChanger`. We use the
|
||||
`@lazy_property` decorator to set it up - this means the handler will not be
|
||||
actually created until someone really wants to use it, by accessing
|
||||
`obj.namechange` later. The decorated `namechange` method returns the handler
|
||||
and makes sure to initialize it with `self` - this becomes the `obj` inside the
|
||||
handler!
|
||||
|
||||
We then make a silly method `add_to_key` that uses the handler to manipulate the
|
||||
key of the object. In this example, the handler is pretty pointless, but
|
||||
grouping functionality this way can both make for an easy-to-remember API and
|
||||
can also allow you cache data for easy access - this is how the
|
||||
`AttributeHandler` (`.attributes`) and `TagHandler` (`.tags`) works.
|
||||
|
||||
## Persistent storage of data in handler
|
||||
|
||||
Let's say we want to track 'quests' in our handler. A 'quest' is a regular class
|
||||
that represents the quest. Let's make it simple as an example:
|
||||
|
||||
```python
|
||||
# for example in mygame/world/quests.py
|
||||
|
||||
|
||||
class Quest:
|
||||
|
||||
key = "The quest for the red key"
|
||||
|
||||
def __init__(self):
|
||||
self.current_step = "start"
|
||||
|
||||
def check_progress(self):
|
||||
# uses self.current_step to check
|
||||
# progress of this quest
|
||||
getattr(self, f"step_{self.current_step}")()
|
||||
|
||||
def step_start(self):
|
||||
# check here if quest-step is complete
|
||||
self.current_step = "find_the_red_key"
|
||||
def step_find_the_red_key(self):
|
||||
# check if step is complete
|
||||
self.current_step = "hand_in_quest"
|
||||
def step_hand_in_quest(self):
|
||||
# check if handed in quest to quest giver
|
||||
self.current_step = None # finished
|
||||
|
||||
```
|
||||
|
||||
We expect the dev to make subclasses of this to implement different quests. Exactly how this works
|
||||
doesn't matter, the key is that we want to track `self.current_step` - a property that _should
|
||||
survive a server reload_. But so far there is no way for `Quest` to accomplish this, it's just a
|
||||
normal Python class with no connection to the database.
|
||||
|
||||
### Handler with save/load capability
|
||||
|
||||
Let's make a `QuestHandler` that manages a character's quests.
|
||||
|
||||
```python
|
||||
# for example in the same mygame/world/quests.py
|
||||
|
||||
|
||||
class QuestHandler:
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
self.do_save = False
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
self.storage = self.obj.attributes.get(
|
||||
"quest_storage", default={}, category="quests")
|
||||
|
||||
def _save(self):
|
||||
self.obj.attributes.add(
|
||||
"quest_storage", self.storage, category="quests")
|
||||
self._load() # important
|
||||
self.do_save = False
|
||||
|
||||
def add(self, questclass):
|
||||
self.storage[questclass.key] = questclass(self.obj)
|
||||
self._save()
|
||||
|
||||
def check_progress(self):
|
||||
quest.check_progress()
|
||||
if self.do_save:
|
||||
# .do_save is set on handler by Quest if it wants to save progress
|
||||
self._save()
|
||||
|
||||
```
|
||||
|
||||
The handler is just a normal Python class and has no database-storage on its own. But it has a link
|
||||
to `.obj`, which is assumed to be a full typeclased entity, on which we can create
|
||||
persistent [Attributes](../Components/Attributes.md) to store things however we like!
|
||||
|
||||
We make two helper methods `_load` and
|
||||
`_save` that handles local fetches and saves `storage` to an Attribute on the object. To avoid
|
||||
saving more than necessary, we have a property `do_save`. This we will set in `Quest` below.
|
||||
|
||||
> Note that once we `_save` the data, we need to call `_load` again. This is to make sure the version we store on the handler is properly de-serialized. If you get an error about data being `bytes`, you probably missed this step.
|
||||
|
||||
|
||||
### Make quests storable
|
||||
|
||||
The handler will save all `Quest` objects as a `dict` in an Attribute on `obj`. We are not done yet
|
||||
though, the `Quest` object needs access to the `obj` too - not only will this is important to figure
|
||||
out if the quest is complete (the `Quest` must be able to check the quester's inventory to see if
|
||||
they have the red key, for example), it also allows the `Quest` to tell the handler when its state
|
||||
changed and it should be saved.
|
||||
|
||||
We change the `Quest` such:
|
||||
|
||||
```python
|
||||
from evennia.utils import dbserialize
|
||||
|
||||
|
||||
class Quest:
|
||||
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
self._current_step = "start"
|
||||
|
||||
def __serialize_dbobjs__(self):
|
||||
self.obj = dbserialize.dbserialize(self.obj)
|
||||
|
||||
def __deserialize_dbobjs__(self):
|
||||
if isinstance(self.obj, bytes):
|
||||
self.obj = dbserialize.dbunserialize(self.obj)
|
||||
|
||||
@property
|
||||
def questhandler(self):
|
||||
return self.obj.quests
|
||||
|
||||
@property
|
||||
def current_step(self):
|
||||
return self._current_step
|
||||
|
||||
@current_step.setter
|
||||
def current_step(self, value):
|
||||
self._current_step = value
|
||||
self.questhandler.do_save = True # this triggers save in handler!
|
||||
|
||||
# [same as before]
|
||||
|
||||
```
|
||||
|
||||
The `Quest.__init__` now takes `obj` as argument, to match what we pass to it in
|
||||
`QuestHandler.add`. We want to monitor the changing of `current_step`, so we
|
||||
make it into a `property`. When we edit that value, we set the `do_save` flag on
|
||||
the handler, which means it will save the status to database once it has checked
|
||||
progress on all its quests. The `Quest.questhandler` property allows to easily
|
||||
get back to the handler (and the object on which it sits).
|
||||
|
||||
The `__serialize__dbobjs__` and `__deserialize_dbobjs__` methods are needed
|
||||
because `Attributes` can't store 'hidden' database objects (the `Quest.obj`
|
||||
property. The methods help Evennia serialize/deserialize `Quest` propertly when
|
||||
the handler saves it. For more information, see [Storing Single
|
||||
objects](../Components/Attributes.md#storing-single-objects) in the Attributes
|
||||
|
||||
### Tying it all together
|
||||
|
||||
The final thing we need to do is to add the quest-handler to the character:
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/characters.py
|
||||
|
||||
from evennia import DefaultCharacter
|
||||
from evennia.utils.utils import lazy_property
|
||||
from .world.quests import QuestHandler # as an example
|
||||
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
# ...
|
||||
@lazy_property
|
||||
def quests(self):
|
||||
return QuestHandler(self)
|
||||
|
||||
```
|
||||
|
||||
|
||||
You can now make your Quest classes to describe your quests and add them to
|
||||
characters with
|
||||
|
||||
```python
|
||||
character.quests.add(FindTheRedKey)
|
||||
```
|
||||
|
||||
and can later do
|
||||
|
||||
```python
|
||||
character.quests.check_progress()
|
||||
```
|
||||
|
||||
and be sure that quest data is not lost between reloads.
|
||||
|
||||
You can find a full-fledged quest-handler example as [EvAdventure
|
||||
quests](evennia.contrib.tutorials.evadventure.quests) contrib in the Evennia
|
||||
repository.
|
||||
|
|
@ -660,6 +660,6 @@ The simple "Power" game mechanic should be easily expandable to something more f
|
|||
useful, same is true for the combat score principle. The `+attack` could be made to target a
|
||||
specific player (or npc) and automatically compare their relevant attributes to determine a result.
|
||||
|
||||
To continue from here, you can take a look at the [Tutorial World](Beginner-Tutorial/Part1/Tutorial-World.md). For
|
||||
To continue from here, you can take a look at the [Tutorial World](Beginner-Tutorial/Part1/Beginner-Tutorial-Tutorial-World.md). For
|
||||
more specific ideas, see the [other tutorials and hints](./Howtos-Overview.md) as well
|
||||
as the [Evennia Component overview](../Components/Components-Overview.md).
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ pip install python-twitter
|
|||
|
||||
Evennia doesn't have a `tweet` command out of the box so you need to write your own little
|
||||
[Command](../Components/Commands.md) in order to tweet. If you are unsure about how commands work and how to add
|
||||
them, it can be an idea to go through the [Adding a Command Tutorial](../Howtos/Beginner-Tutorial/Part1/Adding-Commands.md)
|
||||
them, it can be an idea to go through the [Adding a Command Tutorial](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md)
|
||||
before continuing.
|
||||
|
||||
You can create the command in a separate command module (something like `mygame/commands/tweet.py`)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
# Upgrading an existing installation
|
||||
|
||||
## Evennia v0.9.5 to 1.0
|
||||
## Evennia v0.9.5 to 1.0
|
||||
|
||||
Prior to 1.0, all Evennia installs were [Git-installs](./Installation-Git.md). These instructions
|
||||
assume that you have a cloned `evennia` repo and use a virtualenv (best practices).
|
||||
|
||||
- Make sure to stop Evennia 0.9.5 entirely with `evennia stop`.
|
||||
- `deactivate` to leave your active virtualenv.
|
||||
- Make a _backup_ of your entire `mygame` folder, just to be sure!
|
||||
- Make a _backup_ of your entire `mygame` folder, just to be sure!
|
||||
- Delete the old `evenv` folder, or rename it (in case you want to keep using 0.9.5 for a while).
|
||||
- Install Python 3.10 (recommended) or 3.9. Follow the [Git-installation](./Installation-Git.md) for your OS if needed.
|
||||
- If using virtualenv, make a _new_ one with `python3.10 -m venv evenv`, then activate with `source evenv/bin/activate`
|
||||
|
|
@ -15,28 +15,31 @@ assume that you have a cloned `evennia` repo and use a virtualenv (best practice
|
|||
- `cd` into your `evennia/` folder (you want to see the `docs/`, `bin/` directories as well as a nested `evennia/` folder)
|
||||
- **Prior to 1.0 release only** - do `git checkout develop` to switch to the develop branch. After release, this will
|
||||
be found on the default master branch.
|
||||
- `git pull`
|
||||
- `git pull`
|
||||
- `pip install -e .`
|
||||
- If you want the optional extra libs, do `pip install -r requirements_extra.txt`.
|
||||
- Test that you can run the `evennia` command.
|
||||
|
||||
If you don't have anything you want to keep in your existing game dir, you can just start a new onew
|
||||
using the normal [install instructions](./Installation.md). If you want to keep/convert your existing
|
||||
If you don't have anything you want to keep in your existing game dir, you can just start a new onew
|
||||
using the normal [install instructions](./Installation.md). If you want to keep/convert your existing
|
||||
game dir, continue below.
|
||||
|
||||
- First, make a backup of your exising game dir! If you use version control, make sure to commit your current state.
|
||||
- `cd` to your existing 0.9.5-based game folder (like `mygame`.)
|
||||
- If you have changed `mygame/web`, _rename_ the folder to `web_0.9.5`. If you didn't change anything (or don't have
|
||||
- If you have changed `mygame/web`, _rename_ the folder to `web_0.9.5`. If you didn't change anything (or don't have
|
||||
anything you want to keep), you can _delete_ it entirely.
|
||||
- Copy `evennia/evennia/game_template/web` to `mygame/` (e.g. using `cp -Rf` or a file manager). This new `web` folder
|
||||
replaces the old one and has a very different structure.
|
||||
- `evennia migrate`
|
||||
- `evennia start`
|
||||
- It's possible you need to replace/comment out import and calls to the deprecated
|
||||
[`django.conf.urls`](https://docs.djangoproject.com/en/3.2/ref/urls/#url). The new way to call it is
|
||||
[available here](https://docs.djangoproject.com/en/4.0/ref/urls/#django.urls.re_path).
|
||||
- Run `evennia migrate`
|
||||
- Run `evennia start`
|
||||
|
||||
If you made extensive work in your game dir, you may well find that you need to do some (hopefully minor)
|
||||
changes to your code before it will start with Evennia 1.0. Some important points:
|
||||
If you made extensive work in your game dir, you may well find that you need to do some (hopefully minor)
|
||||
changes to your code before it will start with Evennia 1.0. Some important points:
|
||||
|
||||
- The `evennia/contrib/` folder changed structure - there are now categorized sub-folders, so you have to update
|
||||
- The `evennia/contrib/` folder changed structure - there are now categorized sub-folders, so you have to update
|
||||
your imports.
|
||||
- Any `web` changes need to be moved back from your backup into the new structure of `web/` manually.
|
||||
- See the [Evennia 1.0 Changelog](../Coding/Changelog.md) for all changes.
|
||||
- See the [Evennia 1.0 Changelog](../Coding/Changelog.md) for all changes.
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ beforehand to make sure you don't pick a game name that is already taken - be ni
|
|||
|
||||
You are good to go!
|
||||
|
||||
Evennia comes with a small [Tutorial World](../Howtos/Beginner-Tutorial/Part1/Tutorial-World.md) to experiment and learn from. After logging
|
||||
Evennia comes with a small [Tutorial World](../Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Tutorial-World.md) to experiment and learn from. After logging
|
||||
in, you can create it by running
|
||||
|
||||
batchcommand tutorial_world.build
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ WEBSOCKET_CLIENT_PORT = 4002
|
|||
WEBSERVER_PORTS = [(4001, 4005)]
|
||||
AMP_PORT = 4006
|
||||
|
||||
# This needs to be set to your website address for django or you'll receive a
|
||||
# CSRF error when trying to log on to the web portal
|
||||
CSRF_TRUSTED_ORIGINS = ['https://mymudgame.com']
|
||||
|
||||
# Optional - security measures limiting interface access
|
||||
# (don't set these before you know things work without them)
|
||||
TELNET_INTERFACES = ['203.0.113.0']
|
||||
|
|
|
|||
|
|
@ -31,12 +31,12 @@ value - which may change as Evennia is developed. This way you can
|
|||
always be sure of what you have changed and what is default behaviour.
|
||||
|
||||
"""
|
||||
from django.contrib.messages import constants as messages
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from django.contrib.messages import constants as messages
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
######################################################################
|
||||
# Evennia base server config
|
||||
######################################################################
|
||||
|
|
@ -268,7 +268,7 @@ EXTRA_LAUNCHER_COMMANDS = {}
|
|||
MAX_CHAR_LIMIT = 6000
|
||||
# The warning to echo back to users if they enter a very large string
|
||||
MAX_CHAR_LIMIT_WARNING = (
|
||||
"You entered a string that was too long. " "Please break it up into multiple parts."
|
||||
"You entered a string that was too long. Please break it up into multiple parts."
|
||||
)
|
||||
# If this is true, errors and tracebacks from the engine will be
|
||||
# echoed as text in-game as well as to the log. This can speed up
|
||||
|
|
@ -405,9 +405,11 @@ INITIAL_SETUP_MODULE = "evennia.server.initial_setup"
|
|||
# the server's initial setup sequence (the very first startup of the system).
|
||||
# The check will fail quietly if module doesn't exist or fails to load.
|
||||
AT_INITIAL_SETUP_HOOK_MODULE = "server.conf.at_initial_setup"
|
||||
# Module containing your custom at_server_start(), at_server_reload() and
|
||||
# at_server_stop() methods. These methods will be called every time
|
||||
# the server starts, reloads and resets/stops respectively.
|
||||
# Module(s) containing custom at_server_init(), at_server_start(),
|
||||
# at_server_reload() and at_server_stop() methods. These methods will be called
|
||||
# every time the server starts, reloads and resets/stops
|
||||
# respectively. Can be given as a single path or a list of paths. If a list,
|
||||
# each module's hooks will be called in list order.
|
||||
AT_SERVER_STARTSTOP_MODULE = "server.conf.at_server_startstop"
|
||||
# List of one or more module paths to modules containing a function start_
|
||||
# plugin_services(application). This module will be called with the main
|
||||
|
|
@ -555,8 +557,6 @@ BASE_SCRIPT_TYPECLASS = "typeclasses.scripts.Script"
|
|||
# is Limbo (#2).
|
||||
DEFAULT_HOME = "#2"
|
||||
# The start position for new characters. Default is Limbo (#2).
|
||||
# MULTISESSION_MODE = 0, 1 - used by default unloggedin create command
|
||||
# MULTISESSION_MODE = 2, 3 - used by default character_create command
|
||||
START_LOCATION = "#2"
|
||||
# Lookups of Attributes, Tags, Nicks, Aliases can be aggressively
|
||||
# cached to avoid repeated database hits. This often gives noticeable
|
||||
|
|
@ -726,21 +726,31 @@ GLOBAL_SCRIPTS = {
|
|||
######################################################################
|
||||
|
||||
# Different Multisession modes allow a player (=account) to connect to the
|
||||
# game simultaneously with multiple clients (=sessions). In modes 0,1 there is
|
||||
# only one character created to the same name as the account at first login.
|
||||
# In modes 2,3 no default character will be created and the MAX_NR_CHARACTERS
|
||||
# value (below) defines how many characters the default char_create command
|
||||
# allow per account.
|
||||
# 0 - single session, one account, one character, when a new session is
|
||||
# connected, the old one is disconnected
|
||||
# 1 - multiple sessions, one account, one character, each session getting
|
||||
# the same data
|
||||
# 2 - multiple sessions, one account, many characters, one session per
|
||||
# character (disconnects multiplets)
|
||||
# 3 - like mode 2, except multiple sessions can puppet one character, each
|
||||
# game simultaneously with multiple clients (=sessions).
|
||||
# 0 - single session per account (if reconnecting, disconnect old session)
|
||||
# 1 - multiple sessions per account, all sessions share output
|
||||
# 2 - multiple sessions per account, one session allowed per puppet
|
||||
# 3 - multiple sessions per account, multiple sessions per puppet (share output)
|
||||
# session getting the same data.
|
||||
MULTISESSION_MODE = 0
|
||||
# The maximum number of characters allowed by the default ooc char-creation command
|
||||
# Whether we should create a character with the same name as the account when
|
||||
# a new account is created. Together with AUTO_PUPPET_ON_LOGIN, this mimics
|
||||
# a legacy MUD, where there is no difference between account and character.
|
||||
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = True
|
||||
# Whether an account should auto-puppet the last puppeted puppet when logging in. This
|
||||
# will only work if the session/puppet combination can be determined (usually
|
||||
# MULTISESSION_MODE 0 or 1), otherwise, the player will end up OOC. Use
|
||||
# MULTISESSION_MODE=0, AUTO_CREATE_CHARACTER_WITH_ACCOUNT=True and this value to
|
||||
# mimic a legacy mud with minimal difference between Account and Character. Disable
|
||||
# this and AUTO_PUPPET to get a chargen/character select screen on login.
|
||||
AUTO_PUPPET_ON_LOGIN = True
|
||||
# How many *different* characters an account can puppet *at the same time*. A value
|
||||
# above 1 only makes a difference together with MULTISESSION_MODE > 1.
|
||||
MAX_NR_SIMULTANEOUS_PUPPETS = 1
|
||||
# The maximum number of characters allowed by be created by the default ooc
|
||||
# char-creation command. This can be seen as how big of a 'stable' of characters
|
||||
# an account can have (not how many you can puppet at the same time). Set to
|
||||
# None for no limit.
|
||||
MAX_NR_CHARACTERS = 1
|
||||
# The access hierarchy, in climbing order. A higher permission in the
|
||||
# hierarchy includes access of all levels below it. Used by the perm()/pperm()
|
||||
|
|
|
|||
|
|
@ -48,9 +48,7 @@ div.sphinxsidebarwrapper {
|
|||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
float: left;
|
||||
width: 230px;
|
||||
margin-left: -100%;
|
||||
width: 21%;
|
||||
font-size: 90%;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap : break-word;
|
||||
|
|
@ -72,7 +70,7 @@ div.sphinxsidebar ul ul {
|
|||
}
|
||||
|
||||
div.sphinxsidebar form {
|
||||
margin-top: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input {
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
|
|
@ -23,16 +23,16 @@ body {
|
|||
color: #555;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height:88
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
float: left;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0 0 0 230px;
|
||||
width: 79%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
hr {
|
||||
|
|
@ -435,7 +435,9 @@ code.descname {
|
|||
@media print, screen and (max-width: 960px) {
|
||||
|
||||
div.body {
|
||||
width: auto;
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
padding: 0 30px 30px 30px;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
|
|
@ -446,28 +448,16 @@ code.descname {
|
|||
div.document,
|
||||
div.documentwrapper,
|
||||
div.bodywrapper {
|
||||
margin: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
margin: 0 !important;
|
||||
|
||||
div.sphinxsidebar {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.search {
|
||||
visibility: visible;
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
right: 200px;
|
||||
}
|
||||
|
||||
#top-link {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media print, screen and (max-width: 720px) {
|
||||
|
||||
div.related>ul {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
|
@ -476,16 +466,25 @@ code.descname {
|
|||
visibility: visible;
|
||||
}
|
||||
|
||||
.search {
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media print, screen and (max-width: 480px) {
|
||||
|
||||
div.body {
|
||||
padding-left: 2px;
|
||||
box-sizing: border-box;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/*
|
||||
* At screen sizes this small, the sidebar stacks on top
|
||||
* of the main content, so they both are 100% width.
|
||||
*/
|
||||
div.sphinxsidebar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.document,
|
||||
div.documentwrapper,
|
||||
div.bodywrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,13 +179,14 @@
|
|||
|
||||
<div class="document">
|
||||
{%- block document %}
|
||||
|
||||
<div class="documentwrapper">
|
||||
{%- if render_sidebar %}
|
||||
{%- block sidebar2 %}{{ sidebar() }}{% endblock %}
|
||||
<div class="bodywrapper">
|
||||
{%- endif %}
|
||||
<div class="body" role="main">
|
||||
{% block body %} {% endblock %}
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
{%- if render_sidebar %}
|
||||
</div>
|
||||
|
|
@ -193,8 +194,6 @@
|
|||
</div>
|
||||
{%- endblock %}
|
||||
|
||||
{%- block sidebar2 %}{{ sidebar() }}{% endblock %}
|
||||
<div class="clearer"></div>
|
||||
</div>
|
||||
{%- endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@
|
|||
:license: BSD, see LICENSE for details.
|
||||
#}
|
||||
{%- if display_toc %}
|
||||
<p><h3><a href="{{ pathto(master_doc) }}">{{ _('Table of Contents') }}</a></h3>
|
||||
<h3><a href="{{ pathto(master_doc) }}">{{ _('Table of Contents') }}</a></h3>
|
||||
{{ toc }}
|
||||
{%- endif %}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.grid.ingame\_map\_display.ingame\_map\_display
|
||||
=====================================================================
|
||||
|
||||
.. automodule:: evennia.contrib.grid.ingame_map_display.ingame_map_display
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
18
docs/source/api/evennia.contrib.grid.ingame_map_display.md
Normal file
18
docs/source/api/evennia.contrib.grid.ingame_map_display.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.grid.ingame\_map\_display
|
||||
=================================================
|
||||
|
||||
.. automodule:: evennia.contrib.grid.ingame_map_display
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 6
|
||||
|
||||
evennia.contrib.grid.ingame_map_display.ingame_map_display
|
||||
evennia.contrib.grid.ingame_map_display.tests
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.grid.ingame\_map\_display.tests
|
||||
======================================================
|
||||
|
||||
.. automodule:: evennia.contrib.grid.ingame_map_display.tests
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -12,6 +12,7 @@ evennia.contrib.grid
|
|||
:maxdepth: 6
|
||||
|
||||
evennia.contrib.grid.extended_room
|
||||
evennia.contrib.grid.ingame_map_display
|
||||
evennia.contrib.grid.mapbuilder
|
||||
evennia.contrib.grid.simpledoor
|
||||
evennia.contrib.grid.slow_exit
|
||||
|
|
|
|||
10
docs/source/api/evennia.contrib.rpg.buffs.buff.md
Normal file
10
docs/source/api/evennia.contrib.rpg.buffs.buff.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.rpg.buffs.buff
|
||||
=====================================
|
||||
|
||||
.. automodule:: evennia.contrib.rpg.buffs.buff
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
19
docs/source/api/evennia.contrib.rpg.buffs.md
Normal file
19
docs/source/api/evennia.contrib.rpg.buffs.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.rpg.buffs
|
||||
=================================
|
||||
|
||||
.. automodule:: evennia.contrib.rpg.buffs
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 6
|
||||
|
||||
evennia.contrib.rpg.buffs.buff
|
||||
evennia.contrib.rpg.buffs.samplebuffs
|
||||
evennia.contrib.rpg.buffs.tests
|
||||
|
||||
```
|
||||
10
docs/source/api/evennia.contrib.rpg.buffs.samplebuffs.md
Normal file
10
docs/source/api/evennia.contrib.rpg.buffs.samplebuffs.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.rpg.buffs.samplebuffs
|
||||
============================================
|
||||
|
||||
.. automodule:: evennia.contrib.rpg.buffs.samplebuffs
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
10
docs/source/api/evennia.contrib.rpg.buffs.tests.md
Normal file
10
docs/source/api/evennia.contrib.rpg.buffs.tests.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.rpg.buffs.tests
|
||||
======================================
|
||||
|
||||
.. automodule:: evennia.contrib.rpg.buffs.tests
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -11,6 +11,7 @@ evennia.contrib.rpg
|
|||
.. toctree::
|
||||
:maxdepth: 6
|
||||
|
||||
evennia.contrib.rpg.buffs
|
||||
evennia.contrib.rpg.dice
|
||||
evennia.contrib.rpg.health_bar
|
||||
evennia.contrib.rpg.rpsystem
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.tutorials.evadventure.build\_techdemo
|
||||
============================================================
|
||||
|
||||
.. automodule:: evennia.contrib.tutorials.evadventure.build_techdemo
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.tutorials.evadventure.build\_world
|
||||
=========================================================
|
||||
|
||||
.. automodule:: evennia.contrib.tutorials.evadventure.build_world
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.tutorials.evadventure.characters
|
||||
=======================================================
|
||||
|
||||
.. automodule:: evennia.contrib.tutorials.evadventure.characters
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.tutorials.evadventure.chargen
|
||||
====================================================
|
||||
|
||||
.. automodule:: evennia.contrib.tutorials.evadventure.chargen
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.tutorials.evadventure.combat\_turnbased
|
||||
==============================================================
|
||||
|
||||
.. automodule:: evennia.contrib.tutorials.evadventure.combat_turnbased
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.tutorials.evadventure.commands
|
||||
=====================================================
|
||||
|
||||
.. automodule:: evennia.contrib.tutorials.evadventure.commands
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.tutorials.evadventure.dungeon
|
||||
====================================================
|
||||
|
||||
.. automodule:: evennia.contrib.tutorials.evadventure.dungeon
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.tutorials.evadventure.enums
|
||||
==================================================
|
||||
|
||||
.. automodule:: evennia.contrib.tutorials.evadventure.enums
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.tutorials.evadventure.equipment
|
||||
======================================================
|
||||
|
||||
.. automodule:: evennia.contrib.tutorials.evadventure.equipment
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
```
|
||||
39
docs/source/api/evennia.contrib.tutorials.evadventure.md
Normal file
39
docs/source/api/evennia.contrib.tutorials.evadventure.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
```{eval-rst}
|
||||
evennia.contrib.tutorials.evadventure
|
||||
=============================================
|
||||
|
||||
.. automodule:: evennia.contrib.tutorials.evadventure
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 6
|
||||
|
||||
evennia.contrib.tutorials.evadventure.build_techdemo
|
||||
evennia.contrib.tutorials.evadventure.build_world
|
||||
evennia.contrib.tutorials.evadventure.characters
|
||||
evennia.contrib.tutorials.evadventure.chargen
|
||||
evennia.contrib.tutorials.evadventure.combat_turnbased
|
||||
evennia.contrib.tutorials.evadventure.commands
|
||||
evennia.contrib.tutorials.evadventure.dungeon
|
||||
evennia.contrib.tutorials.evadventure.enums
|
||||
evennia.contrib.tutorials.evadventure.equipment
|
||||
evennia.contrib.tutorials.evadventure.npcs
|
||||
evennia.contrib.tutorials.evadventure.objects
|
||||
evennia.contrib.tutorials.evadventure.quests
|
||||
evennia.contrib.tutorials.evadventure.random_tables
|
||||
evennia.contrib.tutorials.evadventure.rooms
|
||||
evennia.contrib.tutorials.evadventure.rules
|
||||
evennia.contrib.tutorials.evadventure.shops
|
||||
evennia.contrib.tutorials.evadventure.utils
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 6
|
||||
|
||||
evennia.contrib.tutorials.evadventure.tests
|
||||
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue