mirror of
https://github.com/evennia/evennia.git
synced 2026-04-01 21:47:17 +02:00
Merge branch 'evennia:main' into hex_colors
This commit is contained in:
commit
716807aea3
207 changed files with 2521 additions and 1108 deletions
13
.release.sh
13
.release.sh
|
|
@ -9,13 +9,14 @@ echo " 2. On main branch, update CHANGELOG.md."
|
|||
echo " 3. Make sure pyproject.toml is set to the same major.minor.patch version as evennia/VERSION.txt ($VERSION)."
|
||||
echo " 4. If major release:"
|
||||
echo " a. Update docs/sources/conf.py, Add '[MAJOR_VERSION].x' to 'legacy_versions' and 'v$VERSION' to 'legacy_branches'."
|
||||
echo " b. Make sure all changes are committed, e.g. as 'Evennia $VERSION major/minor/patch release'."
|
||||
echo " c. Check out a new branch v$VERSION."
|
||||
echo " d. Push the v$VERSION branch to github."
|
||||
echo " e. On the v$VERSION branch, temporarily set 'current_is_legacy=True' in source/conf.py, then (re)build "
|
||||
echo " b. Update 'SECURITY.md' with latest new version."
|
||||
echo " c. Make sure all changes are committed, e.g. as 'Evennia $VERSION major/minor/patch release'."
|
||||
echo " d. Check out a new branch v$VERSION."
|
||||
echo " e. Push the v$VERSION branch to github."
|
||||
echo " f. On the v$VERSION branch, temporarily set 'current_is_legacy=True' in source/conf.py, then (re)build "
|
||||
echo " the docs for this release with 'make local' and old-version warning headers. Throw away git changes after."
|
||||
echo " f. Rename the created build/html folder to '[MAJOR_VERSION].x'. Manually copy it to the gh-pages branch's build/ folder."
|
||||
echo " g. Add the folder, commit and push to the gh-pages branch. Then checkout main branch again."
|
||||
echo " g. Rename the created build/html folder to '[MAJOR_VERSION].x'. Manually copy it to the gh-pages branch's build/ folder."
|
||||
echo " h. Add the folder, commit and push to the gh-pages branch. Then checkout main branch again."
|
||||
echo " 5. Run 'make local' in docs/ to update dynamic docs (like Changelog.md) and autodocstrings (may have to run twice)."
|
||||
echo " 6. Make sure all changes are committed (if not already), e.g. as 'Evennia $VERSION major/minor/patch release' (un-staged files will be wiped)."
|
||||
echo " 7. Make sure all unit tests pass!"
|
||||
|
|
|
|||
108
CHANGELOG.md
108
CHANGELOG.md
|
|
@ -2,16 +2,120 @@
|
|||
|
||||
## Main branch
|
||||
|
||||
- [Feature][pull3470]: New `exit_order` kwarg to
|
||||
`DefaultObject.get_display_exits` to easier customize the order in which
|
||||
standard exits are displayed in a room (chiizujin)
|
||||
|
||||
[pull3470]: https://github.com/evennia/evennia/pull/3470
|
||||
|
||||
## Evennia 4.1.1
|
||||
|
||||
April 6, 2024
|
||||
|
||||
- [Fix][pull3438]: Error with 'you' mapping in third-person style of
|
||||
`msg_contents` (InspectorCaracal)
|
||||
- [Fix][pull3472]: The new `filter_visible` didn't exclude oneself by default
|
||||
(InspectorCaracal)
|
||||
- Fix: `find #dbref` results didn't include the results of
|
||||
`.get_extra_display_name_info` (the #dbref display by default) (Griatch)
|
||||
- Fix: Add `DefaultAccount.get_extra_display_name_info` method for API
|
||||
compliance with `DefaultObject` in commands. (Griatch)
|
||||
- Fix: Show `XYZRoom` subclass when repr() it. (Griatch)
|
||||
- [Fix][pull3485]: Typo in `sethome` message (chiizujin)
|
||||
- [Fix][pull3487]: Fix traceback when using `get`,`drop` and `give` with no
|
||||
arguments (InspectorCaracal)
|
||||
- [Fix][issue3476]: Don't ignore EvEditor commands with wrong capitalization (Griatch)
|
||||
- [Fix][issue3477]: The `at_server_reload_start()` hook was not firing on
|
||||
a reload (regression).
|
||||
- [Fix][issue3488]: `AttributeProperty(<default>, autocreate=False)`, where
|
||||
`<default>` was mutable would not update/save properly in-place (Griatch)
|
||||
- [Docs] Added new [Server-Lifecycle][doc-server-lifecycle] page to describe
|
||||
the hooks called on server start/stop/reload (Griatch)
|
||||
- [Docs] Doc typo fixes (Griatch, chiizujin)
|
||||
|
||||
[pull3438]: https://github.com/evennia/evennia/pull/3446
|
||||
[pull3485]: https://github.com/evennia/evennia/pull/3485
|
||||
[pull3487]: https://github.com/evennia/evennia/pull/3487
|
||||
[issue3476]: https://github.com/evennia/evennia/issues/3476
|
||||
[issue3477]: https://github.com/evennia/evennia/issues/3477
|
||||
[issue3488]: https://github.com/evennia/evennia/issues/3488
|
||||
[doc-server-lifecycle]: https://www.evennia.com/docs/latest/Concepts/Server-Lifecycle.html
|
||||
|
||||
|
||||
## Evennia 4.1.0
|
||||
|
||||
April 1, 2024
|
||||
|
||||
- [Deprecation]: `DefaultObject.get_visible_contents` - unused in core, will be
|
||||
removed. Use the new `.filter_visible` together with the `.get_display_*` methods instead..
|
||||
- [Deprecation]: `DefaultObject.get_content_names` - unused in core, will be
|
||||
removed. Use the `DefaultObject.get_display_*` methods instead.
|
||||
|
||||
- [Feature][pull3421]: New `utils.compress_whitespace` utility used with
|
||||
default object's `.format_appearance` to make it easier to overload without
|
||||
adding line breaks in hook returns. (InspectorCaracal)
|
||||
- [Feature][pull3458]: New `sethelp/category` switch to change a help topic's
|
||||
category after it was created (chiizujin)
|
||||
- [Feature][pull3467]: Add `alias/delete` switch for removing object aliases
|
||||
from in-game with default command (chiizujin)
|
||||
- [Feature][issue3450]: The default `page` command now tags its `Msg` objects
|
||||
with tag 'page' (category 'comms') and also checks the `Msg`' 'read' lock.
|
||||
made backwards compatible for old pages (Griatch)
|
||||
- [Feature][pull3466]: Add optional `no_article` kwarg to
|
||||
`DefaultObject.get_numbered_name` for the system to skip adding automatic
|
||||
articles. (chiizujin)
|
||||
- [Feature][pull3433]: Add ability to default get/drop to affect stacks of
|
||||
items, such as `get/drop 3 rock` by a custom class parent (InspectorCaracal)
|
||||
- Feature: Clean up the default Command variable list shown when a command has
|
||||
no `func()` defined (Griatch)
|
||||
- [Feature][issue3461]: Add `DefaultObject.filter_display_visible` helper method
|
||||
to make it easier to customize object visibility rules. (Griatch)
|
||||
- [Fix][pull3446]: Use plural ('no apples') instead of singular ('no apple') in
|
||||
`get_numbered_name` for better grammatical form (InspectorCaracal)
|
||||
- Doc: Added Beginner Tutorial lessons for AI and Procedural dungeon (Griatch)
|
||||
- Doc fixes (Griatch, InspectorCaracal)
|
||||
- [Fix][pull3453]: Object aliases not showing in search multi-match
|
||||
disambiguation display (chiizujin)
|
||||
- [Fix][pull3455]: `sethelp/edit <topic>` without a `= text` created a `None`
|
||||
entry that would lose the edit. (chiiziujin)
|
||||
- [Fix][pull3456]: `format_grid` utility used for `help` command caused commands
|
||||
to disappear for wider client widths (chiizujin)
|
||||
- [Fix][pull3457]: Help topic categories with different case would appear as
|
||||
duplicates (chiizujin)
|
||||
- [Fix][pull3454]: Traceback in crafting contrib's `recipe.msg`
|
||||
(InspectorCaracal)
|
||||
- [Fix][pull3459]: EvEditor line-echo compacted whitespace erroneously (chiizujin)
|
||||
- [Fix][pull3463]: EvEditor :help described the :paste operation in the wrong
|
||||
way (chiizujin)
|
||||
- [Fix][pull3464]: EvEditor range:range specification didn't return correct
|
||||
range (chiizujin)
|
||||
- [Fix][issue3462]: EvEditor :UU and :DD etc commands were not properly
|
||||
differentiating from their lower-case alternatives (Griatch)
|
||||
- [Fix][issue3460]: The `menu_login` contrib regression caused it to error out
|
||||
when creating a new character (Griatch)
|
||||
- Doc: Added Beginner Tutorial lessons for [Monster and NPC AI][docAI],
|
||||
[Quests][docQuests] and [Making a Procedural dungeon][docDungeon] (Griatch)
|
||||
- Doc fixes (Griatch, InspectorCaracal, homeofpoe)
|
||||
|
||||
[pull3421]: https://github.com/evennia/evennia/pull/3421
|
||||
[pull3446]: https://github.com/evennia/evennia/pull/3446
|
||||
[pull3453]: https://github.com/evennia/evennia/pull/3453
|
||||
[pull3455]: https://github.com/evennia/evennia/pull/3455
|
||||
[pull3456]: https://github.com/evennia/evennia/pull/3456
|
||||
[pull3457]: https://github.com/evennia/evennia/pull/3457
|
||||
[pull3458]: https://github.com/evennia/evennia/pull/3458
|
||||
[pull3454]: https://github.com/evennia/evennia/pull/3454
|
||||
[pull3459]: https://github.com/evennia/evennia/pull/3459
|
||||
[pull3463]: https://github.com/evennia/evennia/pull/3463
|
||||
[pull3464]: https://github.com/evennia/evennia/pull/3464
|
||||
[pull3466]: https://github.com/evennia/evennia/pull/3466
|
||||
[pull3467]: https://github.com/evennia/evennia/pull/3467
|
||||
[pull3433]: https://github.com/evennia/evennia/pull/3433
|
||||
[issue3450]: https://github.com/evennia/evennia/issues/3450
|
||||
[issue3462]: https://github.com/evennia/evennia/issues/3462
|
||||
[issue3460]: https://github.com/evennia/evennia/issues/3460
|
||||
[issue3461]: https://github.com/evennia/evennia/issues/3461
|
||||
[docAI]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.html
|
||||
[docQuests]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.html
|
||||
[docDungeon]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Dungeon.html
|
||||
|
||||
## Evennia 4.0.0
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ to use the latest minor/patch version within each major version.
|
|||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 3.x | :white_check_mark: |
|
||||
| 4.x | :white_check_mark: |
|
||||
| 3.x | :x: |
|
||||
| 2.x | :x: |
|
||||
| 1.x | :x: |
|
||||
| < 1.0 | :x: |
|
||||
|
|
|
|||
|
|
@ -1,16 +1,113 @@
|
|||
# Changelog
|
||||
|
||||
## Main branch
|
||||
## Evennia 4.1.1
|
||||
|
||||
April 6, 2024
|
||||
|
||||
- [Fix][pull3438]: Error with 'you' mapping in third-person style of
|
||||
`msg_contents` (InspectorCaracal)
|
||||
- [Fix][pull3472]: The new `filter_visible` didn't exclude oneself by default
|
||||
(InspectorCaracal)
|
||||
- Fix: `find #dbref` results didn't include the results of
|
||||
`.get_extra_display_name_info` (the #dbref display by default) (Griatch)
|
||||
- Fix: Add `DefaultAccount.get_extra_display_name_info` method for API
|
||||
compliance with `DefaultObject` in commands. (Griatch)
|
||||
- Fix: Show `XYZRoom` subclass when repr() it. (Griatch)
|
||||
- [Fix][pull3485]: Typo in `sethome` message (chiizujin)
|
||||
- [Fix][pull3487]: Fix traceback when using `get`,`drop` and `give` with no
|
||||
arguments (InspectorCaracal)
|
||||
- [Fix][issue3476]: Don't ignore EvEditor commands with wrong capitalization (Griatch)
|
||||
- [Fix][issue3477]: The `at_server_reload_start()` hook was not firing on
|
||||
a reload (regression).
|
||||
- [Fix][issue3488]: `AttributeProperty(<default>, autocreate=False)`, where
|
||||
`<default>` was mutable would not update/save properly in-place (Griatch)
|
||||
- [Docs] Added new [Server-Lifecycle][doc-server-lifecycle] page to describe
|
||||
the hooks called on server start/stop/reload (Griatch)
|
||||
- [Docs] Doc typo fixes (Griatch, chiizujin)
|
||||
|
||||
[pull3438]: https://github.com/evennia/evennia/pull/3446
|
||||
[pull3485]: https://github.com/evennia/evennia/pull/3485
|
||||
[pull3487]: https://github.com/evennia/evennia/pull/3487
|
||||
[issue3476]: https://github.com/evennia/evennia/issues/3476
|
||||
[issue3477]: https://github.com/evennia/evennia/issues/3477
|
||||
[issue3488]: https://github.com/evennia/evennia/issues/3488
|
||||
[doc-server-lifecycle]: https://www.evennia.com/docs/latest/Concepts/Server-Lifecycle.html
|
||||
|
||||
|
||||
## Evennia 4.1.0
|
||||
|
||||
April 1, 2024
|
||||
|
||||
- [Deprecation]: `DefaultObject.get_visible_contents` - unused in core, will be
|
||||
removed. Use the new `.filter_visible` together with the `.get_display_*` methods instead..
|
||||
- [Deprecation]: `DefaultObject.get_content_names` - unused in core, will be
|
||||
removed. Use the `DefaultObject.get_display_*` methods instead.
|
||||
|
||||
- [Feature][pull3421]: New `utils.compress_whitespace` utility used with
|
||||
default object's `.format_appearance` to make it easier to overload without
|
||||
adding line breaks in hook returns. (InspectorCaracal)
|
||||
- [Feature][pull3458]: New `sethelp/category` switch to change a help topic's
|
||||
category after it was created (chiizujin)
|
||||
- [Feature][pull3467]: Add `alias/delete` switch for removing object aliases
|
||||
from in-game with default command (chiizujin)
|
||||
- [Feature][issue3450]: The default `page` command now tags its `Msg` objects
|
||||
with tag 'page' (category 'comms') and also checks the `Msg`' 'read' lock.
|
||||
made backwards compatible for old pages (Griatch)
|
||||
- [Feature][pull3466]: Add optional `no_article` kwarg to
|
||||
`DefaultObject.get_numbered_name` for the system to skip adding automatic
|
||||
articles. (chiizujin)
|
||||
- [Feature][pull3433]: Add ability to default get/drop to affect stacks of
|
||||
items, such as `get/drop 3 rock` by a custom class parent (InspectorCaracal)
|
||||
- Feature: Clean up the default Command variable list shown when a command has
|
||||
no `func()` defined (Griatch)
|
||||
- [Feature][issue3461]: Add `DefaultObject.filter_display_visible` helper method
|
||||
to make it easier to customize object visibility rules. (Griatch)
|
||||
- [Fix][pull3446]: Use plural ('no apples') instead of singular ('no apple') in
|
||||
`get_numbered_name` for better grammatical form (InspectorCaracal)
|
||||
- Doc fixes (Griatch, InspectorCaracal)
|
||||
- [Fix][pull3453]: Object aliases not showing in search multi-match
|
||||
disambiguation display (chiizujin)
|
||||
- [Fix][pull3455]: `sethelp/edit <topic>` without a `= text` created a `None`
|
||||
entry that would lose the edit. (chiiziujin)
|
||||
- [Fix][pull3456]: `format_grid` utility used for `help` command caused commands
|
||||
to disappear for wider client widths (chiizujin)
|
||||
- [Fix][pull3457]: Help topic categories with different case would appear as
|
||||
duplicates (chiizujin)
|
||||
- [Fix][pull3454]: Traceback in crafting contrib's `recipe.msg`
|
||||
(InspectorCaracal)
|
||||
- [Fix][pull3459]: EvEditor line-echo compacted whitespace erroneously (chiizujin)
|
||||
- [Fix][pull3463]: EvEditor :help described the :paste operation in the wrong
|
||||
way (chiizujin)
|
||||
- [Fix][pull3464]: EvEditor range:range specification didn't return correct
|
||||
range (chiizujin)
|
||||
- [Fix][issue3462]: EvEditor :UU and :DD etc commands were not properly
|
||||
differentiating from their lower-case alternatives (Griatch)
|
||||
- [Fix][issue3460]: The `menu_login` contrib regression caused it to error out
|
||||
when creating a new character (Griatch)
|
||||
- Doc: Added Beginner Tutorial lessons for [Monster and NPC AI][docAI],
|
||||
[Quests][docQuests] and [Making a Procedural dungeon][docDungeon] (Griatch)
|
||||
- Doc fixes (Griatch, InspectorCaracal, homeofpoe)
|
||||
|
||||
[pull3421]: https://github.com/evennia/evennia/pull/3421
|
||||
[pull3446]: https://github.com/evennia/evennia/pull/3446
|
||||
[pull3453]: https://github.com/evennia/evennia/pull/3453
|
||||
[pull3455]: https://github.com/evennia/evennia/pull/3455
|
||||
[pull3456]: https://github.com/evennia/evennia/pull/3456
|
||||
[pull3457]: https://github.com/evennia/evennia/pull/3457
|
||||
[pull3458]: https://github.com/evennia/evennia/pull/3458
|
||||
[pull3454]: https://github.com/evennia/evennia/pull/3454
|
||||
[pull3459]: https://github.com/evennia/evennia/pull/3459
|
||||
[pull3463]: https://github.com/evennia/evennia/pull/3463
|
||||
[pull3464]: https://github.com/evennia/evennia/pull/3464
|
||||
[pull3466]: https://github.com/evennia/evennia/pull/3466
|
||||
[pull3467]: https://github.com/evennia/evennia/pull/3467
|
||||
[pull3433]: https://github.com/evennia/evennia/pull/3433
|
||||
[issue3450]: https://github.com/evennia/evennia/issues/3450
|
||||
[issue3462]: https://github.com/evennia/evennia/issues/3462
|
||||
[issue3460]: https://github.com/evennia/evennia/issues/3460
|
||||
[issue3461]: https://github.com/evennia/evennia/issues/3461
|
||||
[docAI]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-AI.html
|
||||
[docQuests]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Quests.html
|
||||
[docDungeon]: https://www.evennia.com/docs/latest/Howtos/Beginner-Tutorial/Part3/Beginner-Tutorial-Dungeon.html
|
||||
|
||||
## Evennia 4.0.0
|
||||
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ the in-editor help command (`:h`).
|
|||
|
||||
:y <l> - yank (copy) line <l> to the copy buffer
|
||||
:x <l> - cut line <l> and store it in the copy buffer
|
||||
:p <l> - put (paste) previously copied line directly after <l>
|
||||
:p <l> - put (paste) previously copied line directly before <l>
|
||||
:i <l> <txt> - insert new text <txt> at line <l>. Old line will move down
|
||||
:r <l> <txt> - replace line <l> with text <txt>
|
||||
:I <l> <txt> - insert text at the beginning of line <l>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ Banning.md
|
|||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Server-Lifecycle
|
||||
Protocols.md
|
||||
Models.md
|
||||
Zones.md
|
||||
|
|
|
|||
80
docs/source/Concepts/Server-Lifecycle.md
Normal file
80
docs/source/Concepts/Server-Lifecycle.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
|
||||
# Evennia Server Lifecycle
|
||||
|
||||
As part of your game design you may want to change how Evennia behaves when starting or stopping. A common use case would be to start up some piece of custom code you want to always have available once the server is up.
|
||||
|
||||
Evennia has three main life cycles, all of which you can add custom behavior for:
|
||||
|
||||
- **Database life cycle**: Evennia uses a database. This exists in parallel to the code changes you do. The database exists until you choose to reset or delete it. Doing so doesn't require re-downloading Evennia.
|
||||
- **Reboot life cycle**: From When Evennia starts to it being fully shut down, which means both Portal and Server are stopped. At the end of this cycle, all players are disconnected.
|
||||
- **Reload life cycle:** This is the main runtime, until a "reload" event. Reloads refreshes game code but do not kick any players.
|
||||
|
||||
## When Evennia starts for the first time
|
||||
|
||||
This is the beginning of the **Database life cycle**, just after the database is created and migrated for the first time (or after it was deleted and re-built). See [Choosing a Database](../Setup/Choosing-a-Database.md) for instructions on how to reset a database, should you want to re-run this sequence after the first time.
|
||||
|
||||
Hooks called, in sequence:
|
||||
|
||||
1. `evennia.server.initial_setup.handle_setup(last_step=None)`: Evennia's core initialization function. This is what creates the #1 Character (tied to the superuser account) and `Limbo` room. It calls the next hook below and also understands to restart at the last failed step if there was some issue. You should normally not override this function unless you _really_ know what you are doing. To override, change `settings.INITIAL_SETUP_MODULE` to your own module with a `handle_setup` function in it.
|
||||
2. `mygame/server/conf/at_initial_setup.py` contains a single function, `at_initial_setup()`, which will be called without arguments. It's called last in the setup sequence by the above function. Use this to add your own custom behavior or to tweak the initialization. If you for example wanted to change the auto-generated Limbo room, you should do it from here. If you want to change where this function is found, you can do so by changing `settings.AT_INITIAL_SETUP_HOOK_MODULE`.
|
||||
|
||||
## When Evennia starts and shutdowns
|
||||
|
||||
This is part of the **Reboot life cycle**. Evennia consists of two main processes, the [Portal and the Server](../Components/Portal-And-Server.md). On a reboot or shutdown, both Portal and Server shuts down, which means all players are disconnected.
|
||||
|
||||
Each process call a series of hooks located in `mygame/server/conf/at_server_startstop.py`. You can customize the module used with `settings.AT_SERVER_STARTSTOP_MODULE` - this can even be a list of modules, if so, the appropriately-named functions will be called from each module, in sequence.
|
||||
|
||||
All hooks are called without arguments.
|
||||
|
||||
> The use of the term 'server' in the hook-names indicate the whole of Evennia, not just the `Server` component.
|
||||
|
||||
### Server cold start
|
||||
|
||||
Starting the server from zero, after a full stop. This is done with `evennia start` from the terminal.
|
||||
|
||||
1. `at_server_init()` - Always called first in the startup sequence.
|
||||
2. `at_server_cold_start()` - Only called on cold starts.
|
||||
3. `at_server_start()` - Always called last in the startup sequece.
|
||||
|
||||
### Server cold shutdown
|
||||
|
||||
Shutting everything down. Done with `shutdown` in-game or `evennia stop` from the terminal.
|
||||
|
||||
1. `at_server_cold_stop()` - Only called on cold stops.
|
||||
2. `at_server_stop()` - Always called last in the stopping sequence.
|
||||
|
||||
### Server reboots
|
||||
|
||||
This is done with `evennia reboot` and effectively constitutes an automatic cold shutdown followed by a cold start controlled from the `evennia` launcher. There are no special `reboot` hooks for this, instead it looks like you'd expect:
|
||||
|
||||
1. `at_server_cold_stop()`
|
||||
2. `at_server_stop()` (after this, both `Server` + `Portal` have both shut down)
|
||||
3. `at_server_init()` (like a cold start)
|
||||
4. `at_server_cold_start()`
|
||||
5. `at_server_start()`
|
||||
|
||||
## When Evennia reloads and resets
|
||||
|
||||
This is the **Reload life cycle**. As mentioned above, Evennia consists of two components, the [Portal and Server](../Components/Portal-And-Server.md). During a reload, only the `Server` component is shut down and restarted. Since the Portal stays up, players are not disconnected.
|
||||
|
||||
All hooks are called without arguments.
|
||||
|
||||
### Server reload
|
||||
|
||||
Reloads are initiated with the `reload` command in-game, or with `evennia reload` from the terminal.
|
||||
|
||||
1. `at_server_reload_stop()` - Only called on reload stops.
|
||||
2. `at_server_stop` - Always called last in the stopping sequence.
|
||||
3. `at_server_init()` - Always called first in startup sequence.
|
||||
4. `at_server_reload_start()` - Only called on a reload (re)start.
|
||||
5. `at_server_start()` - Always called last in the startup sequence.
|
||||
|
||||
### Server reset
|
||||
|
||||
A 'reset' is a hybrid reload state, where the reload is treated as a cold shutdown only for the sake of running hooks (players are not disconnected). It's run with `reset` in-game or with `evennia reset` from the terminal.
|
||||
|
||||
1. `at_server_cold_stop()`
|
||||
2. `at_server_stop()` (after this, only `Server` has shut down)
|
||||
3. `at_server_init()` (`Server` coming back up)
|
||||
4. `at_server_cold_start()`
|
||||
5. `at_server_start()`
|
||||
|
|
@ -23,12 +23,25 @@ at the root of `evennia/docs/source/`.
|
|||
result in Evennia. This is often on a tutorial or FAQ form and will refer to the rest of the documentation for further reading.
|
||||
- `source/Howtos/Beginner-Tutorial/` holds all documents part of the initial tutorial sequence.
|
||||
|
||||
|
||||
Other files and folders:
|
||||
- `source/api/` contains the auto-generated API documentation as `.html` files. Don't edit these files manually, they are auto-generated from sources.
|
||||
- `source/_templates` and `source/_static` hold files for the doc itself. They should only be modified if wanting to change the look and structure of the documentation generation itself.
|
||||
- `conf.py` holds the Sphinx configuration. It should usually not be modified except to update the Evennia version on a new branch.
|
||||
|
||||
## Automatically generated doc pages
|
||||
|
||||
Some doc pages are automatically generated. Changes to their generated markdown file will be overwritten. Instead they must be modified at the point the automation reads the text from.
|
||||
|
||||
- All API docs under `source/api` are built from the doc strings of Evennia core code. Documentation fixes for these needs to be done in the doc strings of the relevant module, function, class or method.
|
||||
- [Contribs/Contribs-Overview.md](Contribs/Contribs-Overview.md) is completely generated from scratch when building the docs, by the script `evennia/docs/pylib/contrib_readmes2docs.py`.
|
||||
- All contrib blurbs on the above page are taken from the first paragraph of each contrib's `README.md`, found under `evennia/contrib/*/*/README.md`.
|
||||
- Similarly, all contrib documentation linked from the above page is generated from each contrib's `README.md` file.
|
||||
- [Components/Default-Commands.md](Components/Default-Commands.md) is generated from the command classes found under `evennia/commands/default/`.
|
||||
- [Coding/Evennia-Code-Style.md](Coding/Evennia-Code-Style.md) is generated from `evennia/CODING_STYLE.md`.
|
||||
- [Coding/Changelog.md](Coding/Changelog.md) is generated from `evennia/CHANGELOG.md`
|
||||
- [Setup/Settings-Default.md](Setup/Settings-Default.md) is generated from the default settings file `evennia/default_settings.py`
|
||||
|
||||
Most auto-generated pages have a warning in the header indicating that it's auto-generated.
|
||||
|
||||
## Editing syntax
|
||||
|
||||
|
|
|
|||
|
|
@ -91,47 +91,34 @@ The `me.cmdset` is the store of all cmdsets stored on us. By giving the path to
|
|||
Now try
|
||||
|
||||
> echo
|
||||
Command echo has no defined `func()` - showing on-command variables:
|
||||
...
|
||||
...
|
||||
Command "echo" has no defined `func()`. Available properties ...
|
||||
...(lots of stuff)...
|
||||
|
||||
`echo` works! You should be getting a long list of outputs. The reason for this is that your `echo` function is not really "doing" anything yet and the default function is then to show all useful resources available to you when you use your Command. Let's look at some of those listed:
|
||||
`echo` works! You should be getting a long list of outputs. Your `echo` function is not really "doing" anything yet and the default function is then to show all useful resources available to you when you use your Command. Let's look at some of those listed:
|
||||
|
||||
Command echo has no defined `func()` - showing on-command variables:
|
||||
obj (<class 'typeclasses.characters.Character'>): YourName
|
||||
lockhandler (<class 'evennia.locks.lockhandler.LockHandler'>): cmd:all()
|
||||
caller (<class 'typeclasses.characters.Character'>): YourName
|
||||
cmdname (<class 'str'>): echo
|
||||
raw_cmdname (<class 'str'>): echo
|
||||
cmdstring (<class 'str'>): echo
|
||||
args (<class 'str'>):
|
||||
cmdset (<class 'evennia.commands.cmdset.CmdSet'>): @mail, about, access, accounts, addcom, alias, allcom, ban, batchcode, batchcommands, boot, cboot, ccreate,
|
||||
cdesc, cdestroy, cemit, channels, charcreate, chardelete, checklockstring, clientwidth, clock, cmdbare, cmdsets, color, copy, cpattr, create, cwho, delcom,
|
||||
desc, destroy, dig, dolphin, drop, echo, emit, examine, find, force, get, give, grapevine2chan, help, home, ic, inventory, irc2chan, ircstatus, link, lock,
|
||||
look, menutest, mudinfo, mvattr, name, nick, objects, ooc, open, option, page, password, perm, pose, public, py, quell, quit, reload, reset, rss2chan, say,
|
||||
script, scripts, server, service, sessions, set, setdesc, sethelp, sethome, shutdown, spawn, style, tag, tel, test2010, test2028, testrename, testtable,
|
||||
tickers, time, tunnel, typeclass, unban, unlink, up, up, userpassword, wall, whisper, who, wipe
|
||||
session (<class 'evennia.server.serversession.ServerSession'>): Griatch(#1)@1:2:7:.:0:.:0:.:1
|
||||
account (<class 'typeclasses.accounts.Account'>): Griatch(account 1)
|
||||
raw_string (<class 'str'>): echo
|
||||
|
||||
--------------------------------------------------
|
||||
echo - Command variables from evennia:
|
||||
--------------------------------------------------
|
||||
name of cmd (self.key): echo
|
||||
cmd aliases (self.aliases): []
|
||||
cmd locks (self.locks): cmd:all();
|
||||
help category (self.help_category): General
|
||||
object calling (self.caller): Griatch
|
||||
object storing cmdset (self.obj): Griatch
|
||||
command string given (self.cmdstring): echo
|
||||
current cmdset (self.cmdset): ChannelCmdSet
|
||||
```
|
||||
Command "echo" has no defined `func()` method. Available properties on this command are:
|
||||
|
||||
self.key (<class 'str'>): "echo"
|
||||
self.cmdname (<class 'str'>): "echo"
|
||||
self.raw_cmdname (<class 'str'>): "echo"
|
||||
self.raw_string (<class 'str'>): "echo
|
||||
"
|
||||
self.aliases (<class 'list'>): []
|
||||
self.args (<class 'str'>): ""
|
||||
self.caller (<class 'typeclasses.characters.Character'>): YourName
|
||||
self.obj (<class 'typeclasses.characters.Character'>): YourName
|
||||
self.session (<class 'evennia.server.serversession.ServerSession'>): YourName(#1)@1:2:7:.:0:.:0:.:1
|
||||
self.locks (<class 'str'>): "cmd:all();"
|
||||
self.help_category (<class 'str'>): "general"
|
||||
self.cmdset (... a long list of commands ...)
|
||||
```
|
||||
These are all properties you can access with `.` on the Command instance, such as `.key`, `.args` and so on. Evennia makes these available to you and they will be different every time a command is run. The most important ones we will make use of now are:
|
||||
|
||||
- `caller` - this is 'you', the person calling the command.
|
||||
- `args` - this is all arguments to the command. Now it's empty, but if you tried `echo foo bar` you'd find that this would be `" foo bar"`.
|
||||
- `args` - this is all arguments to the command. Now it's empty, but if you tried `echo foo bar` you'd find that this would be `" foo bar"` (including the extra space between `echo` and `foo` that you may want to strip away).
|
||||
- `obj` - this is object on which this Command (and CmdSet) "sits". So you, in this case.
|
||||
- `raw_string` is not commonly used, but it's the completely unmodified input from the user. It even includes the line break used to send to the command to the server (that's why the end-quotes appear on the next line).
|
||||
|
||||
The reason our command doesn't do anything yet is because it's missing a `func` method. This is what Evennia looks for to figure out what a Command actually does. Modify your `CmdEcho` class:
|
||||
|
||||
|
|
@ -238,7 +225,7 @@ Tweak this file as follows:
|
|||
```python
|
||||
# in mygame/commands/default_cmdsets.py
|
||||
|
||||
# ,..
|
||||
# ...
|
||||
|
||||
from . import mycommands # <-------
|
||||
|
||||
|
|
@ -297,10 +284,9 @@ And Bob would see
|
|||
Still in `mygame/commands/mycommands.py`, add a new class, between `CmdEcho` and `MyCmdSet`.
|
||||
|
||||
```{code-block} python
|
||||
# in mygame/commands/mycommands.py
|
||||
|
||||
:linenos:
|
||||
:emphasize-lines: 3,4,11,14,15,17,18,19,21
|
||||
:emphasize-lines: 5,6,13,16,19,20,21,23
|
||||
# in mygame/commands/mycommands.py
|
||||
|
||||
# ...
|
||||
|
||||
|
|
@ -324,18 +310,17 @@ class CmdHit(Command):
|
|||
return
|
||||
self.caller.msg(f"You hit {target.key} with full force!")
|
||||
target.msg(f"You got hit by {self.caller.key} with full force!")
|
||||
# ...
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
A lot of things to dissect here:
|
||||
- **Line 3**: The normal `class` header. We inherit from `Command` which we imported at the top of this file.
|
||||
- **Lines 4-10**: The docstring and help-entry for the command. You could expand on this as much as you wanted.
|
||||
- **Line 11**: We want to write `hit` to use this command.
|
||||
- **Line 14**: We strip the whitespace from the argument like before. Since we don't want to have to do `self.args.strip()` over and over, we store the stripped version in a _local variable_ `args`. Note that we don't modify `self.args` by doing this, `self.args` will still have the whitespace and is not the same as `args` in this example.
|
||||
- **Line 5**: The normal `class` header. We inherit from `Command` which we imported at the top of this file.
|
||||
- **Lines 6-12**: The docstring and help-entry for the command. You could expand on this as much as you wanted.
|
||||
- **Line 13**: We want to write `hit` to use this command.
|
||||
- **Line 16**: We strip the whitespace from the argument like before. Since we don't want to have to do `self.args.strip()` over and over, we store the stripped version in a _local variable_ `args`. Note that we don't modify `self.args` by doing this, `self.args` will still have the whitespace and is not the same as `args` in this example.
|
||||
|
||||
```{sidebar} if-statements
|
||||
|
||||
The full form of the if statement is
|
||||
|
||||
if condition:
|
||||
|
|
@ -346,9 +331,9 @@ The full form of the if statement is
|
|||
...
|
||||
|
||||
There can be any number of `elifs` to mark when different branches of the code should run. If `else` is provided, it will run if none of the other conditions were truthy.
|
||||
|
||||
```
|
||||
- **Line 15** has our first _conditional_, an `if` statement. This is written on the form `if <condition>:` and only if that condition is 'truthy' will the indented code block under the `if` statement run. To learn what is truthy in Python it's usually easier to learn what is "falsy":
|
||||
|
||||
- **Line 17** has our first _conditional_, an `if` statement. This is written on the form `if <condition>:` and only if that condition is 'truthy' will the indented code block under the `if` statement run. To learn what is truthy in Python it's usually easier to learn what is "falsy":
|
||||
- `False` - this is a reserved boolean word in Python. The opposite is `True`.
|
||||
- `None` - another reserved word. This represents nothing, a null-result or value.
|
||||
- `0` or `0.0`
|
||||
|
|
@ -356,12 +341,20 @@ There can be any number of `elifs` to mark when different branches of the code s
|
|||
- Empty _iterables_ we haven't used yet, like empty lists `[]`, empty tuples `()` and empty dicts `{}`.
|
||||
- Everything else is "truthy".
|
||||
|
||||
- **Line 16**'s condition is `not args`. The `not` _inverses_ the result, so if `args` is the empty string (falsy), the whole conditional becomes truthy. Let's continue in the code:
|
||||
The conditional on **Line 16**'s condition is `not args`. The `not` _inverses_ the result, so if `args` is the empty string (falsy), the whole conditional becomes truthy. Let's continue in the code:
|
||||
```{sidebar} Errors in your code
|
||||
|
||||
With longer code snippets to try, it gets more and more likely you'll
|
||||
make an error and get a `traceback` when you reload. This will either appear
|
||||
directly in-game or in your log (view it with `evennia -l` in a terminal).
|
||||
|
||||
Don't panic - tracebacks are your friends! They are to be read bottom-up and usually describe exactly where your problem is. Refer to [The Python introduction lesson](./Beginner-Tutorial-Python-basic-introduction.md) for more hints. If you get stuck, reach out to the Evennia community for help.
|
||||
```
|
||||
- **Lines 16-17**: This code will only run if the `if` statement is truthy, in this case if `args` is the empty string.
|
||||
- **Line 17**: `return` is a reserved Python word that exits `func` immediately.
|
||||
- **Line 18**: We use `self.caller.search` to look for the target in the current location.
|
||||
- **Lines 19-20**: A feature of `.search` is that it will already inform `self.caller` if it couldn't find the target. In that case, `target` will be `None` and we should just directly `return`.
|
||||
- **Lines 21-22**: At this point we have a suitable target and can send our punching strings to each.
|
||||
- **Line 19**: `return` is a reserved Python word that exits `func` immediately.
|
||||
- **Line 20**: We use `self.caller.search` to look for the target in the current location.
|
||||
- **Lines 21-22**: A feature of `.search` is that it will already inform `self.caller` if it couldn't find the target. In that case, `target` will be `None` and we should just directly `return`.
|
||||
- **Lines 23-24**: At this point we have a suitable target and can send our punching strings to each.
|
||||
|
||||
Finally we must also add this to a CmdSet. Let's add it to `MyCmdSet`.
|
||||
|
||||
|
|
@ -377,14 +370,6 @@ class MyCmdSet(CmdSet):
|
|||
|
||||
```
|
||||
|
||||
```{sidebar} Errors in your code
|
||||
|
||||
With longer code snippets to try, it gets more and more likely you'll
|
||||
make an error and get a `traceback` when you reload. This will either appear
|
||||
directly in-game or in your log (view it with `evennia -l` in a terminal).
|
||||
Don't panic; tracebacks are your friends - they are to be read bottom-up and usually describe exactly where your problem is. Refer to [The Python introduction lesson](./Beginner-Tutorial-Python-basic-introduction.md) for more hints. If you get stuck, reach out to the Evennia community for help.
|
||||
```
|
||||
|
||||
Note that since we did `py self.cmdset.remove("commands.mycommands.MyCmdSet")` earlier, this cmdset is no longer available on our Character. Instead we will add these commands directly to our default cmdset.
|
||||
|
||||
```python
|
||||
|
|
@ -439,4 +424,4 @@ You won't see the second string. Only Smaug sees that (and is not amused).
|
|||
In this lesson we learned how to create our own Command, add it to a CmdSet and then to ourselves. We also upset a dragon.
|
||||
|
||||
In the next lesson we'll learn how to hit Smaug with different weapons. We'll also
|
||||
get into how we replace and extend Evennia's default Commands.
|
||||
get into how we replace and extend Evennia's default Commands.
|
||||
|
|
|
|||
|
|
@ -149,6 +149,54 @@ If you you really want all matches to the search parameters you specify. In othe
|
|||
|
||||
There are equivalent search functions for all the main resources. You can find a listing of them [in the Search functions section](../../../Evennia-API.md) of the API front page.
|
||||
|
||||
## Understanding object relationships
|
||||
|
||||
It's important to understand how objects relate to one another when searching.
|
||||
|
||||
Let's consider a `chest` with a `coin` inside it. The chest stands in a `dungeon` room. In the dungeon is also a `door` (an exit leading outside).
|
||||
|
||||
```
|
||||
┌───────────────────────┐
|
||||
│dungeon │
|
||||
│ ┌─────────┐ │
|
||||
│ │chest │ ┌────┐ │
|
||||
│ │ ┌────┐ │ │door│ │
|
||||
│ │ │coin│ │ └────┘ │
|
||||
│ │ └────┘ │ │
|
||||
│ │ │ │
|
||||
│ └─────────┘ │
|
||||
│ │
|
||||
└───────────────────────┘
|
||||
```
|
||||
|
||||
If you have access to any in-game Object, you can find related objects by use if its `.location` and `.contents` properties.
|
||||
|
||||
- `coin.location` is `chest`.
|
||||
- `chest.location` is `dungeon`.
|
||||
- `door.location` is `dungeon`.
|
||||
- `room.location` is `None` since it's not inside something else.
|
||||
|
||||
One can use this to find what is inside what. For example, `coin.location.location` is the `dungeon`.
|
||||
|
||||
- `room.contents` is `[chest, door]`
|
||||
- `chest.contents` is `[coin]`
|
||||
- `coin.contents` is `[]`, the empty list since there's nothing 'inside' the coin.
|
||||
- `door.contents` is `[]` too.
|
||||
|
||||
A convenient helper is `.contents_get` - this allows to restrict what is returned:
|
||||
|
||||
- `room.contents_get(exclude=chest)` - this returns everything in the room except the chest (maybe it's hidden?)
|
||||
|
||||
There is a special property for finding exits:
|
||||
|
||||
- `room.exits` is `[door]`
|
||||
- `coin.exits` is `[]` since it has no exits (same for all the other objects)
|
||||
|
||||
There is a property `.destination` which is only used by exits:
|
||||
|
||||
- `door.destination` is `outside` (or wherever the door leads)
|
||||
- `room.destination` is `None` (same for all the other non-exit objects)
|
||||
|
||||
## What can be searched for
|
||||
|
||||
These are the main database entities one can search for:
|
||||
|
|
@ -162,6 +210,8 @@ These are the main database entities one can search for:
|
|||
|
||||
Most of the time you'll likely spend your time searching for Objects and the occasional Accounts.
|
||||
|
||||
Most search methods are available directly from `evennia`. But there are also a lot of useful search helpers found via `evennia.search`.
|
||||
|
||||
So to find an entity, what can be searched for?
|
||||
|
||||
### Search by key
|
||||
|
|
@ -206,9 +256,9 @@ However, using `search_object` will find the rose wherever it's located:
|
|||
> py evennia.search_object("rose")
|
||||
<QuerySet [Rose]>
|
||||
|
||||
However, if you demand that the room is in the current room, it won't be found:
|
||||
The `evennia.search_object` method doesn't have a `location` argument. What you do instead is to limit the search by setting its `candidates` keyword to the `.contents` of the current location. This is the same as a location search, since it will only accept matches among those in the room. In this example we'll (correctly) find the rose is not in the room.
|
||||
|
||||
> py evennia.search_object("rose", location=here)
|
||||
> py evennia.search_object("rose", candidate=here.contents)
|
||||
<QuerySet []>
|
||||
|
||||
In general, the `Object.search` is a shortcut for doing the very common searches of things in the same location, whereas the `search_object` finds objects anywhere.
|
||||
|
|
@ -267,7 +317,7 @@ For example, let's say our plants have a 'growth state' that updates as it grows
|
|||
|
||||
Now we can find the things that have a given growth state:
|
||||
|
||||
> py evennia.search_object_attribute("growth_state", "withering")
|
||||
> py evennia.search_object("withering", attribute_name="growth_state")
|
||||
<QuerySet [Rose]>
|
||||
|
||||
> Searching by Attribute can be very practical. But if you want to group entities or search very often, using Tags and search by Tags is faster and more resource-efficient.
|
||||
|
|
@ -317,53 +367,11 @@ In legacy code bases you may be used to relying a lot on #dbrefs to find and tra
|
|||
```
|
||||
|
||||
|
||||
## Finding objects relative each other
|
||||
## Summary
|
||||
|
||||
It's important to understand how objects relate to one another when searching.
|
||||
Let's consider a `chest` with a `coin` inside it. The chest stands in a room `dungeon`. In the dungeon is also a `door`. This is an exit leading outside.
|
||||
Knowing how to find things is important and the tools from this section will serve you well. These tools will cover most of your regular needs.
|
||||
|
||||
```
|
||||
┌───────────────────────┐
|
||||
│dungeon │
|
||||
│ ┌─────────┐ │
|
||||
│ │chest │ ┌────┐ │
|
||||
│ │ ┌────┐ │ │door│ │
|
||||
│ │ │coin│ │ └────┘ │
|
||||
│ │ └────┘ │ │
|
||||
│ │ │ │
|
||||
│ └─────────┘ │
|
||||
│ │
|
||||
└───────────────────────┘
|
||||
```
|
||||
|
||||
- `coin.location` is `chest`.
|
||||
- `chest.location` is `dungeon`.
|
||||
- `door.location` is `dungeon`.
|
||||
- `room.location` is `None` since it's not inside something else.
|
||||
|
||||
One can use this to find what is inside what. For example, `coin.location.location` is the `dungeon`.
|
||||
We can also find what is inside each object. This is a list of things.
|
||||
|
||||
- `room.contents` is `[chest, door]`
|
||||
- `chest.contents` is `[coin]`
|
||||
- `coin.contents` is `[]`, the empty list since there's nothing 'inside' the coin.
|
||||
- `door.contents` is `[]` too.
|
||||
|
||||
A convenient helper is `.contents_get` - this allows to restrict what is returned:
|
||||
|
||||
- `room.contents_get(exclude=chest)` - this returns everything in the room except the chest (maybe it's hidden?)
|
||||
|
||||
There is a special property for finding exits:
|
||||
|
||||
- `room.exits` is `[door]`
|
||||
- `coin.exits` is `[]` (same for all the other objects)
|
||||
|
||||
There is a property `.destination` which is only used by exits:
|
||||
|
||||
- `door.destination` is `outside` (or wherever the door leads)
|
||||
- `room.destination` is `None` (same for all the other non-exit objects)
|
||||
|
||||
You can also include this information in searches:
|
||||
Not always though. If we go back to the example of a coin in a chest from before, you _could_ use the following to dynamically figure out if there are any chests in the room with coins inside:
|
||||
|
||||
```python
|
||||
from evennia import search_object
|
||||
|
|
@ -372,13 +380,9 @@ from evennia import search_object
|
|||
dungeons = search_object("dungeon", typeclass="typeclasses.rooms.Room")
|
||||
chests = search_object("chest", location=dungeons[0])
|
||||
# find if there are any skulls in the chest
|
||||
skulls = search_object("Skull", candidates=chests[0].contents)
|
||||
coins = search_object("coin", candidates=chests[0].contents)
|
||||
```
|
||||
|
||||
More advanced, nested queries like this can however often be made more efficient by using the hints in the next lesson.
|
||||
This would work but is both quite inefficient, fragile and a lot to type. This kind of thing is better done by directly querying the database.
|
||||
|
||||
## Summary
|
||||
|
||||
Knowing how to find things is important and the tools from this section will serve you well. These tools will cover most of your needs ...
|
||||
|
||||
... but not always. In the next lesson we will dive further into more complex searching when we look at Django queries and querysets in earnest.
|
||||
In the next lesson we will dive further into more complex searching when we look at Django queries and querysets in earnest.
|
||||
|
|
@ -40,6 +40,8 @@ You can find an AIHandler implemented in `evennia/contrib/tutorials`, in [evadve
|
|||
```
|
||||
This is the core logic for managing AI states. Create a new file `evadventure/ai.py`.
|
||||
|
||||
> Create a new file `evadventure/ai.py`.
|
||||
|
||||
```{code-block} python
|
||||
:linenos:
|
||||
:emphasize-lines: 10,11-13,16,23
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
# Player Characters
|
||||
|
||||
In the [previous lesson about rules and dice rolling](./Beginner-Tutorial-Rules.md) we made some
|
||||
assumptions about the "Player Character" entity:
|
||||
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
|
||||
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.
|
||||
|
||||
|
|
@ -16,8 +14,7 @@ RPG, it will hold everything relevant to that PC.
|
|||
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:
|
||||
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
|
||||
|
|
@ -34,9 +31,7 @@ class EvAdventureMob(EvAdventureNPC):
|
|||
|
||||
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:
|
||||
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
|
||||
|
|
@ -60,8 +55,7 @@ Nevertheless, there are some things that _should_ be common for all 'living thin
|
|||
- 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:
|
||||
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
|
||||
|
|
@ -83,10 +77,7 @@ class EvAdventureMob(LivingMixin, EvadventureNPC):
|
|||
In [evennia/contrib/tutorials/evadventure/characters.py](../../../api/evennia.contrib.tutorials.evadventure.characters.md)
|
||||
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.
|
||||
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
|
||||
|
||||
|
|
@ -178,7 +169,6 @@ Most of these are empty since they will behave differently for characters and np
|
|||
|
||||
Once we create more of our game, we will need to remember to actually call these hook methods so they serve a purpose. For example, once we implement combat, we must remember to call `at_attacked` as well as the other methods involving taking damage, getting defeated or dying.
|
||||
|
||||
|
||||
## Character class
|
||||
|
||||
We will now start making the basic Character class, based on what we need from _Knave_.
|
||||
|
|
@ -234,8 +224,7 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
|||
|
||||
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:
|
||||
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`
|
||||
|
|
@ -249,7 +238,7 @@ We implement the Player Character versions of `at_defeat` and `at_death`. We als
|
|||
|
||||
### Funcparser inlines
|
||||
|
||||
This piece of code is worth some more explanation:
|
||||
This piece of code in the `at_defeat` method above is worth some more extra explanation:
|
||||
|
||||
```python
|
||||
self.location.msg_contents(
|
||||
|
|
@ -259,8 +248,7 @@ self.location.msg_contents(
|
|||
|
||||
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.
|
||||
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."`
|
||||
|
|
@ -303,10 +291,7 @@ You can easily make yourself an `EvAdventureCharacter` in-game by using the
|
|||
|
||||
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`).
|
||||
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:
|
||||
|
||||
|
|
@ -327,8 +312,7 @@ instead.
|
|||
|
||||
> 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.
|
||||
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
|
||||
|
|
@ -368,22 +352,18 @@ class TestCharacters(BaseEvenniaTest):
|
|||
# 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.
|
||||
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
|
||||
evennia test --settings settings.py .evadventure.tests.test_characters
|
||||
|
||||
|
||||
## 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.
|
||||
_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:
|
||||
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
|
||||
|
|
@ -399,8 +379,7 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
|||
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 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.
|
||||
|
|
@ -409,23 +388,16 @@ We'd then need to expand our [rules module](./Beginner-Tutorial-Rules.md) (and l
|
|||
## Summary
|
||||
|
||||
|
||||
With the `EvAdventureCharacter` class in place, we have a better understanding of how our PCs will look
|
||||
like under _Knave_.
|
||||
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
|
||||
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
|
||||
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.
|
||||
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.
|
||||
```
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ We will design a base combat system that supports both styles.
|
|||
> Create a new module `evadventure/combat_base.py`
|
||||
|
||||
```{sidebar}
|
||||
In [evennia/contrib/tutorials/evadventure/combat_base.py](evennia.contrib.tutorials.evadventure.combat_base) you'll find a complete implementation of the base combat module.
|
||||
Under `evennia/contrib/tutorials/evadventure/`, in [combat_base.py](evennia.contrib.tutorials.evadventure.combat_base) you'll find a complete implementation of the base combat module.
|
||||
```
|
||||
Our "Combat Handler" will handle the administration around combat. It needs to be _persistent_ (even is we reload the server your combat should keep going).
|
||||
|
||||
|
|
@ -718,7 +718,7 @@ We rely on the [Equipment handler](./Beginner-Tutorial-Equipment.md) we created
|
|||
> Create a module `evadventure/tests/test_combat.py`.
|
||||
|
||||
```{sidebar}
|
||||
See [evennia/contrib/tutorials/evadventure/tests/test_combat.py](evennia.contrib.tutorials.evadventure.tests.test_combat) for ready-made combat unit tests.
|
||||
Look under `evennia/contrib/tutorials/evadventure/`, in [tests/test_combat.py](evennia.contrib.tutorials.evadventure.tests.test_combat) for ready-made combat unit tests.
|
||||
```
|
||||
|
||||
Unit testing the combat base classes can seem impossible because we have not yet implemented most of it. We can however get very far by the use of [Mocks](https://docs.python.org/3/library/unittest.mock.html). The idea of a Mock is that you _replace_ a piece of code with a dummy object (a 'mock') that can be called to return some specific value.
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ The advantage of using a menu is that you have all possible actions directly ava
|
|||
## General Principle
|
||||
|
||||
```{sidebar}
|
||||
An example of an implemented Turnbased combat system can be found in [evennia/contrib/tutorials/evadventure/combat_turnbased.py](evennia.contrib.tutorials.evadventure.combat_turnbased).
|
||||
An example of an implemented Turnbased combat system can be found under `evennia/contrib/tutorials/evadventure/`, in [combat_turnbased.py](evennia.contrib.tutorials.evadventure.combat_turnbased).
|
||||
```
|
||||
Here is the general principle of the Turnbased combat handler:
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class EvAdventureObject(DefaultObject):
|
|||
"""The top of the description"""
|
||||
return ""
|
||||
|
||||
def get_display_desc(self, looker, **kwargs)
|
||||
def get_display_desc(self, looker, **kwargs):
|
||||
"""The main display - show object stats"""
|
||||
return get_obj_stats(self, owner=looker)
|
||||
|
||||
|
|
@ -216,7 +216,7 @@ class EvAdventureConsumable(EvAdventureObject):
|
|||
"""Called when using the item"""
|
||||
pass
|
||||
|
||||
def at_post_use(self. user, *args, **kwargs):
|
||||
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
|
||||
|
|
@ -452,7 +452,7 @@ _BARE_HANDS = None
|
|||
|
||||
# ...
|
||||
|
||||
class WeaponBareHands(EvAdventureWeapon)
|
||||
class WeaponBareHands(EvAdventureWeapon):
|
||||
obj_type = ObjType.WEAPON
|
||||
inventory_use_slot = WieldLocation.WEAPON_HAND
|
||||
attack_type = Ability.STR
|
||||
|
|
|
|||
|
|
@ -1,5 +1,402 @@
|
|||
# Game Quests
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
A _quest_ is a common feature of games. From classic fetch-quests like retrieving 10 flowers to complex quest chains involving drama and intrigue, quests need to be properly tracked in our game.
|
||||
|
||||
A quest follows a specific development:
|
||||
|
||||
1. The quest is _started_. This normally involves the player accepting the quest, from a quest-giver, job board or other source. But the quest could also be thrust on the player ("save the family from the burning house before it collapses!")
|
||||
2. Once a quest has been accepted and assigned to a character, it is either either `Started` (that is, 'in progress'), `Abandoned`, `Failed` or `Complete`.
|
||||
3. A quest may consist of one or more 'steps'. Each step has its own set of finish conditions.
|
||||
4. At suitable times the quest's _progress_ is checked. This could happen on a timer or when trying to 'hand in' the quest. When checking, the current 'step' is checked against its finish conditions. If ok, that step is closed and the next step is checked until it either hits a step that is not yet complete, or there are no more steps, in which case the entire quest is complete.
|
||||
|
||||
```{sidebar}
|
||||
An example implementation of quests is found under `evennia/contrib/tutorials`, in [evadvanture/quests.py](evennia.contrib.tutorials.evadventure.quests).
|
||||
```
|
||||
To represent quests in code, we need
|
||||
- A convenient flexible way to code how we check the status and current steps of the quest. We want this scripting to be as flexible as possible. Ideally we want to be able to code the quests's logic in full Python.
|
||||
- Persistence. The fact that we accepted the quest, as well as its status and other flags must be saved in the database and survive a server reboot.
|
||||
|
||||
We'll accomplish this using two pieces of Python code:
|
||||
- `EvAdventureQuest`: A Python class with helper methods that we can call to check current quest status, figure if a given quest-step is complete or not. We will create and script new quests by simply inheriting from this base class and implement new methods on it in a standardized way.
|
||||
- `EvAdventureQuestHandler` will sit 'on' each Character as `character.quests`. It will hold all `EvAdventureQuest`s that the character is or has been involved in. It is also responsible for storing quest state using [Attributes](../../../Components/Attributes.md) on the Character.
|
||||
|
||||
## The Quest Handler
|
||||
|
||||
> Create a new module `evadventure/quests.py`.
|
||||
|
||||
We saw the implementation of an on-object handler back in the [lesson about NPC and monster AI](./Beginner-Tutorial-AI.md#the-aihandler) (the `AIHandler`).
|
||||
|
||||
```{code-block} python
|
||||
:linenos:
|
||||
:emphasize-lines: 9,10,11,14-18,21,24-28
|
||||
# in evadventure/quests.py
|
||||
|
||||
class EvAdventureQuestHandler:
|
||||
quest_storage_attribute_key = "_quests"
|
||||
quest_storage_attribute_category = "evadventure"
|
||||
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
self.quest_classes = {}
|
||||
self.quests = {}
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
self.quest_classes = self.obj.attributes.get(
|
||||
self.quest_storage_attribute_key,
|
||||
category=self.quest_storage_attribute_category,
|
||||
default={},
|
||||
)
|
||||
# instantiate all quests
|
||||
for quest_key, quest_class in self.quest_classes.items():
|
||||
self.quests[quest_key] = quest_class(self.obj)
|
||||
|
||||
def _save(self):
|
||||
self.obj.attributes.add(
|
||||
self.quest_storage_attribute_key,
|
||||
self.quest_classes,
|
||||
category=self.quest_storage_attribute_category,
|
||||
)
|
||||
|
||||
def get(self, quest_key):
|
||||
return self.quests.get(quest_key)
|
||||
|
||||
def all(self):
|
||||
return list(self.quests.values())
|
||||
|
||||
def add(self, quest_class):
|
||||
self.quest_classes[quest_class.key] = quest_class
|
||||
self.quests[quest_class.key] = quest_class(self.obj)
|
||||
self._save()
|
||||
|
||||
def remove(self, quest_key):
|
||||
quest = self.quests.pop(quest_key, None)
|
||||
self.quest_classes.pop(quest_key, None)
|
||||
self.quests.pop(quest_key, None)
|
||||
self._save()
|
||||
|
||||
```
|
||||
|
||||
```{sidebar} Persistent handler pattern
|
||||
Persistent handlers are commonly used throughout Evennia. You can read more about them in the [Making a Persistent object Handler](../../Tutorial-Persistent-Handler.md) tutorial.
|
||||
```
|
||||
- **Line 9**: We know that the quests themselves will be Python classes inheriting from `EvAdventureQuest` (which we haven't created yet). We will store those classes in `self.quest_classes` on the handler. Note that there is a difference between a class and an _instance_ of a class! The class cannot hold any _state_ on its own, such as the status of that quest is for this particular character. The class only holds python code.
|
||||
- **Line 10**: We set aside another property on the handler - `self.quest` This is dictionary that will hold `EvAdventureQuest` _instances_.
|
||||
- **Line 11**: Note that we call the `self._load()` method here, this loads up data from the database whenever this handler is accessed.
|
||||
- **Lines 14-18**: We use `self.obj.attributes.get` to fetch an [Attribute](../../../Components/Attributes.md) on the Character named `_quests` and with a category of `evadventure`. If it doesn't exist yet (because we never started any quests), we just return an empty dict.
|
||||
- **Line 21**: Here we loop over all the classes and instantiate them. We haven't defined how these quest-classes look yet, but by instantiating them with `self.obj` (the Character) we should be covered - from the Character class the quest will be able to get to everything else (this handler itself will be accessible as `obj.quests` from that quest instance after all).
|
||||
- **Line 24**: Here we do the corresponding save operation.
|
||||
|
||||
The rest of the handler are just access methods for getting, adding and removing quests from the handler. We make one assumption in those code, namely that the quest class has a property `.key` being the unique quest-name.
|
||||
|
||||
This is how it would be used in practice:
|
||||
|
||||
```python
|
||||
# in some questing code
|
||||
|
||||
from evennia import search_object
|
||||
from evadventure import quests
|
||||
|
||||
class EvAdventureSuperQuest(quests.EvAdventureQuest):
|
||||
key = "superquest"
|
||||
# quest implementation here
|
||||
|
||||
def start_super_quest(character):
|
||||
character.quests.add(EvAdventureSuperQuest)
|
||||
|
||||
```
|
||||
```{sidebar} What can be saved in Attributes?
|
||||
For more details, see [the Attributes documentation](../../../Components/Attributes.md#what-types-of-data-can-i-save-in-an-attribute) on the matter.
|
||||
```
|
||||
We chose to store classes and not instances of classes above. The reason for this has to do with what can be stored in a database `Attribute` - one limitation of an Attribute is that we can't save a class instance _with other database entities baked inside it_. If we saved quest instances as-is, it's highly likely they'd contain database entities 'hidden' inside them - a reference to the Character, maybe to objects required for the quest to be complete etc. Evennia would fail trying to save that data.
|
||||
Instead we store only the classes, instantiate those classes with the Character, and let the quest store its state flags separately, like this:
|
||||
|
||||
```python
|
||||
# in evadventure/quests.py
|
||||
|
||||
class EvAdventureQuestHandler:
|
||||
|
||||
# ...
|
||||
quest_data_attribute_template = "_quest_data_{quest_key}"
|
||||
quest_data_attribute_category = "evadventure"
|
||||
|
||||
# ...
|
||||
|
||||
def save_quest_data(self, quest_key):
|
||||
quest = self.get(quest_key)
|
||||
if quest:
|
||||
self.obj.attributes.add(
|
||||
self.quest_data_attribute_template.format(quest_key=quest_key),
|
||||
quest.data,
|
||||
category=self.quest_data_attribute_category,
|
||||
)
|
||||
|
||||
def load_quest_data(self, quest_key):
|
||||
return self.obj.attributes.get(
|
||||
self.quest_data_attribute_template.format(quest_key=quest_key),
|
||||
category=self.quest_data_attribute_category,
|
||||
default={},
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
This works the same as the `_load` and `_save` methods, except it fetches a property `.data` (this will be a `dict`) on the quest instance and save it. As long as we make sure to call these methods from the quest the quest whenever that `.data` property is changed, all will be well - this is because Attributes know how to properly analyze a `dict` to find and safely serialize any database entities found within.
|
||||
|
||||
Our handler is ready. We created the `EvAdventureCharacter` class back in the [Character lesson](./Beginner-Tutorial-Characters.md) - let's add quest-support to it.
|
||||
|
||||
```python
|
||||
# in evadventure/characters.py
|
||||
|
||||
# ...
|
||||
|
||||
from evennia.utils import lazy_property
|
||||
from evadventure.quests import EvAdventureQuestHandler
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
# ...
|
||||
|
||||
@lazy_property
|
||||
def quests(self):
|
||||
return EvAdventureQuestHandler(self)
|
||||
|
||||
# ...
|
||||
|
||||
```
|
||||
|
||||
We also need a way to represent the quests themselves though!
|
||||
## The Quest class
|
||||
|
||||
|
||||
```{code-block} python
|
||||
:linenos:
|
||||
:emphasize-lines: 7,12,13,34-36
|
||||
# in evadventure/quests.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureQuest:
|
||||
|
||||
key = "base-quest"
|
||||
desc = "Base quest"
|
||||
start_step = "start"
|
||||
|
||||
def __init__(self, quester):
|
||||
self.quester = quester
|
||||
self.data = self.questhandler.load_quest_data(self.key)
|
||||
self._current_step = self.get_data("current_step")
|
||||
|
||||
if not self.current_step:
|
||||
self.current_step = self.start_step
|
||||
|
||||
def add_data(self, key, value):
|
||||
self.data[key] = value
|
||||
self.questhandler.save_quest_data(self.key)
|
||||
|
||||
def get_data(self, key, default=None):
|
||||
return self.data.get(key, default)
|
||||
|
||||
def remove_data(self, key):
|
||||
self.data.pop(key, None)
|
||||
self.questhandler.save_quest_data(self.key)
|
||||
|
||||
@property
|
||||
def questhandler(self):
|
||||
return self.quester.quests
|
||||
|
||||
@property
|
||||
def current_step(self):
|
||||
return self._current_step
|
||||
|
||||
@current_step.setter
|
||||
def current_step(self, step_name):
|
||||
self._current_step = step_name
|
||||
self.add_data("current_step", step_name)
|
||||
|
||||
```
|
||||
|
||||
- **Line 7**: Each class must have a `.key` property unquely identifying the quest. We depend on this in the quest-handler.
|
||||
- **Line 12**: `quester` (the Character) is passed into this class when it is initiated inside `EvAdventureQuestHandler._load()`.
|
||||
- **Line 13**: We load the quest data into `self.data` directly using the `questhandler.load_quest-data` method (which in turn loads it from an Attribute on the Character). Note that the `.questhandler` property is defined on **lines 34-36** as a shortcut to get to the handler.
|
||||
|
||||
The `add/get/remove_data` methods are convenient wrappers for getting data in and out of the database using the matching methods on the handler. When we implement a quest we should prefer to use `.get_data`, `add_data` and `remove_data` over manipulating `.data` directly, since the former will make sure to save said that to the database automatically.
|
||||
|
||||
The `current_step` tracks the current quest 'step' we are in; what this means is up to each Quest. We set up convenient properties for setting the `current_state` and also make sure to save it in the data dict as "current_step".
|
||||
|
||||
The quest can have a few possible statuses: "started", "completed", "abandoned" and "failed". We create a few properties and methods for easily control that, while saving everything under the hood:
|
||||
|
||||
```python
|
||||
# in evadventure/quests.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureQuest:
|
||||
|
||||
# ...
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self.get_data("status", "started")
|
||||
|
||||
@status.setter
|
||||
def status(self, value):
|
||||
self.add_data("status", value)
|
||||
|
||||
@property
|
||||
def is_completed(self):
|
||||
return self.status == "completed"
|
||||
|
||||
@property
|
||||
def is_abandoned(self):
|
||||
return self.status == "abandoned"
|
||||
|
||||
@property
|
||||
def is_failed(self):
|
||||
return self.status == "failed"
|
||||
|
||||
def complete(self):
|
||||
self.status = "completed"
|
||||
|
||||
def abandon(self):
|
||||
self.status = "abandoned"
|
||||
|
||||
def fail(self):
|
||||
self.status = "failed"
|
||||
|
||||
|
||||
```
|
||||
|
||||
So far we have only added convenience functions for checking statuses. How will the actual "quest" aspect of this work?
|
||||
|
||||
What will happen when the system wants to check the progress of the quest, is that it will call a method `.progress()` on this class. Similarly, to get help for the current step, it will call a method `.help()`
|
||||
|
||||
```python
|
||||
|
||||
start_step = "start"
|
||||
|
||||
# help entries for quests (could also be methods)
|
||||
help_start = "You need to start first"
|
||||
help_end = "You need to end the quest"
|
||||
|
||||
def progress(self, *args, **kwargs):
|
||||
getattr(self, f"step_{self.current_step}")(*args, **kwargs)
|
||||
|
||||
def help(self, *args, **kwargs):
|
||||
if self.status in ("abandoned", "completed", "failed"):
|
||||
help_resource = getattr(self, f"help_{self.status}",
|
||||
f"You have {self.status} this quest.")
|
||||
else:
|
||||
help_resource = getattr(self, f"help_{self.current_step}", "No help available.")
|
||||
|
||||
if callable(help_resource):
|
||||
# the help_* methods can be used to dynamically generate help
|
||||
return help_resource(*args, **kwargs)
|
||||
else:
|
||||
# normally it's just a string
|
||||
return str(help_resource)
|
||||
|
||||
```
|
||||
|
||||
```{sidebar} What's with the *args, **kwargs?
|
||||
These are optional, but allow you to pass extra information into your quest-check. This could be very powerful if you want to add extra context to determine if a quest-step is currently complete or not.
|
||||
```
|
||||
Calling the `.progress(*args, **kwargs)` method will call a method named `step_<current_step>(*args, **kwargs)` on this class. That is, if we are on the _start_ step, the method called will be `self.step_start(*args, **kwargs)`. Where is this method? It has not been implemented yet! In fact, it's up to us to implement methods like this for each quest. By just adding a correctly added method, we will easily be able to add more steps to a quest.
|
||||
|
||||
Similarly, calling `.help(*args, **kwargs)` will try to find a property `help_<current_step>`. If this is a callable, it will be called as for example `self.help_start(*args, **kwargs)`. If it is given as a string, then the string will be returned as-is and the `*args, **kwargs` will be ignored.
|
||||
|
||||
### Example quest
|
||||
|
||||
```python
|
||||
# in some quest module, like world/myquests.py
|
||||
|
||||
from evadventure.quests import EvAdventureQuest
|
||||
|
||||
class ShortQuest(EvAdventureQuest):
|
||||
|
||||
key = "simple-quest"
|
||||
desc = "A very simple quest."
|
||||
|
||||
def step_start(self, *args, **kwargs):
|
||||
"""Example step!"""
|
||||
self.quester.msg("Quest started!")
|
||||
self.current_step = "end"
|
||||
|
||||
def step_end(self, *args, **kwargs):
|
||||
if not self.is_completed:
|
||||
self.quester.msg("Quest ended!")
|
||||
self.complete()
|
||||
|
||||
```
|
||||
|
||||
This is a very simple quest that will resolve on its own after two `.progress()` checks. Here's the full life cycle of this quest:
|
||||
|
||||
```python
|
||||
# in some module somewhere, using evennia shell or in-game using py
|
||||
|
||||
from evennia import search_object
|
||||
from world.myquests import ShortQuest
|
||||
|
||||
character = search_object("MyCharacterName")[0]
|
||||
character.quests.add(ShortQuest)
|
||||
|
||||
# this will echo "Quest started!" to character
|
||||
character.quests.get("short-quest").progress()
|
||||
# this will echo "Quest ended!" to character
|
||||
character.quests.get("short-quest").progress()
|
||||
|
||||
```
|
||||
|
||||
### A useful Command
|
||||
|
||||
The player must know which quests they have and be able to inspect them. Here's a simple `quests` command to handle this:
|
||||
|
||||
```python
|
||||
# in evadventure/quests.py
|
||||
|
||||
class CmdQuests(Command):
|
||||
"""
|
||||
List all quests and their statuses as well as get info about the status of
|
||||
a specific quest.
|
||||
|
||||
Usage:
|
||||
quests
|
||||
quest <questname>
|
||||
|
||||
"""
|
||||
key = "quests"
|
||||
aliases = ["quest"]
|
||||
|
||||
def parse(self):
|
||||
self.quest_name = self.args.strip()
|
||||
|
||||
def func(self):
|
||||
if self.quest_name:
|
||||
quest = self.caller.quests.get(self.quest_name)
|
||||
if not quest:
|
||||
self.msg(f"Quest {self.quest_name} not found.")
|
||||
return
|
||||
self.msg(f"Quest {quest.key}: {quest.status}\n{quest.help()}")
|
||||
return
|
||||
|
||||
quests = self.caller.quests.all()
|
||||
if not quests:
|
||||
self.msg("No quests.")
|
||||
return
|
||||
|
||||
for quest in quests:
|
||||
self.msg(f"Quest {quest.key}: {quest.status}")
|
||||
```
|
||||
|
||||
Add this to the `CharacterCmdSet` in `mygame/commands/default_cmdsets.py`. Follow the [Adding a command lesson](../Part1/Beginner-Tutorial-Adding-Commands.md#add-the-echo-command-to-the-default-cmdset) if you are unsure how to do this. Reload and if you are playing as an `EvAdventureCharacter` you should be able to use `quests` to view your quests.
|
||||
|
||||
## Testing
|
||||
|
||||
> Create a new folder `evadventure/tests/test_quests.py`.
|
||||
|
||||
```{sidebar}
|
||||
An example test suite for quests is found in `evennia/contrib/tutorials/evadventure`, as [tests/test_quests.py](evennia.contrib.tutorials.evadventure.tests.test_quests).
|
||||
```
|
||||
Testing of the quests means creating a test character, making a dummy quest, add it to the character's quest handler and making sure all methods work correcly. Create the testing quest so that it will automatically step forward when calling `.progress()`, so you can make sure it works as intended.
|
||||
|
||||
## Conclusions
|
||||
|
||||
What we created here is just the framework for questing. The actual complexity will come when creating the quests themselves (that is, implementing the `step_<current_step>(*args, **kwargs)` methods), which is something we'll get to later, in [Part 4](../Part4/Beginner-Tutorial-Part4-Overview.md) of this tutorial.
|
||||
|
|
@ -31,6 +31,7 @@ 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.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# Changing Game Settings
|
||||
|
||||
Evennia runs out of the box without any changes to its settings. But there are several important
|
||||
ways to customize the server and expand it with your own plugins.
|
||||
Evennia runs out of the box without any changes to its settings. But there are several important ways to customize the server and expand it with your own plugins.
|
||||
|
||||
All game-specific settings are located in the `mygame/server/conf/` directory.
|
||||
|
||||
|
|
@ -17,13 +16,9 @@ heavily documented and up-to-date, so you should refer to this file directly for
|
|||
Since `mygame/server/conf/settings.py` is a normal Python module, it simply imports
|
||||
`evennia/settings_default.py` into itself at the top.
|
||||
|
||||
This means that if any setting you want to change were to depend on some *other* default setting,
|
||||
you might need to copy & paste both in order to change them and get the effect you want (for most
|
||||
commonly changed settings, this is not something you need to worry about).
|
||||
This means that if any setting you want to change were to depend on some *other* default setting, you might need to copy & paste both in order to change them and get the effect you want (for most commonly changed settings, this is not something you need to worry about).
|
||||
|
||||
You should never edit `evennia/settings_default.py`. Rather you should copy&paste the select
|
||||
variables you want to change into your `settings.py` and edit them there. This will overload the
|
||||
previously imported defaults.
|
||||
You should never edit `evennia/settings_default.py`. Rather you should copy&paste the select variables you want to change into your `settings.py` and edit them there. This will overload the previously imported defaults.
|
||||
|
||||
```{warning} Don't copy everything!
|
||||
It may be tempting to copy *everything* from `settings_default.py` into your own settings file just to have it all in one place. Don't do this. By copying only what you need, you can easier track what you changed.
|
||||
|
|
@ -41,45 +36,24 @@ In code, the settings is accessed through
|
|||
|
||||
Each setting appears as a property on the imported `settings` object. You can also explore all possible options with `evennia.settings_full` (this also includes advanced Django defaults that are not touched in default Evennia).
|
||||
|
||||
> When importing `settings` into your code like this, it will be *read
|
||||
only*. You *cannot* edit your settings from your code! The only way to change an Evennia setting is
|
||||
to edit `mygame/server/conf/settings.py` directly. You will also need to restart the server
|
||||
(possibly also the Portal) before a changed setting becomes available.
|
||||
> When importing `settings` into your code like this, it will be *read only*. You *cannot* edit your settings from your code! The only way to change an Evennia setting is to edit `mygame/server/conf/settings.py` directly. You will also need to restart the server (possibly also the Portal) before a changed setting becomes available.
|
||||
|
||||
## Other files in the `server/conf` directory
|
||||
|
||||
Apart from the main `settings.py` file,
|
||||
|
||||
- `at_initial_setup.py` - this allows you to add a custom startup method to be called (only) the
|
||||
very first time Evennia starts (at the same time as user #1 and Limbo is created). It can be made to
|
||||
start your own global scripts or set up other system/world-related things your game needs to have
|
||||
running from the start.
|
||||
- `at_server_startstop.py` - this module contains two functions that Evennia will call every time
|
||||
the Server starts and stops respectively - this includes stopping due to reloading and resetting as
|
||||
well as shutting down completely. It's a useful place to put custom startup code for handlers and
|
||||
other things that must run in your game but which has no database persistence.
|
||||
- `connection_screens.py` - all global string variables in this module are interpreted by Evennia as
|
||||
a greeting screen to show when an Account first connects. If more than one string variable is
|
||||
present in the module a random one will be picked.
|
||||
- `at_initial_setup.py` - this allows you to add a custom startup method to be called (only) the very first time Evennia starts (at the same time as user #1 and Limbo is created). It can be made to start your own global scripts or set up other system/world-related things your game needs to have running from the start.
|
||||
- `at_server_startstop.py` - this module contains functions that Evennia will call every time the Server starts and stops respectively - this includes stopping due to reloading and resetting as well as shutting down completely. It's a useful place to put custom startup code for handlers and other things that must run in your game but which has no database persistence.
|
||||
- `connection_screens.py` - all global string variables in this module are interpreted by Evennia as a greeting screen to show when an Account first connects. If more than one string variable is present in the module a random one will be picked.
|
||||
- `inlinefuncs.py` - this is where you can define custom [FuncParser functions](../Components/FuncParser.md).
|
||||
- `inputfuncs.py` - this is where you define custom [Input functions](../Components/Inputfuncs.md) to handle data
|
||||
from the client.
|
||||
- `lockfuncs.py` - this is one of many possible modules to hold your own "safe" *lock functions* to
|
||||
make available to Evennia's [Locks](../Components/Locks.md).
|
||||
- `mssp.py` - this holds meta information about your game. It is used by MUD search engines (which
|
||||
you often have to register with) in order to display what kind of game you are running along with
|
||||
statistics such as number of online accounts and online status.
|
||||
- `inputfuncs.py` - this is where you define custom [Input functions](../Components/Inputfuncs.md) to handle data from the client.
|
||||
- `lockfuncs.py` - this is one of many possible modules to hold your own "safe" *lock functions* to make available to Evennia's [Locks](../Components/Locks.md).
|
||||
- `mssp.py` - this holds meta information about your game. It is used by MUD search engines (which you often have to register with) in order to display what kind of game you are running along with statistics such as number of online accounts and online status.
|
||||
- `oobfuncs.py` - in here you can define custom [OOB functions](../Concepts/OOB.md).
|
||||
- `portal_services_plugin.py` - this allows for adding your own custom services/protocols to the
|
||||
Portal. It must define one particular function that will be called by Evennia at startup. There can
|
||||
be any number of service plugin modules, all will be imported and used if defined. More info can be
|
||||
found [here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols).
|
||||
- `server_services_plugin.py` - this is equivalent to the previous one, but used for adding new
|
||||
services to the Server instead. More info can be found
|
||||
[here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols).
|
||||
- `portal_services_plugin.py` - this allows for adding your own custom services/protocols to the Portal. It must define one particular function that will be called by Evennia at startup. There can be any number of service plugin modules, all will be imported and used if defined. More info can be found [here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols).
|
||||
- `server_services_plugin.py` - this is equivalent to the previous one, but used for adding new services to the Server instead. More info can be found [here](https://code.google.com/p/evennia/wiki/SessionProtocols#Adding_custom_Protocols).
|
||||
|
||||
Some other Evennia systems can be customized by plugin modules but has no explicit template in
|
||||
`conf/`:
|
||||
Some other Evennia systems can be customized by plugin modules but has no explicit template in `conf/`:
|
||||
|
||||
- *cmdparser.py* - a custom module can be used to totally replace Evennia's default command parser. All this does is to split the incoming string into "command name" and "the rest". It also handles things like error messages for no-matches and multiple-matches among other things that makes this more complex than it sounds. The default parser is *very* generic, so you are most often best served by modifying things further down the line (on the command parse level) than here.
|
||||
- *at_search.py* - this allows for replacing the way Evennia handles search results. It allows to change how errors are echoed and how multi-matches are resolved and reported (like how the default understands that "2-ball" should match the second "ball" object if there are two of them in the room).
|
||||
|
|
@ -374,8 +374,12 @@ def setup(app):
|
|||
|
||||
# build toctree file
|
||||
sys.path.insert(1, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
||||
from docs.pylib import (auto_link_remapper, contrib_readmes2docs,
|
||||
update_default_cmd_index, update_dynamic_pages)
|
||||
from docs.pylib import (
|
||||
auto_link_remapper,
|
||||
contrib_readmes2docs,
|
||||
update_default_cmd_index,
|
||||
update_dynamic_pages,
|
||||
)
|
||||
|
||||
_no_autodoc = os.environ.get("NOAUTODOC")
|
||||
update_default_cmd_index.run_update(no_autodoc=_no_autodoc)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
4.0.0
|
||||
4.1.1
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ to launch such a shell (using python or ipython depending on your install).
|
|||
See www.evennia.com for full documentation.
|
||||
|
||||
"""
|
||||
|
||||
import evennia
|
||||
|
||||
# docstring header
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ character object, so you should customize that
|
|||
instead for most things).
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
import typing
|
||||
|
|
@ -372,6 +373,18 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
session.protocol_flags.get("SCREENREADER") for session in self.sessions.all()
|
||||
)
|
||||
|
||||
def get_extra_display_name_info(self, looker, **kwargs):
|
||||
"""
|
||||
Used in .get_display_name() to provide extra information to the looker. We split this
|
||||
to be consistent with the Object version of this method.
|
||||
|
||||
This is used e.g. by the `find` command by default.
|
||||
|
||||
"""
|
||||
if looker and self.locks.check_lockstring(looker, "perm(Admin)"):
|
||||
return f"(#{self.id})"
|
||||
return ""
|
||||
|
||||
def get_display_name(self, looker, **kwargs):
|
||||
"""
|
||||
This is used by channels and other OOC communications methods to give a
|
||||
|
|
@ -1334,7 +1347,8 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
if isinstance(searchdata, str):
|
||||
# handle wrapping of common terms
|
||||
if searchdata.lower() in ("me", "*me", "self", "*self"):
|
||||
return self
|
||||
return [self] if quiet else self
|
||||
|
||||
searchdata = self.nicks.nickreplace(
|
||||
searchdata, categories=("account",), include_account=False
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ persistently store attributes of its own. This is ideal for extra
|
|||
account info and OOC account configuration variables etc.
|
||||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ same inputs as the default one.
|
|||
|
||||
"""
|
||||
|
||||
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ Set theory.
|
|||
to affect the low-priority cmdset. Ex: A1,A3 + B1,B2,B4,B5 = B2,B4,B5
|
||||
|
||||
"""
|
||||
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ example, you can have a 'On a boat' set, onto which you then tack on
|
|||
the 'Fishing' set. Fishing from a boat? No problem!
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
from importlib import import_module
|
||||
from inspect import trace
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ The base Command class.
|
|||
All commands in Evennia inherit from the 'Command' class in this module.
|
||||
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import math
|
||||
import re
|
||||
|
|
@ -21,7 +22,6 @@ CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES
|
|||
|
||||
|
||||
class InterruptCommand(Exception):
|
||||
|
||||
"""Cleanly interrupt a command."""
|
||||
|
||||
pass
|
||||
|
|
@ -487,31 +487,29 @@ class Command(metaclass=CommandMeta):
|
|||
purposes when making commands.
|
||||
|
||||
"""
|
||||
variables = "\n".join(
|
||||
" |w{}|n ({}): {}".format(key, type(val), val) for key, val in self.__dict__.items()
|
||||
)
|
||||
string = f"""
|
||||
Command {self} has no defined `func()` - showing on-command variables:
|
||||
{variables}
|
||||
"""
|
||||
# a simple test command to show the available properties
|
||||
string += "-" * 50
|
||||
string += "\n|w%s|n - Command variables from evennia:\n" % self.key
|
||||
string += "-" * 50
|
||||
string += "\nname of cmd (self.key): |w%s|n\n" % self.key
|
||||
string += "cmd aliases (self.aliases): |w%s|n\n" % self.aliases
|
||||
string += "cmd locks (self.locks): |w%s|n\n" % self.locks
|
||||
string += "help category (self.help_category): |w%s|n\n" % self.help_category.capitalize()
|
||||
string += "object calling (self.caller): |w%s|n\n" % self.caller
|
||||
string += "object storing cmdset (self.obj): |w%s|n\n" % self.obj
|
||||
string += "command string given (self.cmdstring): |w%s|n\n" % self.cmdstring
|
||||
# show cmdset.key instead of cmdset to shorten output
|
||||
string += fill(
|
||||
"current cmdset (self.cmdset): |w%s|n\n"
|
||||
% (self.cmdset.key if self.cmdset.key else self.cmdset.__class__)
|
||||
)
|
||||
output_string = """
|
||||
Command \"{cmdname}\" has no defined `func()` method. Available properties on this command are:
|
||||
|
||||
self.msg(string)
|
||||
{variables}"""
|
||||
variables = [
|
||||
" |w{}|n ({}): {}".format(key, type(val), f'"{val}"' if isinstance(val, str) else val)
|
||||
for key, val in (
|
||||
("self.key", self.key),
|
||||
("self.cmdname", self.cmdstring),
|
||||
("self.raw_cmdname", self.raw_cmdname),
|
||||
("self.raw_string", self.raw_string),
|
||||
("self.aliases", self.aliases),
|
||||
("self.args", self.args),
|
||||
("self.caller", self.caller),
|
||||
("self.obj", self.obj),
|
||||
("self.session", self.session),
|
||||
("self.locks", self.locks),
|
||||
("self.help_category", self.help_category),
|
||||
("self.cmdset", self.cmdset),
|
||||
)
|
||||
]
|
||||
output = output_string.format(cmdname=self.key, variables="\n ".join(variables))
|
||||
self.msg(output)
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ self.msg() and similar methods to reroute returns to the correct
|
|||
method. Otherwise all text will be returned to all connected sessions.
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
from codecs import lookup as codecs_lookup
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ the Evennia API. It is also a severe security risk and should
|
|||
therefore always be limited to superusers only.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
"""
|
||||
Building and world design commands
|
||||
"""
|
||||
|
||||
import re
|
||||
import typing
|
||||
|
||||
import evennia
|
||||
from django.conf import settings
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Max, Min, Q
|
||||
|
||||
import evennia
|
||||
from evennia import InterruptCommand
|
||||
from evennia.commands.cmdhandler import (generate_cmdset_providers,
|
||||
get_and_merge_cmdsets)
|
||||
from evennia.commands.cmdhandler import generate_cmdset_providers, get_and_merge_cmdsets
|
||||
from evennia.locks.lockhandler import LockException
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.prototypes import menus as olc_menus
|
||||
|
|
@ -23,10 +24,18 @@ from evennia.utils.dbserialize import deserialize
|
|||
from evennia.utils.eveditor import EvEditor
|
||||
from evennia.utils.evmore import EvMore
|
||||
from evennia.utils.evtable import EvTable
|
||||
from evennia.utils.utils import (class_from_module, crop, dbref, display_len,
|
||||
format_grid, get_all_typeclasses,
|
||||
inherits_from, interactive, list_to_string,
|
||||
variable_from_module)
|
||||
from evennia.utils.utils import (
|
||||
class_from_module,
|
||||
crop,
|
||||
dbref,
|
||||
display_len,
|
||||
format_grid,
|
||||
get_all_typeclasses,
|
||||
inherits_from,
|
||||
interactive,
|
||||
list_to_string,
|
||||
variable_from_module,
|
||||
)
|
||||
|
||||
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||
|
||||
|
|
@ -68,7 +77,7 @@ __all__ = (
|
|||
)
|
||||
|
||||
# used by set
|
||||
from ast import literal_eval as _LITERAL_EVAL
|
||||
from ast import literal_eval as _LITERAL_EVAL # noqa
|
||||
|
||||
LIST_APPEND_CHAR = "+"
|
||||
|
||||
|
|
@ -218,10 +227,13 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
|||
alias <obj> [= [alias[,alias,alias,...]]]
|
||||
alias <obj> =
|
||||
alias/category <obj> = [alias[,alias,...]:<category>
|
||||
alias/delete <obj> = <alias>
|
||||
|
||||
Switches:
|
||||
category - requires ending input with :category, to store the
|
||||
given aliases with the given category.
|
||||
delete - deletes all occurrences of the given alias, regardless
|
||||
of category
|
||||
|
||||
Assigns aliases to an object so it can be referenced by more
|
||||
than one name. Assign empty to remove all aliases from object. If
|
||||
|
|
@ -235,7 +247,7 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "@alias"
|
||||
aliases = "setobjalias"
|
||||
switch_options = ("category",)
|
||||
switch_options = ("category", "delete")
|
||||
locks = "cmd:perm(setobjalias) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -252,12 +264,12 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
objname = self.lhs
|
||||
|
||||
# Find the object to receive aliases
|
||||
# Find the object to receive/delete aliases
|
||||
obj = caller.search(objname)
|
||||
if not obj:
|
||||
return
|
||||
if self.rhs is None:
|
||||
# no =, so we just list aliases on object.
|
||||
if self.rhs is None and "delete" not in self.switches:
|
||||
# no =, and not deleting, so we just list aliases on object.
|
||||
aliases = obj.aliases.all(return_key_and_category=True)
|
||||
if aliases:
|
||||
caller.msg(
|
||||
|
|
@ -280,7 +292,9 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
|
||||
if not self.rhs:
|
||||
# we have given an empty =, so delete aliases
|
||||
# we have given an empty =, so delete aliases.
|
||||
# as a side-effect, 'alias/delete obj' and 'alias/delete obj='
|
||||
# will also be caught here, which is fine
|
||||
old_aliases = obj.aliases.all()
|
||||
if old_aliases:
|
||||
caller.msg(
|
||||
|
|
@ -292,6 +306,19 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
|||
caller.msg("No aliases to clear.")
|
||||
return
|
||||
|
||||
if "delete" in self.switches:
|
||||
# delete all matching keys, regardless of category
|
||||
existed = False
|
||||
for key, category in obj.aliases.all(return_key_and_category=True):
|
||||
if key == self.rhs:
|
||||
obj.aliases.remove(key=self.rhs, category=category)
|
||||
existed = True
|
||||
if existed:
|
||||
caller.msg("Alias '%s' deleted from %s." % (self.rhs, obj.get_display_name(caller)))
|
||||
else:
|
||||
caller.msg("%s has no alias '%s'." % (obj.get_display_name(caller), self.rhs))
|
||||
return
|
||||
|
||||
category = None
|
||||
if "category" in self.switches:
|
||||
if ":" in self.rhs:
|
||||
|
|
@ -1378,7 +1405,7 @@ class CmdSetHome(CmdLink):
|
|||
obj.home = new_home
|
||||
if old_home:
|
||||
string = (
|
||||
f"Home location of {obj} was changed from {old_home}({old_home.dbref} to"
|
||||
f"Home location of {obj} was changed from {old_home}({old_home.dbref}) to"
|
||||
f" {new_home}({new_home.dbref})."
|
||||
)
|
||||
else:
|
||||
|
|
@ -2938,9 +2965,9 @@ class CmdExamine(ObjManipCommand):
|
|||
):
|
||||
objdata["Stored Cmdset(s)"] = self.format_stored_cmdsets(obj)
|
||||
objdata["Merged Cmdset(s)"] = self.format_merged_cmdsets(obj, current_cmdset)
|
||||
objdata[
|
||||
f"Commands available to {obj.key} (result of Merged Cmdset(s))"
|
||||
] = self.format_current_cmds(obj, current_cmdset)
|
||||
objdata[f"Commands available to {obj.key} (result of Merged Cmdset(s))"] = (
|
||||
self.format_current_cmds(obj, current_cmdset)
|
||||
)
|
||||
if self.object_type == "script":
|
||||
objdata["Description"] = self.format_script_desc(obj)
|
||||
objdata["Persistent"] = self.format_script_is_persistent(obj)
|
||||
|
|
@ -3255,9 +3282,15 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
|||
string += f"\n |RNo match found for '{searchstring}' in #dbref interval.|n"
|
||||
else:
|
||||
result = result[0]
|
||||
string += f"\n|g {result.get_display_name(caller)} - {result.path}|n"
|
||||
string += (
|
||||
f"\n|g {result.get_display_name(caller)}"
|
||||
f"{result.get_extra_display_name_info(caller)} - {result.path}|n"
|
||||
)
|
||||
if "loc" in self.switches and not is_account and result.location:
|
||||
string += f" (|wlocation|n: |g{result.location.get_display_name(caller)}|n)"
|
||||
string += (
|
||||
f" (|wlocation|n: |g{result.location.get_display_name(caller)}"
|
||||
f"{result.get_extra_display_name_info(caller)}|n)"
|
||||
)
|
||||
else:
|
||||
# Not an account/dbref search but a wider search; build a queryset.
|
||||
# Searches for key and aliases
|
||||
|
|
@ -3397,9 +3430,11 @@ class ScriptEvMore(EvMore):
|
|||
|
||||
table.add_row(
|
||||
f"#{script.id}",
|
||||
f"{script.obj.key}({script.obj.dbref})"
|
||||
if (hasattr(script, "obj") and script.obj)
|
||||
else "<Global>",
|
||||
(
|
||||
f"{script.obj.key}({script.obj.dbref})"
|
||||
if (hasattr(script, "obj") and script.obj)
|
||||
else "<Global>"
|
||||
),
|
||||
script.key,
|
||||
script.interval if script.interval > 0 else "--",
|
||||
nextrep,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ available (i.e. IC commands). Note that some commands, such as
|
|||
communication-commands are instead put on the account level, in the
|
||||
Account cmdset. Account commands remain available also to Characters.
|
||||
"""
|
||||
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.commands.default import (
|
||||
admin,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
This module stores session-level commands.
|
||||
"""
|
||||
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.commands.default import account
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ This module describes the unlogged state of the default game.
|
|||
The setting STATE_UNLOGGED should be set to the python path
|
||||
of the state instance in this module.
|
||||
"""
|
||||
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.commands.default import unloggedin
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ Communication commands:
|
|||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
|
||||
from evennia.accounts import bots
|
||||
from evennia.accounts.models import AccountDB
|
||||
|
|
@ -1338,8 +1339,24 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
# get the messages we've sent (not to channels)
|
||||
pages_we_sent = Msg.objects.get_messages_by_sender(caller).order_by("-db_date_created")
|
||||
# get only messages tagged as pages or not tagged at all (legacy pages)
|
||||
pages_we_sent = pages_we_sent.filter(
|
||||
Q(db_tags__db_key__iexact="page", db_tags__db_category__iexact="comms")
|
||||
| Q(db_tags__isnull=True)
|
||||
)
|
||||
# we need to default to True to allow for legacy pages
|
||||
pages_we_sent = [msg for msg in pages_we_sent if msg.access(caller, "read", default=True)]
|
||||
|
||||
# get last messages we've got
|
||||
pages_we_got = Msg.objects.get_messages_by_receiver(caller).order_by("-db_date_created")
|
||||
pages_we_got = pages_we_got.filter(
|
||||
Q(db_tags__db_key__iexact="page", db_tags__db_category__iexact="comms")
|
||||
| Q(db_tags__isnull=True)
|
||||
)
|
||||
# we need to default to True to allow for legacy pages
|
||||
pages_we_got = [msg for msg in pages_we_got if msg.access(caller, "read", default=True)]
|
||||
|
||||
# get only messages tagged as pages or not tagged at all (legacy pages)
|
||||
targets, message, number = [], None, None
|
||||
|
||||
if "last" in self.switches:
|
||||
|
|
@ -1360,6 +1377,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
|
|||
targets.append(target_obj)
|
||||
message = self.rhs.strip()
|
||||
else:
|
||||
# no = sign, handler this as well
|
||||
target, *message = self.args.split(" ", 1)
|
||||
if target and target.isnumeric():
|
||||
# a number to specify a historic page
|
||||
|
|
@ -1395,7 +1413,17 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
|
|||
message = f"{caller.key} {message.strip(':').strip()}"
|
||||
|
||||
# create the persistent message object
|
||||
create.create_message(caller, message, receivers=targets)
|
||||
create.create_message(
|
||||
caller,
|
||||
message,
|
||||
receivers=targets,
|
||||
locks=(
|
||||
f"read:id({caller.id}) or perm(Admin);"
|
||||
f"delete:id({caller.id}) or perm(Admin);"
|
||||
f"edit:id({caller.id}) or perm(Admin)"
|
||||
),
|
||||
tags=[("page", "comms")],
|
||||
)
|
||||
|
||||
# tell the accounts they got a message.
|
||||
received = []
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
"""
|
||||
General Character commands usually available to all characters
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
import evennia
|
||||
from django.conf import settings
|
||||
|
||||
import evennia
|
||||
from evennia.typeclasses.attributes import NickTemplateInvalid
|
||||
from evennia.utils import utils
|
||||
|
||||
|
|
@ -378,20 +380,57 @@ class CmdInventory(COMMAND_DEFAULT_CLASS):
|
|||
self.msg(text=(string, {"type": "inventory"}))
|
||||
|
||||
|
||||
class CmdGet(COMMAND_DEFAULT_CLASS):
|
||||
class NumberedTargetCommand(COMMAND_DEFAULT_CLASS):
|
||||
"""
|
||||
A class that parses out an optional number component from the input string. This
|
||||
class is intended to be inherited from to provide additional functionality, rather
|
||||
than used on its own.
|
||||
"""
|
||||
|
||||
def parse(self):
|
||||
"""
|
||||
Parser that extracts a `.number` property from the beginning of the input string.
|
||||
|
||||
For example, if the input string is "3 apples", this parser will set `self.number = 3` and
|
||||
`self.args = "apples"`. If the input string is "apples", this parser will set
|
||||
`self.number = 0` and `self.args = "apples"`.
|
||||
|
||||
"""
|
||||
super().parse()
|
||||
self.number = 0
|
||||
if getattr(self, "lhs", None):
|
||||
# handle self.lhs but don't require it
|
||||
count, *args = self.lhs.split(maxsplit=1)
|
||||
# we only use the first word as a count if it's a number and
|
||||
# there is more text afterwards
|
||||
if args and count.isdecimal():
|
||||
self.number = int(count)
|
||||
self.lhs = args[0]
|
||||
if self.args:
|
||||
# check for numbering
|
||||
count, *args = self.args.split(maxsplit=1)
|
||||
# we only use the first word as a count if it's a number and
|
||||
# there is more text afterwards
|
||||
if args and count.isdecimal():
|
||||
self.args = args[0]
|
||||
# we only re-assign self.number if it wasn't already taken from self.lhs
|
||||
if not self.number:
|
||||
self.number = int(count)
|
||||
|
||||
|
||||
class CmdGet(NumberedTargetCommand):
|
||||
"""
|
||||
pick up something
|
||||
|
||||
Usage:
|
||||
get <obj>
|
||||
|
||||
Picks up an object from your location and puts it in
|
||||
your inventory.
|
||||
Picks up an object from your location and puts it in your inventory.
|
||||
"""
|
||||
|
||||
key = "get"
|
||||
aliases = "grab"
|
||||
locks = "cmd:all();view:perm(Developer);read:perm(Developer)"
|
||||
locks = "cmd:all()"
|
||||
arg_regex = r"\s|$"
|
||||
|
||||
def func(self):
|
||||
|
|
@ -400,36 +439,49 @@ class CmdGet(COMMAND_DEFAULT_CLASS):
|
|||
caller = self.caller
|
||||
|
||||
if not self.args:
|
||||
caller.msg("Get what?")
|
||||
self.msg("Get what?")
|
||||
return
|
||||
obj = caller.search(self.args, location=caller.location)
|
||||
if not obj:
|
||||
objs = caller.search(self.args, location=caller.location, stacked=self.number)
|
||||
if not objs:
|
||||
return
|
||||
if caller == obj:
|
||||
caller.msg("You can't get yourself.")
|
||||
return
|
||||
if not obj.access(caller, "get"):
|
||||
if obj.db.get_err_msg:
|
||||
caller.msg(obj.db.get_err_msg)
|
||||
else:
|
||||
caller.msg("You can't get that.")
|
||||
# the 'stacked' search sometimes returns a list, sometimes not, so we make it always a list
|
||||
# NOTE: this behavior may be a bug, see issue #3432
|
||||
objs = utils.make_iter(objs)
|
||||
|
||||
if len(objs) == 1 and caller == objs[0]:
|
||||
self.msg("You can't get yourself.")
|
||||
return
|
||||
|
||||
# calling at_pre_get hook method
|
||||
if not obj.at_pre_get(caller):
|
||||
return
|
||||
# if we aren't allowed to get any of the objects, cancel the get
|
||||
for obj in objs:
|
||||
# check the locks
|
||||
if not obj.access(caller, "get"):
|
||||
if obj.db.get_err_msg:
|
||||
self.msg(obj.db.get_err_msg)
|
||||
else:
|
||||
self.msg("You can't get that.")
|
||||
return
|
||||
# calling at_pre_get hook method
|
||||
if not obj.at_pre_get(caller):
|
||||
return
|
||||
|
||||
success = obj.move_to(caller, quiet=True, move_type="get")
|
||||
if not success:
|
||||
caller.msg("This can't be picked up.")
|
||||
moved = []
|
||||
# attempt to move all of the objects
|
||||
for obj in objs:
|
||||
if obj.move_to(caller, quiet=True, move_type="get"):
|
||||
moved.append(obj)
|
||||
# calling at_get hook method
|
||||
obj.at_get(caller)
|
||||
|
||||
if not moved:
|
||||
# none of the objects were successfully moved
|
||||
self.msg("That can't be picked up.")
|
||||
else:
|
||||
singular, _ = obj.get_numbered_name(1, caller)
|
||||
caller.location.msg_contents(f"$You() $conj(pick) up {singular}.", from_obj=caller)
|
||||
# calling at_get hook method
|
||||
obj.at_get(caller)
|
||||
obj_name = moved[0].get_numbered_name(len(moved), caller, return_string=True)
|
||||
caller.location.msg_contents(f"$You() $conj(pick) up {obj_name}.", from_obj=caller)
|
||||
|
||||
|
||||
class CmdDrop(COMMAND_DEFAULT_CLASS):
|
||||
class CmdDrop(NumberedTargetCommand):
|
||||
"""
|
||||
drop something
|
||||
|
||||
|
|
@ -454,30 +506,42 @@ class CmdDrop(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
# Because the DROP command by definition looks for items
|
||||
# in inventory, call the search function using location = caller
|
||||
obj = caller.search(
|
||||
objs = caller.search(
|
||||
self.args,
|
||||
location=caller,
|
||||
nofound_string=f"You aren't carrying {self.args}.",
|
||||
multimatch_string=f"You carry more than one {self.args}:",
|
||||
stacked=self.number,
|
||||
)
|
||||
if not obj:
|
||||
if not objs:
|
||||
return
|
||||
# the 'stacked' search sometimes returns a list, sometimes not, so we make it always a list
|
||||
# NOTE: this behavior may be a bug, see issue #3432
|
||||
objs = utils.make_iter(objs)
|
||||
|
||||
# Call the object script's at_pre_drop() method.
|
||||
if not obj.at_pre_drop(caller):
|
||||
return
|
||||
# if any objects fail the drop permission check, cancel the drop
|
||||
for obj in objs:
|
||||
# Call the object's at_pre_drop() method.
|
||||
if not obj.at_pre_drop(caller):
|
||||
return
|
||||
|
||||
success = obj.move_to(caller.location, quiet=True, move_type="drop")
|
||||
if not success:
|
||||
caller.msg("This couldn't be dropped.")
|
||||
# do the actual dropping
|
||||
moved = []
|
||||
for obj in objs:
|
||||
if obj.move_to(caller.location, quiet=True, move_type="drop"):
|
||||
moved.append(obj)
|
||||
# Call the object's at_drop() method.
|
||||
obj.at_drop(caller)
|
||||
|
||||
if not moved:
|
||||
# none of the objects were successfully moved
|
||||
self.msg("That can't be dropped.")
|
||||
else:
|
||||
singular, _ = obj.get_numbered_name(1, caller)
|
||||
caller.location.msg_contents(f"$You() $conj(drop) {singular}.", from_obj=caller)
|
||||
# Call the object script's at_drop() method.
|
||||
obj.at_drop(caller)
|
||||
obj_name = moved[0].get_numbered_name(len(moved), caller, return_string=True)
|
||||
caller.location.msg_contents(f"$You() $conj(drop) {obj_name}.", from_obj=caller)
|
||||
|
||||
|
||||
class CmdGive(COMMAND_DEFAULT_CLASS):
|
||||
class CmdGive(NumberedTargetCommand):
|
||||
"""
|
||||
give away something to someone
|
||||
|
||||
|
|
@ -500,37 +564,50 @@ class CmdGive(COMMAND_DEFAULT_CLASS):
|
|||
if not self.args or not self.rhs:
|
||||
caller.msg("Usage: give <inventory object> = <target>")
|
||||
return
|
||||
# find the thing(s) to give away
|
||||
to_give = caller.search(
|
||||
self.lhs,
|
||||
location=caller,
|
||||
nofound_string=f"You aren't carrying {self.lhs}.",
|
||||
multimatch_string=f"You carry more than one {self.lhs}:",
|
||||
stacked=self.number,
|
||||
)
|
||||
if not to_give:
|
||||
return
|
||||
# find the target to give to
|
||||
target = caller.search(self.rhs)
|
||||
if not (to_give and target):
|
||||
if not target:
|
||||
return
|
||||
|
||||
singular, _ = to_give.get_numbered_name(1, caller)
|
||||
# the 'stacked' search sometimes returns a list, sometimes not, so we make it always a list
|
||||
# NOTE: this behavior may be a bug, see issue #3432
|
||||
to_give = utils.make_iter(to_give)
|
||||
|
||||
singular, plural = to_give[0].get_numbered_name(len(to_give), caller)
|
||||
if target == caller:
|
||||
caller.msg(f"You keep {singular} to yourself.")
|
||||
return
|
||||
if not to_give.location == caller:
|
||||
caller.msg(f"You are not holding {singular}.")
|
||||
caller.msg(f"You keep {plural if len(to_give) > 1 else singular} to yourself.")
|
||||
return
|
||||
|
||||
# calling at_pre_give hook method
|
||||
if not to_give.at_pre_give(caller, target):
|
||||
return
|
||||
# if any of the objects aren't allowed to be given, cancel the give
|
||||
for obj in to_give:
|
||||
# calling at_pre_give hook method
|
||||
if not obj.at_pre_give(caller, target):
|
||||
return
|
||||
|
||||
# give object
|
||||
success = to_give.move_to(target, quiet=True, move_type="give")
|
||||
if not success:
|
||||
caller.msg(f"You could not give {singular} to {target.key}.")
|
||||
# do the actual moving
|
||||
moved = []
|
||||
for obj in to_give:
|
||||
if obj.move_to(target, quiet=True, move_type="give"):
|
||||
moved.append(obj)
|
||||
# Call the object's at_give() method.
|
||||
obj.at_give(caller, target)
|
||||
|
||||
if not moved:
|
||||
caller.msg(f"You could not give that to {target.get_display_name(caller)}.")
|
||||
else:
|
||||
caller.msg(f"You give {singular} to {target.key}.")
|
||||
target.msg(f"{caller.key} gives you {singular}.")
|
||||
# Call the object script's at_give() method.
|
||||
to_give.at_give(caller, target)
|
||||
obj_name = to_give[0].get_numbered_name(len(moved), caller, return_string=True)
|
||||
caller.msg(f"You give {obj_name} to {target.get_display_name(caller)}.")
|
||||
target.msg(f"{caller.get_display_name(target)} gives you {obj_name}.")
|
||||
|
||||
|
||||
class CmdSetDesc(COMMAND_DEFAULT_CLASS):
|
||||
|
|
|
|||
|
|
@ -780,13 +780,14 @@ class CmdSetHelp(CmdHelp):
|
|||
Edit the help database.
|
||||
|
||||
Usage:
|
||||
sethelp[/switches] <topic>[[;alias;alias][,category[,locks]] [= <text>]
|
||||
|
||||
sethelp[/switches] <topic>[[;alias;alias][,category[,locks]]
|
||||
[= <text or new category>]
|
||||
Switches:
|
||||
edit - open a line editor to edit the topic's help text.
|
||||
replace - overwrite existing help topic.
|
||||
append - add text to the end of existing topic with a newline between.
|
||||
extend - as append, but don't add a newline.
|
||||
category - change category of existing help topic.
|
||||
delete - remove help topic.
|
||||
|
||||
Examples:
|
||||
|
|
@ -794,6 +795,7 @@ class CmdSetHelp(CmdHelp):
|
|||
sethelp/append pickpocketing,Thievery = This steals ...
|
||||
sethelp/replace pickpocketing, ,attr(is_thief) = This steals ...
|
||||
sethelp/edit thievery
|
||||
sethelp/category thievery = classes
|
||||
|
||||
If not assigning a category, the `settings.DEFAULT_HELP_CATEGORY` category
|
||||
will be used. If no lockstring is specified, everyone will be able to read
|
||||
|
|
@ -840,7 +842,7 @@ class CmdSetHelp(CmdHelp):
|
|||
|
||||
key = "sethelp"
|
||||
aliases = []
|
||||
switch_options = ("edit", "replace", "append", "extend", "delete")
|
||||
switch_options = ("edit", "replace", "append", "extend", "category", "delete")
|
||||
locks = "cmd:perm(Helper)"
|
||||
help_category = "Building"
|
||||
arg_regex = None
|
||||
|
|
@ -857,7 +859,7 @@ class CmdSetHelp(CmdHelp):
|
|||
|
||||
if not self.args:
|
||||
self.msg(
|
||||
"Usage: sethelp[/switches] <topic>[;alias;alias][,category[,locks,..] = <text>"
|
||||
"Usage: sethelp[/switches] <topic>[[;alias;alias][,category[,locks]] [= <text or new category>]"
|
||||
)
|
||||
return
|
||||
|
||||
|
|
@ -953,7 +955,7 @@ class CmdSetHelp(CmdHelp):
|
|||
else:
|
||||
helpentry = create.create_help_entry(
|
||||
topicstr,
|
||||
self.rhs,
|
||||
self.rhs if self.rhs is not None else "",
|
||||
category=category,
|
||||
locks=lockstring,
|
||||
aliases=aliases,
|
||||
|
|
@ -986,6 +988,19 @@ class CmdSetHelp(CmdHelp):
|
|||
self.msg(f"Entry updated:\n{old_entry.entrytext}{aliastxt}")
|
||||
return
|
||||
|
||||
if "category" in switches:
|
||||
# set the category
|
||||
if not old_entry:
|
||||
self.msg(f"Could not find topic '{topicstr}'{aliastxt}.")
|
||||
return
|
||||
if not self.rhs:
|
||||
self.msg("You must supply a category.")
|
||||
return
|
||||
category = self.rhs.lower()
|
||||
old_entry.help_category = category
|
||||
self.msg(f"Category for entry '{topicstr}'{aliastxt} changed to '{category}'.")
|
||||
return
|
||||
|
||||
if "delete" in switches or "del" in switches:
|
||||
# delete the help entry
|
||||
if not old_entry:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ System commands
|
|||
|
||||
"""
|
||||
|
||||
|
||||
import code
|
||||
import datetime
|
||||
import os
|
||||
|
|
@ -112,7 +111,6 @@ class CmdReset(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
|
||||
class CmdShutdown(COMMAND_DEFAULT_CLASS):
|
||||
|
||||
"""
|
||||
stop the server completely
|
||||
|
||||
|
|
@ -277,7 +275,6 @@ def evennia_local_vars(caller):
|
|||
|
||||
|
||||
class EvenniaPythonConsole(code.InteractiveConsole):
|
||||
|
||||
"""Evennia wrapper around a Python interactive console."""
|
||||
|
||||
def __init__(self, caller):
|
||||
|
|
|
|||
|
|
@ -14,10 +14,13 @@ main test suite started with
|
|||
import datetime
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import evennia
|
||||
from anything import Anything
|
||||
from django.conf import settings
|
||||
from django.test import override_settings
|
||||
from parameterized import parameterized
|
||||
from twisted.internet import task
|
||||
|
||||
import evennia
|
||||
from evennia import (
|
||||
DefaultCharacter,
|
||||
DefaultExit,
|
||||
|
|
@ -29,7 +32,14 @@ from evennia import (
|
|||
from evennia.commands import cmdparser
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.commands.command import Command, InterruptCommand
|
||||
from evennia.commands.default import account, admin, batchprocess, building, comms, general
|
||||
from evennia.commands.default import (
|
||||
account,
|
||||
admin,
|
||||
batchprocess,
|
||||
building,
|
||||
comms,
|
||||
general,
|
||||
)
|
||||
from evennia.commands.default import help as help_module
|
||||
from evennia.commands.default import syscommands, system, unloggedin
|
||||
from evennia.commands.default.cmdset_character import CharacterCmdSet
|
||||
|
|
@ -38,8 +48,6 @@ from evennia.prototypes import prototypes as protlib
|
|||
from evennia.utils import create, gametime, utils
|
||||
from evennia.utils.test_resources import BaseEvenniaCommandTest # noqa
|
||||
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaCommandTest
|
||||
from parameterized import parameterized
|
||||
from twisted.internet import task
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Command testing
|
||||
|
|
@ -108,8 +116,12 @@ class TestGeneral(BaseEvenniaCommandTest):
|
|||
self.call(general.CmdNick(), "/list", "Defined Nicks:")
|
||||
|
||||
def test_get_and_drop(self):
|
||||
self.call(general.CmdGet(), "Obj", "You pick up an Obj")
|
||||
self.call(general.CmdDrop(), "Obj", "You drop an Obj")
|
||||
self.call(general.CmdGet(), "Obj", "You pick up an Obj.")
|
||||
self.call(general.CmdDrop(), "Obj", "You drop an Obj.")
|
||||
# test stacking
|
||||
self.obj2.key = "Obj"
|
||||
self.call(general.CmdGet(), "2 Obj", "You pick up two Objs.")
|
||||
self.call(general.CmdDrop(), "2 Obj", "You drop two Objs.")
|
||||
|
||||
def test_give(self):
|
||||
self.call(general.CmdGive(), "Obj to Char2", "You aren't carrying Obj.")
|
||||
|
|
@ -117,6 +129,21 @@ class TestGeneral(BaseEvenniaCommandTest):
|
|||
self.call(general.CmdGet(), "Obj", "You pick up an Obj")
|
||||
self.call(general.CmdGive(), "Obj to Char2", "You give")
|
||||
self.call(general.CmdGive(), "Obj = Char", "You give", caller=self.char2)
|
||||
# test stacking
|
||||
self.obj2.key = "Obj"
|
||||
self.obj2.location = self.char1
|
||||
self.call(general.CmdGive(), "2 Obj = Char2", "You give two Objs")
|
||||
|
||||
def test_numbered_target_command(self):
|
||||
class CmdTest(general.NumberedTargetCommand):
|
||||
key = "test"
|
||||
|
||||
def func(self):
|
||||
self.msg(f"Number: {self.number} Args: {self.args}")
|
||||
|
||||
self.call(CmdTest(), "", "Number: 0 Args: ")
|
||||
self.call(CmdTest(), "obj", "Number: 0 Args: obj")
|
||||
self.call(CmdTest(), "1 obj", "Number: 1 Args: obj")
|
||||
|
||||
def test_mux_command(self):
|
||||
class CmdTest(MuxCommand):
|
||||
|
|
@ -197,6 +224,12 @@ class TestHelp(BaseEvenniaCommandTest):
|
|||
cmdset=CharacterCmdSet(),
|
||||
)
|
||||
self.call(help_module.CmdHelp(), "testhelp", "Help for testhelp", cmdset=CharacterCmdSet())
|
||||
self.call(
|
||||
help_module.CmdSetHelp(),
|
||||
"/category testhelp = misc",
|
||||
"Category for entry 'testhelp' changed to 'misc'.",
|
||||
cmdset=CharacterCmdSet(),
|
||||
)
|
||||
|
||||
@parameterized.expand(
|
||||
[
|
||||
|
|
@ -784,6 +817,24 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(building.CmdSetObjAlias(), "Obj2 =", "Cleared aliases from Obj2")
|
||||
self.call(building.CmdSetObjAlias(), "Obj2 =", "No aliases to clear.")
|
||||
|
||||
self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj: testobj1b")
|
||||
self.call(
|
||||
building.CmdSetObjAlias(),
|
||||
"/category Obj = testobj1b:category1",
|
||||
"Alias(es) for 'Obj' set to 'testobj1b' (category: 'category1').",
|
||||
)
|
||||
self.call(
|
||||
building.CmdSetObjAlias(),
|
||||
"/category Obj = testobj1b:category2",
|
||||
"Alias(es) for 'Obj' set to 'testobj1b,testobj1b' (category: 'category2').",
|
||||
)
|
||||
self.call(
|
||||
building.CmdSetObjAlias(), # delete both occurences of alias 'testobj1b'
|
||||
"/delete Obj = testobj1b",
|
||||
"Alias 'testobj1b' deleted from Obj.",
|
||||
)
|
||||
self.call(building.CmdSetObjAlias(), "Obj =", "No aliases to clear.")
|
||||
|
||||
def test_copy(self):
|
||||
self.call(
|
||||
building.CmdCopy(),
|
||||
|
|
@ -1754,8 +1805,7 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdSpawn(),
|
||||
"{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', "
|
||||
"'key':'goblin', 'location':'%s'}"
|
||||
% spawnLoc.dbref,
|
||||
"'key':'goblin', 'location':'%s'}" % spawnLoc.dbref,
|
||||
"Spawned goblin",
|
||||
)
|
||||
goblin = get_object(self, "goblin")
|
||||
|
|
@ -1803,8 +1853,7 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdSpawn(),
|
||||
"/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo',"
|
||||
" 'location':'%s'}"
|
||||
% spawnLoc.dbref,
|
||||
" 'location':'%s'}" % spawnLoc.dbref,
|
||||
"Spawned Ball",
|
||||
)
|
||||
ball = get_object(self, "Ball")
|
||||
|
|
@ -2048,6 +2097,11 @@ class TestComms(BaseEvenniaCommandTest):
|
|||
),
|
||||
receiver=self.account,
|
||||
)
|
||||
from evennia.comms.models import Msg
|
||||
|
||||
msgs = Msg.objects.filter(db_tags__db_key="page", db_tags__db_category="comms")
|
||||
self.assertEqual(msgs[0].senders, [self.account])
|
||||
self.assertEqual(msgs[0].receivers, [self.account2])
|
||||
|
||||
|
||||
@override_settings(DISCORD_BOT_TOKEN="notarealtoken", DISCORD_ENABLED=True)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
Commands that are available from the connect screen.
|
||||
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import re
|
||||
from codecs import lookup as codecs_lookup
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
Base typeclass for in-game Channels.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ Comm system components.
|
|||
|
||||
"""
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ connect to channels by use of a ChannelConnect object (this object is
|
|||
necessary to easily be able to delete connections on the fly).
|
||||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
|
|
|||
|
|
@ -221,7 +221,6 @@ def get_available_overwrite_name(name, max_length):
|
|||
|
||||
@deconstructible
|
||||
class S3Boto3StorageFile(File):
|
||||
|
||||
"""
|
||||
The default file object used by the S3Boto3Storage backend.
|
||||
This file implements file streaming using boto's multipart
|
||||
|
|
|
|||
|
|
@ -2,5 +2,6 @@
|
|||
Build-menu contrib - vincent-lg 2018
|
||||
|
||||
"""
|
||||
|
||||
from .building_menu import BuildingMenu # noqa
|
||||
from .building_menu import GenericBuildingCmd # noqa
|
||||
|
|
|
|||
|
|
@ -331,7 +331,6 @@ def menu_edit(caller, choice, obj):
|
|||
|
||||
|
||||
class CmdNoInput(Command):
|
||||
|
||||
"""No input has been found."""
|
||||
|
||||
key = _CMD_NOINPUT
|
||||
|
|
@ -352,7 +351,6 @@ class CmdNoInput(Command):
|
|||
|
||||
|
||||
class CmdNoMatch(Command):
|
||||
|
||||
"""No input has been found."""
|
||||
|
||||
key = _CMD_NOMATCH
|
||||
|
|
@ -394,7 +392,6 @@ class CmdNoMatch(Command):
|
|||
|
||||
|
||||
class BuildingMenuCmdSet(CmdSet):
|
||||
|
||||
"""Building menu CmdSet."""
|
||||
|
||||
key = "building_menu"
|
||||
|
|
@ -421,7 +418,6 @@ class BuildingMenuCmdSet(CmdSet):
|
|||
|
||||
|
||||
class Choice:
|
||||
|
||||
"""A choice object, created by `add_choice`."""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -557,7 +553,6 @@ class Choice:
|
|||
|
||||
|
||||
class BuildingMenu:
|
||||
|
||||
"""
|
||||
Class allowing to create and set building menus to edit specific objects.
|
||||
|
||||
|
|
@ -1200,7 +1195,6 @@ class BuildingMenu:
|
|||
|
||||
# Generic building menu and command
|
||||
class GenericBuildingMenu(BuildingMenu):
|
||||
|
||||
"""A generic building menu, allowing to edit any object.
|
||||
|
||||
This is more a demonstration menu. By default, it allows to edit the
|
||||
|
|
@ -1241,7 +1235,6 @@ class GenericBuildingMenu(BuildingMenu):
|
|||
|
||||
|
||||
class GenericBuildingCmd(Command):
|
||||
|
||||
"""
|
||||
Generic building command.
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ This helps writing isolated code and reusing it over multiple objects.
|
|||
|
||||
See the docs for more information.
|
||||
"""
|
||||
|
||||
from . import exceptions # noqa
|
||||
from .component import Component # noqa
|
||||
from .dbfield import DBField, NDBField, TagField # noqa
|
||||
|
|
|
|||
|
|
@ -155,6 +155,22 @@ class Component(metaclass=BaseComponent):
|
|||
"""
|
||||
return self.host.attributes
|
||||
|
||||
@property
|
||||
def pk(self):
|
||||
"""
|
||||
Shortcut property returning the host's primary key.
|
||||
|
||||
Returns:
|
||||
int: The Host's primary key.
|
||||
|
||||
Notes:
|
||||
This is requried to allow AttributeProperties to correctly update `_SaverMutable` data
|
||||
(like lists) in-place (since the DBField sits on the Component which doesn't itself
|
||||
have a primary key, this save operation would otherwise fail).
|
||||
|
||||
"""
|
||||
return self.host.pk
|
||||
|
||||
@property
|
||||
def nattributes(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ Components - ChrisLR 2022
|
|||
|
||||
This file contains the Descriptors used to set Fields in Components
|
||||
"""
|
||||
|
||||
import typing
|
||||
|
||||
from evennia.typeclasses.attributes import AttributeProperty, NAttributeProperty
|
||||
from evennia.typeclasses.attributes import (AttributeProperty,
|
||||
NAttributeProperty)
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .components import Component
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ class TestComponents(EvenniaTest):
|
|||
|
||||
def test_mutables_are_not_shared_when_autocreate(self):
|
||||
self.char1.test_a.my_list.append(1)
|
||||
self.assertNotEqual(self.char1.test_a.my_list, self.char2.test_a.my_list)
|
||||
self.assertNotEqual(id(self.char1.test_a.my_list), id(self.char2.test_a.my_list))
|
||||
|
||||
def test_replacing_class_component_slot_with_runtime_component(self):
|
||||
self.char1.components.add_default("replacement_inherited_test_a")
|
||||
|
|
|
|||
|
|
@ -309,7 +309,6 @@ def schedule(callback, repeat=False, **kwargs):
|
|||
|
||||
|
||||
class GametimeScript(DefaultScript):
|
||||
|
||||
"""Gametime-sensitive script."""
|
||||
|
||||
def at_script_creation(self):
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ You could also pass extra data to this client for advanced functionality.
|
|||
|
||||
See the docs for more information.
|
||||
"""
|
||||
|
||||
from evennia.contrib.base_systems.godotwebsocket.text2bbcode import (
|
||||
BBCODE_PARSER,
|
||||
parse_to_bbcode,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ Godot Websocket - ChrisLR 2022
|
|||
|
||||
This file contains the necessary code and data to convert text with color tags to bbcode (For godot)
|
||||
"""
|
||||
|
||||
from evennia.utils.ansi import *
|
||||
from evennia.utils.text2html import TextToHTMLparser
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Godot Websocket - ChrisLR 2022
|
|||
This file contains the code necessary to dedicate a port to communicate with Godot via Websockets.
|
||||
It uses the plugin system and should be plugged via settings as detailed in the readme.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from autobahn.twisted import WebSocketServerFactory
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ from collections import namedtuple
|
|||
|
||||
|
||||
class CallbackHandler(object):
|
||||
|
||||
"""
|
||||
The callback handler for a specific object.
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ Use the /del switch to remove callbacks that should not be connected.
|
|||
|
||||
|
||||
class CmdCallback(COMMAND_DEFAULT_CLASS):
|
||||
|
||||
"""
|
||||
Command to edit callbacks.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ RE_LINE_ERROR = re.compile(r'^ File "\<string\>", line (\d+)')
|
|||
|
||||
|
||||
class EventHandler(DefaultScript):
|
||||
|
||||
"""
|
||||
The event handler that contains all events in a global script.
|
||||
|
||||
|
|
@ -600,7 +599,6 @@ class EventHandler(DefaultScript):
|
|||
|
||||
# Script to call time-related events
|
||||
class TimeEventScript(DefaultScript):
|
||||
|
||||
"""Gametime-sensitive script."""
|
||||
|
||||
def at_script_creation(self):
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ OLD_EVENTS = {}
|
|||
|
||||
|
||||
class TestEventHandler(BaseEvenniaTest):
|
||||
|
||||
"""
|
||||
Test cases of the event handler to add, edit or delete events.
|
||||
"""
|
||||
|
|
@ -259,7 +258,6 @@ class TestEventHandler(BaseEvenniaTest):
|
|||
|
||||
|
||||
class TestCmdCallback(BaseEvenniaCommandTest):
|
||||
|
||||
"""Test the @callback command."""
|
||||
|
||||
def setUp(self):
|
||||
|
|
@ -448,7 +446,6 @@ class TestCmdCallback(BaseEvenniaCommandTest):
|
|||
|
||||
|
||||
class TestDefaultCallbacks(BaseEvenniaCommandTest):
|
||||
|
||||
"""Test the default callbacks."""
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
|||
|
|
@ -166,7 +166,6 @@ Variables you can use in this event:
|
|||
|
||||
@register_events
|
||||
class EventCharacter(DefaultCharacter):
|
||||
|
||||
"""Typeclass to represent a character and call event types."""
|
||||
|
||||
_events = {
|
||||
|
|
@ -625,7 +624,6 @@ Variables you can use in this event:
|
|||
|
||||
@register_events
|
||||
class EventExit(DefaultExit):
|
||||
|
||||
"""Modified exit including management of events."""
|
||||
|
||||
_events = {
|
||||
|
|
@ -721,7 +719,6 @@ Variables you can use in this event:
|
|||
|
||||
@register_events
|
||||
class EventObject(DefaultObject):
|
||||
|
||||
"""Default object with management of events."""
|
||||
|
||||
_events = {
|
||||
|
|
@ -892,7 +889,6 @@ Variables you can use in this event:
|
|||
|
||||
@register_events
|
||||
class EventRoom(DefaultRoom):
|
||||
|
||||
"""Default room with management of events."""
|
||||
|
||||
_events = {
|
||||
|
|
|
|||
|
|
@ -251,7 +251,6 @@ def phrase_event(callbacks, parameters):
|
|||
|
||||
|
||||
class InterruptEvent(RuntimeError):
|
||||
|
||||
"""
|
||||
Interrupt the current event.
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ def node_enter_username(caller, raw_text, **kwargs):
|
|||
else:
|
||||
new_user = False
|
||||
|
||||
if new_user and not settings.ACCOUNT_REGISTRATION_ENABLED:
|
||||
if new_user and not settings.NEW_ACCOUNT_REGISTRATION_ENABLED:
|
||||
caller.msg("Registration is currently disabled.")
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
|||
```
|
||||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from .unixcommand import UnixCommand
|
|||
|
||||
|
||||
class CmdDummy(UnixCommand):
|
||||
|
||||
"""A dummy UnixCommand."""
|
||||
|
||||
key = "dummy"
|
||||
|
|
|
|||
|
|
@ -72,14 +72,12 @@ from evennia.utils.ansi import raw
|
|||
|
||||
|
||||
class ParseError(Exception):
|
||||
|
||||
"""An error occurred during parsing."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnixCommandParser(argparse.ArgumentParser):
|
||||
|
||||
"""A modifier command parser for unix commands.
|
||||
|
||||
This parser is used to replace `argparse.ArgumentParser`. It
|
||||
|
|
@ -183,7 +181,6 @@ class UnixCommandParser(argparse.ArgumentParser):
|
|||
|
||||
|
||||
class HelpAction(argparse.Action):
|
||||
|
||||
"""Override the -h/--help action in the default parser.
|
||||
|
||||
Using the default -h/--help will call the exit function in different
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ Here player user can set their own description as well as select to create a
|
|||
new room (to start from scratch) or join an existing room (with other players).
|
||||
|
||||
"""
|
||||
|
||||
from evennia import EvMenu
|
||||
from evennia.utils import create, justify, list_to_string, logger
|
||||
from evennia.utils.evmenu import list_node
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ Available parents:
|
|||
- Positionable (supports sit/lie/knee/climb at once)
|
||||
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import re
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
Unit tests for the Evscaperoom
|
||||
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import pkgutil
|
||||
from os import path
|
||||
|
|
|
|||
|
|
@ -72,9 +72,11 @@ with which to test the system:
|
|||
wear shirt
|
||||
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from evennia import DefaultCharacter, DefaultObject, default_cmds
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
from evennia.utils import (
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ or implement the same locks/hooks in your own typeclasses.
|
|||
at_pre_get_from(getter, target, **kwargs) - called with the pre-get hooks
|
||||
at_pre_put_in(putter, target, **kwargs) - called with the pre-put hooks
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from evennia import AttributeProperty, CmdSet, DefaultObject
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ class CraftingRecipeBase:
|
|||
**kwargs: Any optional properties relevant to this send.
|
||||
|
||||
"""
|
||||
self.crafter.msg(message, {"type": "crafting"})
|
||||
self.crafter.msg(text=(message, {"type": "crafting"}))
|
||||
|
||||
def pre_craft(self, **kwargs):
|
||||
"""
|
||||
|
|
@ -615,9 +615,11 @@ class CraftingRecipe(CraftingRecipeBase):
|
|||
)
|
||||
else:
|
||||
self.output_names = [
|
||||
prot.get("key", prot.get("typeclass", "unnamed"))
|
||||
if isinstance(prot, dict)
|
||||
else str(prot)
|
||||
(
|
||||
prot.get("key", prot.get("typeclass", "unnamed"))
|
||||
if isinstance(prot, dict)
|
||||
else str(prot)
|
||||
)
|
||||
for prot in self.output_prototypes
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ class TestCraftingRecipeBase(BaseEvenniaTestCase):
|
|||
"""Test messaging to crafter"""
|
||||
|
||||
self.recipe.msg("message")
|
||||
self.crafter.msg.assert_called_with("message", {"type": "crafting"})
|
||||
self.crafter.msg.assert_called_with(text=("message", {"type": "crafting"}))
|
||||
|
||||
def test_pre_craft(self):
|
||||
"""Test validating hook"""
|
||||
|
|
@ -206,7 +206,7 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
|
|||
self.assertEqual(result[0].key, "Result1")
|
||||
self.assertEqual(result[0].tags.all(), ["result1", "resultprot"])
|
||||
self.crafter.msg.assert_called_with(
|
||||
recipe.success_message.format(outputs="Result1"), {"type": "crafting"}
|
||||
text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
|
||||
)
|
||||
|
||||
# make sure consumables are gone
|
||||
|
|
@ -235,7 +235,7 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
|
|||
self.assertEqual(result[0].key, "Result1")
|
||||
self.assertEqual(result[0].tags.all(), ["result1", "resultprot"])
|
||||
self.crafter.msg.assert_called_with(
|
||||
recipe.success_message.format(outputs="Result1"), {"type": "crafting"}
|
||||
text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
|
||||
)
|
||||
|
||||
# make sure consumables are gone
|
||||
|
|
@ -251,8 +251,10 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
|
|||
result = recipe.craft()
|
||||
self.assertFalse(result)
|
||||
self.crafter.msg.assert_called_with(
|
||||
recipe.error_tool_missing_message.format(outputs="Result1", missing="tool2"),
|
||||
{"type": "crafting"},
|
||||
text=(
|
||||
recipe.error_tool_missing_message.format(outputs="Result1", missing="tool2"),
|
||||
{"type": "crafting"},
|
||||
)
|
||||
)
|
||||
|
||||
# make sure consumables are still there
|
||||
|
|
@ -269,8 +271,10 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
|
|||
result = recipe.craft()
|
||||
self.assertFalse(result)
|
||||
self.crafter.msg.assert_called_with(
|
||||
recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"),
|
||||
{"type": "crafting"},
|
||||
text=(
|
||||
recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"),
|
||||
{"type": "crafting"},
|
||||
)
|
||||
)
|
||||
|
||||
# make sure consumables are still there
|
||||
|
|
@ -293,8 +297,10 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
|
|||
|
||||
self.assertFalse(result)
|
||||
self.crafter.msg.assert_called_with(
|
||||
recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"),
|
||||
{"type": "crafting"},
|
||||
text=(
|
||||
recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"),
|
||||
{"type": "crafting"},
|
||||
)
|
||||
)
|
||||
|
||||
# make sure consumables are deleted even though we failed
|
||||
|
|
@ -317,10 +323,12 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
|
|||
result = recipe.craft()
|
||||
self.assertFalse(result)
|
||||
self.crafter.msg.assert_called_with(
|
||||
recipe.error_tool_excess_message.format(
|
||||
outputs="Result1", excess=wrong.get_display_name(looker=self.crafter)
|
||||
),
|
||||
{"type": "crafting"},
|
||||
text=(
|
||||
recipe.error_tool_excess_message.format(
|
||||
outputs="Result1", excess=wrong.get_display_name(looker=self.crafter)
|
||||
),
|
||||
{"type": "crafting"},
|
||||
)
|
||||
)
|
||||
# make sure consumables are still there
|
||||
self.assertIsNotNone(self.cons1.pk)
|
||||
|
|
@ -342,10 +350,12 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
|
|||
result = recipe.craft()
|
||||
self.assertFalse(result)
|
||||
self.crafter.msg.assert_called_with(
|
||||
recipe.error_tool_excess_message.format(
|
||||
outputs="Result1", excess=tool3.get_display_name(looker=self.crafter)
|
||||
),
|
||||
{"type": "crafting"},
|
||||
text=(
|
||||
recipe.error_tool_excess_message.format(
|
||||
outputs="Result1", excess=tool3.get_display_name(looker=self.crafter)
|
||||
),
|
||||
{"type": "crafting"},
|
||||
)
|
||||
)
|
||||
|
||||
# make sure consumables are still there
|
||||
|
|
@ -369,10 +379,12 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
|
|||
result = recipe.craft()
|
||||
self.assertFalse(result)
|
||||
self.crafter.msg.assert_called_with(
|
||||
recipe.error_consumable_excess_message.format(
|
||||
outputs="Result1", excess=cons4.get_display_name(looker=self.crafter)
|
||||
),
|
||||
{"type": "crafting"},
|
||||
text=(
|
||||
recipe.error_consumable_excess_message.format(
|
||||
outputs="Result1", excess=cons4.get_display_name(looker=self.crafter)
|
||||
),
|
||||
{"type": "crafting"},
|
||||
)
|
||||
)
|
||||
|
||||
# make sure consumables are still there
|
||||
|
|
@ -396,7 +408,7 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
|
|||
result = recipe.craft()
|
||||
self.assertTrue(result)
|
||||
self.crafter.msg.assert_called_with(
|
||||
recipe.success_message.format(outputs="Result1"), {"type": "crafting"}
|
||||
text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
|
||||
)
|
||||
|
||||
# make sure consumables are gone
|
||||
|
|
@ -419,7 +431,7 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
|
|||
result = recipe.craft()
|
||||
self.assertTrue(result)
|
||||
self.crafter.msg.assert_called_with(
|
||||
recipe.success_message.format(outputs="Result1"), {"type": "crafting"}
|
||||
text=(recipe.success_message.format(outputs="Result1"), {"type": "crafting"})
|
||||
)
|
||||
|
||||
# make sure consumables are gone
|
||||
|
|
@ -439,10 +451,12 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
|
|||
result = recipe.craft()
|
||||
self.assertFalse(result)
|
||||
self.crafter.msg.assert_called_with(
|
||||
recipe.error_tool_order_message.format(
|
||||
outputs="Result1", missing=self.tool2.get_display_name(looker=self.crafter)
|
||||
),
|
||||
{"type": "crafting"},
|
||||
text=(
|
||||
recipe.error_tool_order_message.format(
|
||||
outputs="Result1", missing=self.tool2.get_display_name(looker=self.crafter)
|
||||
),
|
||||
{"type": "crafting"},
|
||||
)
|
||||
)
|
||||
|
||||
# make sure consumables are still there
|
||||
|
|
@ -462,10 +476,12 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
|
|||
result = recipe.craft()
|
||||
self.assertFalse(result)
|
||||
self.crafter.msg.assert_called_with(
|
||||
recipe.error_consumable_order_message.format(
|
||||
outputs="Result1", missing=self.cons3.get_display_name(looker=self.crafter)
|
||||
),
|
||||
{"type": "crafting"},
|
||||
text=(
|
||||
recipe.error_consumable_order_message.format(
|
||||
outputs="Result1", missing=self.cons3.get_display_name(looker=self.crafter)
|
||||
),
|
||||
{"type": "crafting"},
|
||||
)
|
||||
)
|
||||
|
||||
# make sure consumables are still there
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ Test gendersub contrib.
|
|||
|
||||
"""
|
||||
|
||||
|
||||
from mock import patch
|
||||
|
||||
from evennia.commands.default.tests import BaseEvenniaCommandTest
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ Reload the server and you should have the +desc command available (it
|
|||
will replace the default `desc` command).
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from evennia import default_cmds
|
||||
|
|
|
|||
|
|
@ -823,7 +823,6 @@ class CmdExtendedRoomDesc(default_cmds.CmdDesc):
|
|||
|
||||
|
||||
class CmdExtendedRoomDetail(default_cmds.MuxCommand):
|
||||
|
||||
"""
|
||||
sets a detail on a room
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ Testing of ExtendedRoom contrib
|
|||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from evennia import create_object
|
||||
from evennia.utils.test_resources import BaseEvenniaCommandTest, EvenniaTestCase
|
||||
from mock import Mock, patch
|
||||
from parameterized import parameterized
|
||||
|
||||
from evennia import create_object
|
||||
from evennia.utils.test_resources import BaseEvenniaCommandTest, EvenniaTestCase
|
||||
|
||||
from . import extended_room
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ This changes the default map width/height. 2-5 for most clients is sensible.
|
|||
If you don't want the player to be able to specify the size of the map, ignore any
|
||||
arguments passed into the Map command.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ Tests of ingame_map_display.
|
|||
|
||||
"""
|
||||
|
||||
|
||||
from typeclasses import exits, rooms
|
||||
|
||||
from evennia.commands.default.tests import BaseEvenniaCommandTest
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ Tests of simpledoor.
|
|||
|
||||
"""
|
||||
|
||||
|
||||
from evennia.commands.default.tests import BaseEvenniaCommandTest
|
||||
|
||||
from . import simpledoor
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
XYZGrid - Griatch 2021
|
||||
|
||||
"""
|
||||
|
||||
from . import (
|
||||
example,
|
||||
launchcmd,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ and/or
|
|||
{'prototype_parent': 'xyz_exit', ...}
|
||||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -3,14 +3,15 @@
|
|||
Tests for the XYZgrid system.
|
||||
|
||||
"""
|
||||
|
||||
from random import randint
|
||||
from unittest import mock
|
||||
|
||||
from django.test import TestCase
|
||||
from evennia.utils.test_resources import (BaseEvenniaCommandTest,
|
||||
BaseEvenniaTest)
|
||||
from parameterized import parameterized
|
||||
|
||||
from evennia.utils.test_resources import BaseEvenniaCommandTest, BaseEvenniaTest
|
||||
|
||||
from . import commands, xymap, xymap_legend, xyzgrid, xyzroom
|
||||
|
||||
MAP1 = """
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ See `./example.py` for a full grid example.
|
|||
|
||||
----
|
||||
"""
|
||||
|
||||
import pickle
|
||||
from collections import defaultdict
|
||||
from os import mkdir
|
||||
|
|
@ -108,6 +109,7 @@ except ImportError as err:
|
|||
"the SciPy package. Install with `pip install scipy'."
|
||||
)
|
||||
from django.conf import settings
|
||||
|
||||
from evennia.prototypes import prototypes as protlib
|
||||
from evennia.prototypes.spawner import flatten_prototype
|
||||
from evennia.utils import logger
|
||||
|
|
@ -172,6 +174,7 @@ class XYMap:
|
|||
but recommended for readability!
|
||||
|
||||
"""
|
||||
|
||||
mapcorner_symbol = "+"
|
||||
max_pathfinding_length = 500
|
||||
empty_symbol = " "
|
||||
|
|
@ -475,10 +478,10 @@ class XYMap:
|
|||
max_X, max_Y = max(max_X, iX), max(max_Y, iY)
|
||||
node_index += 1
|
||||
|
||||
xygrid[ix][iy] = XYgrid[iX][iY] = node_index_map[
|
||||
node_index
|
||||
] = mapnode_or_link_class(
|
||||
x=ix, y=iy, Z=self.Z, node_index=node_index, symbol=char, xymap=self
|
||||
xygrid[ix][iy] = XYgrid[iX][iY] = node_index_map[node_index] = (
|
||||
mapnode_or_link_class(
|
||||
x=ix, y=iy, Z=self.Z, node_index=node_index, symbol=char, xymap=self
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
|
|
@ -668,8 +671,7 @@ class XYMap:
|
|||
"""
|
||||
global _XYZROOMCLASS
|
||||
if not _XYZROOMCLASS:
|
||||
from evennia.contrib.grid.xyzgrid.xyzroom import \
|
||||
XYZRoom as _XYZROOMCLASS
|
||||
from evennia.contrib.grid.xyzgrid.xyzroom import XYZRoom as _XYZROOMCLASS
|
||||
x, y = xy
|
||||
wildcard = "*"
|
||||
spawned = []
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ import uuid
|
|||
from collections import defaultdict
|
||||
|
||||
from django.core import exceptions as django_exceptions
|
||||
|
||||
from evennia.prototypes import spawner
|
||||
from evennia.utils.utils import class_from_module
|
||||
|
||||
from .utils import (BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError,
|
||||
MapParserError)
|
||||
from .utils import BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError, MapParserError
|
||||
|
||||
NodeTypeclass = None
|
||||
ExitTypeclass = None
|
||||
|
|
@ -327,6 +327,13 @@ class MapNode:
|
|||
nodeobj, err = Typeclass.create(self.prototype.get("key", "An empty room"), xyz=xyz)
|
||||
if err:
|
||||
raise RuntimeError(err)
|
||||
except django_exceptions.MultipleObjectsReturned:
|
||||
raise MapError(
|
||||
f"Multiple objects found: {NodeTypeclass.objects.filter_xyz(xyz=xyz)}. "
|
||||
"This may be due to manual creation of XYZRooms at this position. "
|
||||
"Delete duplicates.",
|
||||
self,
|
||||
)
|
||||
else:
|
||||
self.log(f" updating existing room (if changed) at xyz={xyz}")
|
||||
|
||||
|
|
@ -844,6 +851,7 @@ class SmartRerouterMapLink(MapLink):
|
|||
/|
|
||||
|
||||
"""
|
||||
|
||||
multilink = True
|
||||
|
||||
def get_direction(self, start_direction):
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ The grid has three main functions:
|
|||
|
||||
|
||||
"""
|
||||
|
||||
from evennia.scripts.scripts import DefaultScript
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.utils import variable_from_module
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ class XYZRoom(DefaultRoom):
|
|||
|
||||
def __repr__(self):
|
||||
x, y, z = self.xyz
|
||||
return f"<XYZRoom '{self.db_key}', XYZ=({x},{y},{z})>"
|
||||
return f"<{self.__class__.__name__} '{self.db_key}', XYZ=({x},{y},{z})>"
|
||||
|
||||
@property
|
||||
def xyz(self):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
from .buff import (BaseBuff, BuffableProperty, BuffHandler, CmdBuff, # noqa
|
||||
Mod, cleanup_buffs, tick_buff)
|
||||
from .samplebuffs import (Exploit, Exploited, Leeching, Poison, Sated, # noqa
|
||||
StatBuff)
|
||||
from .buff import CmdBuff # noqa
|
||||
from .buff import BaseBuff, BuffableProperty, BuffHandler, Mod, cleanup_buffs, tick_buff
|
||||
from .samplebuffs import Exploit, Exploited, Leeching, Poison, Sated, StatBuff # noqa
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ You can see all the features of the `BaseBuff` class below, or browse `samplebuf
|
|||
many attributes and hook methods you can overload to create complex, interrelated buffs.
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
from random import random
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
Tests for the buff system contrib
|
||||
"""
|
||||
|
||||
from unittest.mock import Mock, call, patch
|
||||
|
||||
from evennia import DefaultObject, create_object
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ and examples, including how to allow players to choose and confirm
|
|||
character names from within the menu.
|
||||
|
||||
"""
|
||||
|
||||
import string
|
||||
from random import choices
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ of the roll separately:
|
|||
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from ast import literal_eval
|
||||
from random import randint
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ This allows to quickly build a large corpus of translated words
|
|||
that never change (if this is desired).
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from random import choice, randint
|
||||
|
|
|
|||
|
|
@ -148,19 +148,25 @@ Extra Installation Instructions:
|
|||
`type/reset/force me = typeclasses.characters.Character`
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from string import punctuation
|
||||
|
||||
import inflect
|
||||
from django.conf import settings
|
||||
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.commands.command import Command
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.objects.objects import DefaultCharacter, DefaultObject
|
||||
from evennia.utils import ansi, logger
|
||||
from evennia.utils.utils import (iter_to_str, lazy_property, make_iter,
|
||||
variable_from_module)
|
||||
from evennia.utils.utils import (
|
||||
iter_to_str,
|
||||
lazy_property,
|
||||
make_iter,
|
||||
variable_from_module,
|
||||
)
|
||||
|
||||
_INFLECT = inflect.engine()
|
||||
|
||||
|
|
@ -1277,6 +1283,8 @@ class RPSystemCmdSet(CmdSet):
|
|||
Mix-in for adding rp-commands to default cmdset.
|
||||
"""
|
||||
|
||||
key = "rpsystem_cmdset"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdEmote())
|
||||
self.add(CmdSay())
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@
|
|||
Tests for RP system
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from anything import Anything
|
||||
|
||||
from evennia import DefaultObject, create_object, default_cmds
|
||||
from evennia.commands.default import building
|
||||
from evennia.commands.default.tests import BaseEvenniaCommandTest
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
||||
|
|
@ -413,10 +416,9 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
|
|||
|
||||
expected_first_call = [
|
||||
"More than one match for 'Mushroom' (please narrow target):",
|
||||
f" Mushroom-1 []",
|
||||
f" Mushroom-2 []",
|
||||
f" Mushroom-1",
|
||||
f" Mushroom-2",
|
||||
]
|
||||
|
||||
self.call(default_cmds.CmdLook(), "Mushroom", "\n".join(expected_first_call)) # PASSES
|
||||
|
||||
expected_second_call = f"Mushroom(#{mushroom1.id})\nThe first mushroom is brown."
|
||||
|
|
@ -424,3 +426,13 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
|
|||
|
||||
expected_third_call = f"Mushroom(#{mushroom2.id})\nThe second mushroom is red."
|
||||
self.call(default_cmds.CmdLook(), "Mushroom-2", expected_third_call) # FAILS
|
||||
|
||||
expected_fourth_call = "Alias(es) for 'Mushroom' set to 'fungus'."
|
||||
self.call(building.CmdSetObjAlias(), "Mushroom-1 = fungus", expected_fourth_call) # PASSES
|
||||
|
||||
expected_fifth_call = [
|
||||
"More than one match for 'Mushroom' (please narrow target):",
|
||||
f" Mushroom-1 [fungus]",
|
||||
f" Mushroom-2",
|
||||
]
|
||||
self.call(default_cmds.CmdLook(), "Mushroom", "\n".join(expected_fifth_call)) # PASSES
|
||||
|
|
|
|||
|
|
@ -452,7 +452,6 @@ class Character(DefaultCharacter):
|
|||
|
||||
"""
|
||||
|
||||
|
||||
from functools import total_ordering
|
||||
from time import time
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ The script will only send messages to the object it is stored on, so
|
|||
make sure to put it on yourself or you won't see any messages!
|
||||
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
from evennia import DefaultScript
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
Tests for the bodyfunctions.
|
||||
|
||||
"""
|
||||
|
||||
from mock import Mock, patch
|
||||
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ class AIHandler:
|
|||
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
self.ai_state = obj.attributes.get(self.attribute_name,
|
||||
category=self.attribute_category,
|
||||
default="idle")
|
||||
self.ai_state = obj.attributes.get(
|
||||
self.attribute_name, category=self.attribute_category, default="idle"
|
||||
)
|
||||
|
||||
def set_state(self, state):
|
||||
self.ai_state = state
|
||||
|
|
@ -122,6 +122,7 @@ class AIMixin:
|
|||
of multiple inheritance. In a real game, you would probably want to use a mixin like this.
|
||||
|
||||
"""
|
||||
|
||||
@lazy_property
|
||||
def ai(self):
|
||||
return AIHandler(self)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
EvAdventure character generation.
|
||||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from evennia.objects.models import ObjectDB
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ action that takes several vulnerable turns to complete.
|
|||
|
||||
"""
|
||||
|
||||
|
||||
import random
|
||||
from collections import defaultdict
|
||||
|
||||
|
|
@ -843,5 +842,7 @@ class TurnCombatCmdSet(CmdSet):
|
|||
CmdSet for the turn-based combat.
|
||||
"""
|
||||
|
||||
key = "turncombat_cmdset"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdTurnAttack())
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ This implements a 'twitch' (aka DIKU or other traditional muds) style of MUD com
|
|||
----
|
||||
|
||||
"""
|
||||
|
||||
from evennia import AttributeProperty, CmdSet, default_cmds
|
||||
from evennia.commands.command import Command, InterruptCommand
|
||||
from evennia.utils.utils import (
|
||||
|
|
@ -560,6 +561,8 @@ class TwitchCombatCmdSet(CmdSet):
|
|||
Add to character, to be able to attack others in a twitch-style way.
|
||||
"""
|
||||
|
||||
key = "twitch_combat_cmdset"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdAttack())
|
||||
self.add(CmdHold())
|
||||
|
|
@ -573,5 +576,7 @@ class TwitchLookCmdSet(CmdSet):
|
|||
This will be added/removed dynamically when in combat.
|
||||
"""
|
||||
|
||||
key = "twitch_look_cmdset"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdLook())
|
||||
|
|
|
|||
|
|
@ -105,10 +105,10 @@ class EvAdventureDungeonRoom(EvAdventureRoom):
|
|||
|
||||
"""
|
||||
self.tags.add("not_clear", category="dungeon_room")
|
||||
|
||||
def clear_room(self):
|
||||
self.tags.remove("not_clear", category="dungeon_room")
|
||||
|
||||
|
||||
@property
|
||||
def is_room_clear(self):
|
||||
return not bool(self.tags.get("not_clear", category="dungeon_room"))
|
||||
|
|
@ -146,9 +146,7 @@ class EvAdventureDungeonExit(DefaultExit):
|
|||
dungeon_branch = self.location.db.dungeon_branch
|
||||
if target_location == self.location:
|
||||
# destination points back to us - create a new room
|
||||
self.destination = target_location = dungeon_branch.new_room(
|
||||
self
|
||||
)
|
||||
self.destination = target_location = dungeon_branch.new_room(self)
|
||||
dungeon_branch.register_exit_traversed(self)
|
||||
|
||||
super().at_traverse(traversing_object, target_location, **kwargs)
|
||||
|
|
|
|||
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