From 87c43ccce04579ad451d93a2caa960fcd69c1337 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 28 Nov 2020 22:58:07 +0100 Subject: [PATCH] Add documentation for new crafting contrib --- CHANGELOG.md | 6 +- docs/pylib/auto_link_remapper.py | 2 +- docs/source/Components/Components-Overview.md | 2 +- docs/source/Components/Objects.md | 2 +- ...pawner-and-Prototypes.md => Prototypes.md} | 2 +- docs/source/Contribs/Contrib-Overview.md | 32 ++ docs/source/Contribs/Crafting.md | 214 +++++++++++++ docs/source/Evennia-API.md | 2 +- .../Part1/Evennia-Library-Overview.md | 2 +- .../Howto/Starting/Part1/Gamedir-Overview.md | 2 +- .../api/evennia.contrib.crafting.crafting.rst | 7 + ...ennia.contrib.crafting.example_recipes.rst | 7 + docs/source/api/evennia.contrib.crafting.rst | 16 + .../api/evennia.contrib.crafting.tests.rst | 7 + .../evennia.contrib.evscaperoom.commands.rst | 7 + .../api/evennia.contrib.evscaperoom.menu.rst | 7 + .../evennia.contrib.evscaperoom.objects.rst | 7 + .../api/evennia.contrib.evscaperoom.room.rst | 7 + .../api/evennia.contrib.evscaperoom.rst | 21 ++ .../evennia.contrib.evscaperoom.scripts.rst | 7 + .../api/evennia.contrib.evscaperoom.state.rst | 7 + .../api/evennia.contrib.evscaperoom.tests.rst | 7 + .../api/evennia.contrib.evscaperoom.utils.rst | 7 + docs/source/api/evennia.contrib.rst | 2 + docs/source/toc.md | 3 +- evennia/VERSION.txt | 2 +- evennia/contrib/crafting/__init__.py | 0 evennia/contrib/crafting/crafting.py | 294 ++++++++++++------ evennia/contrib/crafting/example_recipes.py | 128 +++++--- evennia/contrib/crafting/tests.py | 204 ++++++------ evennia/contrib/evscaperoom/__init__.py | 0 evennia/contrib/evscaperoom/tests.py | 9 +- 32 files changed, 765 insertions(+), 257 deletions(-) rename docs/source/Components/{Spawner-and-Prototypes.md => Prototypes.md} (99%) create mode 100644 docs/source/Contribs/Crafting.md create mode 100644 docs/source/api/evennia.contrib.crafting.crafting.rst create mode 100644 docs/source/api/evennia.contrib.crafting.example_recipes.rst create mode 100644 docs/source/api/evennia.contrib.crafting.rst create mode 100644 docs/source/api/evennia.contrib.crafting.tests.rst create mode 100644 docs/source/api/evennia.contrib.evscaperoom.commands.rst create mode 100644 docs/source/api/evennia.contrib.evscaperoom.menu.rst create mode 100644 docs/source/api/evennia.contrib.evscaperoom.objects.rst create mode 100644 docs/source/api/evennia.contrib.evscaperoom.room.rst create mode 100644 docs/source/api/evennia.contrib.evscaperoom.rst create mode 100644 docs/source/api/evennia.contrib.evscaperoom.scripts.rst create mode 100644 docs/source/api/evennia.contrib.evscaperoom.state.rst create mode 100644 docs/source/api/evennia.contrib.evscaperoom.tests.rst create mode 100644 docs/source/api/evennia.contrib.evscaperoom.utils.rst create mode 100644 evennia/contrib/crafting/__init__.py create mode 100644 evennia/contrib/evscaperoom/__init__.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e5e78b31b8..925f37a022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,10 +24,12 @@ - Make IP throttle use Django-based cache system for optional persistence (PR by strikaco) - Renamed Tutorial classes "Weapon" and "WeaponRack" to "TutorialWeapon" and "TutorialWeaponRack" to prevent collisions with classes in mygame +- New `crafting` contrib, adding a full crafting subsystem (Griatch 2020) -### Evennia 0.9.5 +### Evennia 0.9.5 (2019-2020) -A transitional release, including new doc system +Released 2020-11-14. +A transitional release, including new doc system. - `is_typeclass(obj (Object), exact (bool))` now defaults to exact=False - `py` command now reroutes stdout to output results in-game client. `py` diff --git a/docs/pylib/auto_link_remapper.py b/docs/pylib/auto_link_remapper.py index 3ab48719a3..2ec27ac02d 100644 --- a/docs/pylib/auto_link_remapper.py +++ b/docs/pylib/auto_link_remapper.py @@ -46,7 +46,7 @@ URL_REMAPS = { "Starting/Adding-Command-Tutorial": "Adding-Commands", "Adding-Command-Tutorial": "Adding-Commands", "CmdSet": "Command-Sets", - "Spawner": "Spawner-and-Prototypes", + "Spawner": "Prototypes", "issue": "github:issue", "issues": "github:issue", "bug": "github:issue", diff --git a/docs/source/Components/Components-Overview.md b/docs/source/Components/Components-Overview.md index 8bee47ba6a..c7cf21c449 100644 --- a/docs/source/Components/Components-Overview.md +++ b/docs/source/Components/Components-Overview.md @@ -15,7 +15,7 @@ than, the doc-strings of each component in the [API](../Evennia-API). - [Attributes](./Attributes) - [Nicks](./Nicks) - [Tags](./Tags) -- [Spawner and prototypes](./Spawner-and-Prototypes) +- [Spawner and prototypes](./Prototypes) - [Help entries](./Help-System) ## Commands diff --git a/docs/source/Components/Objects.md b/docs/source/Components/Objects.md index 21c22b2789..388b66fb3d 100644 --- a/docs/source/Components/Objects.md +++ b/docs/source/Components/Objects.md @@ -53,7 +53,7 @@ like [Scripts](./Scripts)). This particular Rose class doesn't really do much, all it does it make sure the attribute `desc`(which is what the `look` command looks for) is pre-set, which is pretty pointless since you will usually want to change this at build time (using the `@desc` command or using the -[Spawner](./Spawner-and-Prototypes)). The `Object` typeclass offers many more hooks that is available +[Spawner](./Prototypes)). The `Object` typeclass offers many more hooks that is available to use though - see next section. ## Properties and functions on Objects diff --git a/docs/source/Components/Spawner-and-Prototypes.md b/docs/source/Components/Prototypes.md similarity index 99% rename from docs/source/Components/Spawner-and-Prototypes.md rename to docs/source/Components/Prototypes.md index 245eb32336..1a4a348fe9 100644 --- a/docs/source/Components/Spawner-and-Prototypes.md +++ b/docs/source/Components/Prototypes.md @@ -119,7 +119,7 @@ Deprecated as of Evennia 0.8: - `ndb_` - sets the value of a non-persistent attribute (`"ndb_"` is stripped from the name). This is simply not useful in a prototype and is deprecated. - `exec` - This accepts a code snippet or a list of code snippets to run. This should not be used - - use callables or [$protfuncs](./Spawner-and-Prototypes#protfuncs) instead (see below). + use callables or [$protfuncs](./Prototypes#protfuncs) instead (see below). ### Prototype values diff --git a/docs/source/Contribs/Contrib-Overview.md b/docs/source/Contribs/Contrib-Overview.md index abc884a0cb..d8bde10abb 100644 --- a/docs/source/Contribs/Contrib-Overview.md +++ b/docs/source/Contribs/Contrib-Overview.md @@ -3,20 +3,52 @@ The [evennia/contrib/](api:evennia.contrib) folder holds Game-specific tools, systems and utilities created by the community. This gathers longer-form documentation associated with particular contribs. +## Crafting +A full, extendable crafting system. + +- [Crafting overview](./Crafting) +- [Crafting API documentation](api:evennia.contrib.crafting.crafting) +- [Example of a sword crafting tree](api:evennia.contrib.crafting.example_recipes) + ## In-Game-Python +Allow Builders to add Python-scripted events to their objects (OBS-not for untrusted users!) + - [A voice-operated elevator using events](./A-voice-operated-elevator-using-events) - [Dialogues using events](./Dialogues-in-events) ## Maps +Solutions for generating and displaying maps in-game. + - [Dynamic in-game map](./Dynamic-In-Game-Map) - [Static in-game map](./Static-In-Game-Map) ## The tutorial-world +The Evennia single-player sole quest. Made to be analyzed to learn. + - [The tutorial world introduction](../Howto/Starting/Part1/Tutorial-World-Introduction) ## Menu-builder +A tool for building using an in-game menu instead of the normal build commands. Meant to +be expanded for the needs of your game. + - [Building Menus](./Building-menus) + + +```toctree:: + :hidden: + + ./Crafting + ../api/evennia.contrib.crafting.crafting + ../api/evennia.contrib.crafting.example_recipes + ./A-voice-operated-elevator-using-events + ./Dialogues-in-events + ./Dynamic-In-Game-Map + ./Static-In-Game-Map + ../Howto/Starting/Part1/Tutorial-World-Introduction + ./Building-menus + +``` \ No newline at end of file diff --git a/docs/source/Contribs/Crafting.md b/docs/source/Contribs/Crafting.md new file mode 100644 index 0000000000..bba6301635 --- /dev/null +++ b/docs/source/Contribs/Crafting.md @@ -0,0 +1,214 @@ +# Crafting system contrib + +_Contrib by Griatch 2020_ +```versionadded:: 1.0 +``` + +This contrib implements a full Crafting system that can be expanded and modified to fit your game. + +- See the [evennia/contrib/crafting/crafting.py API](api:evennia.contrib.crafting.crafting) for installation + instructrions. +- See the [sword example](api:evennia.contrib.crafting.example_recipes) for an example of how to design + a crafting tree for crafting a sword from base elements. + +From in-game it uses the new `craft` command: + +```bash +> craft bread from flour, eggs, salt, water, yeast using oven, roller +> craft bandage from cloth using scissors +``` + +The syntax is `craft [from ,...][ using ,...]`. + +The above example uses the `bread` *recipe* and requires `flour`, `eggs`, `salt`, `water` and `yeast` objects +to be in your inventory. These will be consumed as part of crafting (baking) the bread. + +The `oven` and `roller` are "tools" that can be either in your inventory or in your current location (you are not carrying an oven +around with you after all). Tools are *not* consumed in the crafting. If the added ingredients/tools matches +the requirements of the recipe, a new `bread` object will appear in the crafter's inventory. + +If you wanted, you could also picture recipes without any consumables: + +``` +> craft fireball using wand, spellbook +``` + +With a little creativity, the 'recipe' concept could be adopted to all sorts of things, like puzzles or +magic systems. + +In code, you can craft using the `evennia.contrib.crafting.crafting.craft` function: + +```python +from evennia.contrib.crafting.crafting import craft + +result = craft(caller, *inputs) + +``` +Here, `caller` is the one doing the crafting and `*inputs` is any combination of consumables and/or tool +Objects. The system will identify which is which by the [Tags](../Components/Tags) on them (see below) +The `result` is always a list. + +## Adding new recipes + +A *recipe* is a class inheriting from `evennia.contrib.crafting.crafting.CraftingRecipe`. This class +implements the most common form of crafting - that using in-game objects. Each recipe is a separate class +which gets initialized with the consumables/tools you provide. + +For the `craft` command to find your custom recipes, you need to tell Evennia where they are. Add a new +line to your `mygame/server/conf/settings.py` file, with a list to any new modules with recipe classes. + +```python +CRAFT_RECIPE_MODULES = ["world.myrecipes"] +``` + +(You need to reload after adding this). All global-level classes in these modules (whose names don't start +with underscore) are considered by the system as viable recipes. + +Here we assume you created `mygame/world/myrecipes.py` to match the above example setting: + +```python +# in mygame/world/myrecipes.py + +from evennia.contrib.crafting.crafting import CraftingRecipe + +class WoodenPuppetRecipe(CraftingRecipe): + """A puppet"""" + name = "wooden puppet" # name to refer to this recipe as + tool_tags = ["knife"] + consumable_tags = ["wood"] + output_prototypes = [ + {"key": "A carved wooden doll", + "typeclass": "typeclasses.objects.decorations.Toys", + "desc": "A small carved doll", + ] + +``` + +This specifies what tags to look for in the inputs and a [Prototype](../Components/Prototypes) to spawn +the result on the fly (a recipe could spawn more than one result if needed). Instead of specifying +the full prototype-dict, you could also just provide a list of `prototype_key`s to existing +prototypes you have. + +After reloading the server, this recipe would now be available to use. To try it we should +create materials and tools the recipe understands. + +The recipe looks only for the [Tag](../Components/Tags) of the ingredients. The tag-category used +can be set per-recipe using the (`.consumable_tag_category` and +`.tool_tag_category` respectively). The defaults are `crafting_material` and `crafting_tool`. For +the puppet we need one object with the `wood` tag and another with the `knife` tag: + +```python +from evennia import create_object + +knife = create_object(key="Hobby knife", tags=[("knife", "crafting_tool")]) +wood = create_object(key="Piece of wood", tags[("wood", "crafting_material")]) +``` + +Note that the objects can have any name, all that matters is the tag/tag-category. This means if a +"bayonet" also had the "knife" crafting tag, it could also be used to carve a puppet. This is also +potentially interesting for use in puzzles and to allow users to experiment and find alternatives to +know ingredients. + +Assuming these objects were put in our inventory, we could now craft using the in-game command: + +```bash +> craft wooden puppet from wood using hobby knife +``` +In code we would do + +```python +from evennia.contrub.crafting.crafting import craft +puppet = craft(crafter, "wooden puppet", knife, wood) + +``` +In the call to `craft`, the order of `knife` and `wood` doesn't matter - the recipe will sort out which +is which based on their tags. + +## Deeper customization of recipes + +To understand how to customize recipes further, it helps to understand how they are used directly: + +```python +class MyRecipe(CraftingRecipe): + ... + +# convenient helper to get dummy objects with the right tags +tools, consumables = MyRecipe.seed() + +recipe = MyRecipe(crafter, *(tools + consumables)) +result = recipe.craft() + +``` +This is useful for testing and allows you to use the class directly without adding it to a module +in `settings.CRAFTING_RECIPE_MODULES`. The `seed` class method is useful e.g. for making unit tests. + +Even without modifying more than the class properties, there are a lot of options to set on +the `CraftingRecipe` class. Easiest is to refer to the +[CraftingRecipe api documentation](evennia.contrib.crafting.crafting.html#evennia.contrib.crafting.crafting.CraftingRecipe). +For example, you can customize the validation-error messages, decide if the ingredients have +to be exactly right, if a failure still consumes the ingredients or not, and much more. + +For even more control you can override hooks in your own class: + +- `pre_craft` - this should handle input validation and store its data in `.validated_consumables` and + `validated_tools` respectively. On error, this reports the error to the crafter and raises the + `CraftingValidationError`. +- `do_craft` - this will only be called if `pre_craft` finished without an exception. This should + return the result of the crafting, by spawnging the prototypes. Or the empty list if crafting + fails for some reason. This is the place to add skill-checks or random chance if you need it + for your game. +- `post_craft` - this receives the result from `do_craft` and handles error messages and also deletes + any consumables as needed. It may also modify the result before returning it. +- `msg` - this is a wrapper for `self.crafter.msg` and should be used to send messages to the + crafter. Centralizing this means you can also easily modify the sending style in one place later. + +The class constructor (and the `craft` access function) takes optional `**kwargs`. These are passed +into each crafting hook. These are unused by default but could be used to customize things per-call. + +### Skilled crafters + +What the crafting system does not have out of the box is a 'skill' system - the notion of being able +to fail the craft if you are not skilled enough. Just how skills work is game-dependent, so to add +this you need to make your own recipe parent class and have your recipes inherit from this. + + +```python +from random import randint +from evennia.contrib.crafting.crafting import CraftingRecipe + +class SkillRecipe(CraftingRecipe): + """A recipe that considers skill""" + + difficulty = 20 + + def do_craft(self, **kwargs): + """The input is ok. Determine if crafting succeeds""" + + # this is set at initialization + crafter = self.crafte + + # let's assume the skill is stored directly on the crafter + # - the skill is 0..100. + crafting_skill = crafter.db.skill_crafting + # roll for success: + if randint(1, 100) <= (crafting_skill - self.difficulty): + # all is good, craft away + return super().do_craft() + else: + self.msg("You are not good enough to craft this. Better luck next time!") + return [] +``` +In this example we introduce a `.difficulty` for the recipe and makes a 'dice roll' to see +if we succed. We would of course make this a lot more immersive and detailed in a full game. In +principle you could customize each recipe just the way you want it, but you could also inherit from +a central parent like this to cut down on work. + +The [sword recipe example module](api:evennia.contrib.crafting.example_recipes) also shows an example +of a random skill-check being implemented in a parent and then inherited for multiple use. + +## Even more customization + +The base class `evennia.contrib.crafting.crafting.CraftingRecipeBase` implements just the minimum +needed to be a recipe. It doesn't know about Objects or tags. If you want to adopt the crafting system +for something entirely different (maybe using different input or validation logic), starting from this +may be cleaner than overriding things in the more opinionated `CraftingRecipe`. \ No newline at end of file diff --git a/docs/source/Evennia-API.md b/docs/source/Evennia-API.md index 2e3611d365..421cfa660c 100644 --- a/docs/source/Evennia-API.md +++ b/docs/source/Evennia-API.md @@ -70,7 +70,7 @@ The flat API is defined in `__init__.py` [viewable here](github:evennia/__init__ - [evennia.gametime](api:evennia.utils.gametime) - server run- and game time ([docs](Components/Coding-Utils#gametime)) - [evennia.logger](api:evennia.utils.logger) - logging tools - [evennia.ansi](api:evennia.utils.ansi) - ansi coloring tools -- [evennia.spawn](api:evennia.prototypes.spawner#evennia.prototypes.spawner.Spawn) - spawn/prototype system ([docs](Components/Spawner-and-Prototypes)) +- [evennia.spawn](api:evennia.prototypes.spawner#evennia.prototypes.spawner.Spawn) - spawn/prototype system ([docs](Components/Prototypes)) - [evennia.lockfuncs](api:evennia.locks.lockfuncs) - default lock functions for access control ([docs](Components/Locks)) - [evennia.EvMenu](api:evennia.utils.evmenu#evennia.utils.evmenu.EvMenu) - menu system ([docs](Components/EvMenu)) - [evennia.EvTable](api:evennia.utils.evtable#evennia.utils.evtable.EvTable) - text table creater diff --git a/docs/source/Howto/Starting/Part1/Evennia-Library-Overview.md b/docs/source/Howto/Starting/Part1/Evennia-Library-Overview.md index 2759df207b..795561c73d 100644 --- a/docs/source/Howto/Starting/Part1/Evennia-Library-Overview.md +++ b/docs/source/Howto/Starting/Part1/Evennia-Library-Overview.md @@ -62,7 +62,7 @@ from here to `mygame/server/settings.py` file. - `locale/` - Language files ([i18n](../../../Concepts/Internationalization)). - [`locks/`](../../../Components/Locks) - Lock system for restricting access to in-game entities. - [`objects/`](../../../Components/Objects) - In-game entities (all types of items and Characters). - - [`prototypes/`](../../../Components/Spawner-and-Prototypes) - Object Prototype/spawning system and OLC menu + - [`prototypes/`](../../../Components/Prototypes) - Object Prototype/spawning system and OLC menu - [`accounts/`](../../../Components/Accounts) - Out-of-game Session-controlled entities (accounts, bots etc) - [`scripts/`](../../../Components/Scripts) - Out-of-game entities equivalence to Objects, also with timer support. - [`server/`](../../../Components/Portal-And-Server) - Core server code and Session handling. diff --git a/docs/source/Howto/Starting/Part1/Gamedir-Overview.md b/docs/source/Howto/Starting/Part1/Gamedir-Overview.md index 23416dd756..c18616d2f4 100644 --- a/docs/source/Howto/Starting/Part1/Gamedir-Overview.md +++ b/docs/source/Howto/Starting/Part1/Gamedir-Overview.md @@ -201,7 +201,7 @@ people change and re-structure this in various ways to better fit their ideas. - [batch_cmds.ev](github:evennia/game_template/world/batch_cmds.ev) - This is an `.ev` file, which is essentially just a list of Evennia commands to execute in sequence. This one is empty and ready to expand on. The [Tutorial World](./Tutorial-World-Introduction) was built with such a batch-file. -- [prototypes.py](github:evennia/game_template/world/prototypes.py) - A [prototype](../../../Components/Spawner-and-Prototypes) is a way +- [prototypes.py](github:evennia/game_template/world/prototypes.py) - A [prototype](../../../Components/Prototypes) is a way to easily vary objects without changing their base typeclass. For example, one could use prototypes to tell that Two goblins, while both of the class 'Goblin' (so they follow the same code logic), should have different equipment, stats and looks. diff --git a/docs/source/api/evennia.contrib.crafting.crafting.rst b/docs/source/api/evennia.contrib.crafting.crafting.rst new file mode 100644 index 0000000000..fcc8cf0b1a --- /dev/null +++ b/docs/source/api/evennia.contrib.crafting.crafting.rst @@ -0,0 +1,7 @@ +evennia.contrib.crafting.crafting +======================================== + +.. automodule:: evennia.contrib.crafting.crafting + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/evennia.contrib.crafting.example_recipes.rst b/docs/source/api/evennia.contrib.crafting.example_recipes.rst new file mode 100644 index 0000000000..26733502f9 --- /dev/null +++ b/docs/source/api/evennia.contrib.crafting.example_recipes.rst @@ -0,0 +1,7 @@ +evennia.contrib.crafting.example\_recipes +================================================ + +.. automodule:: evennia.contrib.crafting.example_recipes + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/evennia.contrib.crafting.rst b/docs/source/api/evennia.contrib.crafting.rst new file mode 100644 index 0000000000..c24b3ebcd5 --- /dev/null +++ b/docs/source/api/evennia.contrib.crafting.rst @@ -0,0 +1,16 @@ +evennia.contrib.crafting +================================ + +.. automodule:: evennia.contrib.crafting + :members: + :undoc-members: + :show-inheritance: + + + +.. toctree:: + :maxdepth: 6 + + evennia.contrib.crafting.crafting + evennia.contrib.crafting.example_recipes + evennia.contrib.crafting.tests diff --git a/docs/source/api/evennia.contrib.crafting.tests.rst b/docs/source/api/evennia.contrib.crafting.tests.rst new file mode 100644 index 0000000000..f6cd3e51d1 --- /dev/null +++ b/docs/source/api/evennia.contrib.crafting.tests.rst @@ -0,0 +1,7 @@ +evennia.contrib.crafting.tests +===================================== + +.. automodule:: evennia.contrib.crafting.tests + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/evennia.contrib.evscaperoom.commands.rst b/docs/source/api/evennia.contrib.evscaperoom.commands.rst new file mode 100644 index 0000000000..7cfa1127f6 --- /dev/null +++ b/docs/source/api/evennia.contrib.evscaperoom.commands.rst @@ -0,0 +1,7 @@ +evennia.contrib.evscaperoom.commands +=========================================== + +.. automodule:: evennia.contrib.evscaperoom.commands + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/evennia.contrib.evscaperoom.menu.rst b/docs/source/api/evennia.contrib.evscaperoom.menu.rst new file mode 100644 index 0000000000..f06cef17af --- /dev/null +++ b/docs/source/api/evennia.contrib.evscaperoom.menu.rst @@ -0,0 +1,7 @@ +evennia.contrib.evscaperoom.menu +======================================= + +.. automodule:: evennia.contrib.evscaperoom.menu + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/evennia.contrib.evscaperoom.objects.rst b/docs/source/api/evennia.contrib.evscaperoom.objects.rst new file mode 100644 index 0000000000..5fad35fc00 --- /dev/null +++ b/docs/source/api/evennia.contrib.evscaperoom.objects.rst @@ -0,0 +1,7 @@ +evennia.contrib.evscaperoom.objects +========================================== + +.. automodule:: evennia.contrib.evscaperoom.objects + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/evennia.contrib.evscaperoom.room.rst b/docs/source/api/evennia.contrib.evscaperoom.room.rst new file mode 100644 index 0000000000..5e0c7dad0f --- /dev/null +++ b/docs/source/api/evennia.contrib.evscaperoom.room.rst @@ -0,0 +1,7 @@ +evennia.contrib.evscaperoom.room +======================================= + +.. automodule:: evennia.contrib.evscaperoom.room + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/evennia.contrib.evscaperoom.rst b/docs/source/api/evennia.contrib.evscaperoom.rst new file mode 100644 index 0000000000..3bb5625be4 --- /dev/null +++ b/docs/source/api/evennia.contrib.evscaperoom.rst @@ -0,0 +1,21 @@ +evennia.contrib.evscaperoom +=================================== + +.. automodule:: evennia.contrib.evscaperoom + :members: + :undoc-members: + :show-inheritance: + + + +.. toctree:: + :maxdepth: 6 + + evennia.contrib.evscaperoom.commands + evennia.contrib.evscaperoom.menu + evennia.contrib.evscaperoom.objects + evennia.contrib.evscaperoom.room + evennia.contrib.evscaperoom.scripts + evennia.contrib.evscaperoom.state + evennia.contrib.evscaperoom.tests + evennia.contrib.evscaperoom.utils diff --git a/docs/source/api/evennia.contrib.evscaperoom.scripts.rst b/docs/source/api/evennia.contrib.evscaperoom.scripts.rst new file mode 100644 index 0000000000..4188d69b3e --- /dev/null +++ b/docs/source/api/evennia.contrib.evscaperoom.scripts.rst @@ -0,0 +1,7 @@ +evennia.contrib.evscaperoom.scripts +========================================== + +.. automodule:: evennia.contrib.evscaperoom.scripts + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/evennia.contrib.evscaperoom.state.rst b/docs/source/api/evennia.contrib.evscaperoom.state.rst new file mode 100644 index 0000000000..9be9727aae --- /dev/null +++ b/docs/source/api/evennia.contrib.evscaperoom.state.rst @@ -0,0 +1,7 @@ +evennia.contrib.evscaperoom.state +======================================== + +.. automodule:: evennia.contrib.evscaperoom.state + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/evennia.contrib.evscaperoom.tests.rst b/docs/source/api/evennia.contrib.evscaperoom.tests.rst new file mode 100644 index 0000000000..da9d7ab4e6 --- /dev/null +++ b/docs/source/api/evennia.contrib.evscaperoom.tests.rst @@ -0,0 +1,7 @@ +evennia.contrib.evscaperoom.tests +======================================== + +.. automodule:: evennia.contrib.evscaperoom.tests + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/evennia.contrib.evscaperoom.utils.rst b/docs/source/api/evennia.contrib.evscaperoom.utils.rst new file mode 100644 index 0000000000..85a1bbc237 --- /dev/null +++ b/docs/source/api/evennia.contrib.evscaperoom.utils.rst @@ -0,0 +1,7 @@ +evennia.contrib.evscaperoom.utils +======================================== + +.. automodule:: evennia.contrib.evscaperoom.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/evennia.contrib.rst b/docs/source/api/evennia.contrib.rst index f3c36d6160..86d18b1f21 100644 --- a/docs/source/api/evennia.contrib.rst +++ b/docs/source/api/evennia.contrib.rst @@ -45,6 +45,8 @@ evennia.contrib :maxdepth: 6 evennia.contrib.awsstorage + evennia.contrib.crafting + evennia.contrib.evscaperoom evennia.contrib.ingame_python evennia.contrib.security evennia.contrib.turnbattle diff --git a/docs/source/toc.md b/docs/source/toc.md index 21f7217eee..5ad330aff6 100644 --- a/docs/source/toc.md +++ b/docs/source/toc.md @@ -37,12 +37,12 @@ - [Components/Objects](Components/Objects) - [Components/Outputfuncs](Components/Outputfuncs) - [Components/Portal And Server](Components/Portal-And-Server) +- [Components/Prototypes](Components/Prototypes) - [Components/Scripts](Components/Scripts) - [Components/Server](Components/Server) - [Components/Server Conf](Components/Server-Conf) - [Components/Sessions](Components/Sessions) - [Components/Signals](Components/Signals) -- [Components/Spawner and Prototypes](Components/Spawner-and-Prototypes) - [Components/Tags](Components/Tags) - [Components/TickerHandler](Components/TickerHandler) - [Components/Typeclasses](Components/Typeclasses) @@ -70,6 +70,7 @@ - [Contribs/Arxcode installing help](Contribs/Arxcode-installing-help) - [Contribs/Building menus](Contribs/Building-menus) - [Contribs/Contrib Overview](Contribs/Contrib-Overview) +- [Contribs/Crafting](Contribs/Crafting) - [Contribs/Dialogues in events](Contribs/Dialogues-in-events) - [Contribs/Dynamic In Game Map](Contribs/Dynamic-In-Game-Map) - [Contribs/Static In Game Map](Contribs/Static-In-Game-Map) diff --git a/evennia/VERSION.txt b/evennia/VERSION.txt index ac39a106c4..15ba4a5203 100644 --- a/evennia/VERSION.txt +++ b/evennia/VERSION.txt @@ -1 +1 @@ -0.9.0 +1.0-dev diff --git a/evennia/contrib/crafting/__init__.py b/evennia/contrib/crafting/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/evennia/contrib/crafting/crafting.py b/evennia/contrib/crafting/crafting.py index cd4c7cdbbe..92ef2ff1d6 100644 --- a/evennia/contrib/crafting/crafting.py +++ b/evennia/contrib/crafting/crafting.py @@ -2,69 +2,88 @@ Crafting - Griatch 2020 This is a general crafting engine. The basic functionality of crafting is to -combine any number of of items in a 'recipe' to produce a new result. This is -useful not only for traditional crafting but also for puzzle-solving or -similar. +combine any number of of items or tools in a 'recipe' to produce a new result. + + item + item + item + tool + tool -> recipe -> new result + +This is useful not only for traditional crafting but the engine is flexible +enough to also be useful for puzzles or similar. ## Installation +- Add the `CmdCraft` Command from this module to your default cmdset. This + allows for crafting from in-game using a simple syntax. - Create a new module and add it to a new list in your settings file - (`server/conf/settings.py`) named `CRAFT_MODULE_RECIPES`. -- In the new module, create one or more classes, each a child of + (`server/conf/settings.py`) named `CRAFT_RECIPES_MODULES`, such as + `CRAFT_RECIPE_MODULES = ["world.recipes_weapons"]`. +- In the new module(s), create one or more classes, each a child of `CraftingRecipe` from this module. Each such class must have a unique `.name` property. It also defines what inputs are required and what is created using this recipe. - Objects to use for crafting should (by default) be tagged with tags using the - tag-category `crafting_material`. The name of the object doesn't matter, only - its tag. -- Add the `CmdCraft` command from this module to your default cmdset. This is a - very simple example-command (your real command will most likely need to do - skill-checks etc!). + tag-category `crafting_material` or `crafting_tool`. The name of the object + doesn't matter, only its tag. -## Usage +## Crafting in game -By default the crafter needs to specify which components -should be used for the recipe: +The default `craft` command handles all crafting needs. +:: - craft spiked club from club, nails + > craft spiked club from club, nails Here, `spiked club` specifies the recipe while `club` and `nails` are objects the crafter must have in their inventory. These will be consumed during crafting (by default only if crafting was successful). -A recipe can also require _tools_. These must be either in inventory or in -the current location. Tools are not consumed during the crafting. +A recipe can also require *tools* (like the `hammer` above). These must be +either in inventory *or* be in the current location. Tools are *not* consumed +during the crafting process. +:: - craft wooden doll from wood with knife + > craft wooden doll from wood with knife + +## Crafting in code In code, you should use the helper function `craft` from this module. This specifies the name of the recipe to use and expects all suitable ingredients/tools as arguments (consumables and tools should be added together, tools will be identified before consumables). - spiked_club = craft(crafter, "spiked club", club, nails) +```python -A fail leads to an empty return. The crafter should already have been notified -of any error in this case (this should be handle by the recipe itself). + from evennia.contrib.crafting import crafting + + spiked_club = crafting.craft(crafter, "spiked club", club, nails) + +``` + +The result is always a list with zero or more objects. A fail leads to an empty +list. The crafter should already have been notified of any error in this case +(this should be handle by the recipe itself). ## Recipes -A _recipe_ works like an input/output blackbox: you put consumables (and/or -tools) into it and if they match the recipe, a new result is spit out. -Consumables are consumed in the process while tools are not. +A *recipe* is a class that works like an input/output blackbox: you initialize +it with consumables (and/or tools) if they match the recipe, a new +result is spit out. Consumables are consumed in the process while tools are not. This module contains a base class for making new ingredient types (`CraftingRecipeBase`) and an implementation of the most common form of crafting (`CraftingRecipe`) using objects and prototypes. Recipes are put in one or more modules added as a list to the -`CRAFT_MODULE_RECIPES` setting, for example: +`CRAFT_RECIPE_MODULES` setting, for example: - CRAFT_MODULE_RECIPES = ['world.recipes_weapons', 'world.recipes_potions'] +```python -Below is an example of a crafting recipe. See the `CraftingRecipe` class for -details of which properties and methods are available to override - the craft -behavior can be modified substantially this way. + CRAFT_RECIPE_MODULES = ['world.recipes_weapons', 'world.recipes_potions'] + +``` + +Below is an example of a crafting recipe and how `craft` calls it under the +hood. See the `CraftingRecipe` class for details of which properties and +methods are available to override - the craft behavior can be modified +substantially this way. ```python @@ -73,7 +92,7 @@ behavior can be modified substantially this way. class PigIronRecipe(CraftingRecipe): # Pig iron is a high-carbon result of melting iron in a blast furnace. - name = "pig iron" + name = "pig iron" # this is what crafting.craft and CmdCraft uses tool_tags = ["blast furnace"] consumable_tags = ["iron ore", "coal", "coal"] output_prototypes = [ @@ -82,18 +101,26 @@ behavior can be modified substantially this way. "tags": [("pig iron", "crafting_material")]} ] + # for testing, conveniently spawn all we need based on the tags on the class + tools, consumables = PigIronRecipe.seed() + + recipe = PigIronRecipe(caller, *(tools + consumables)) + result = recipe.craft() + ``` -The `evennia/contrib/crafting/example_recipes.py` module has more examples of -recipes. +If the above class was added to a module in `CRAFT_RECIPE_MODULES`, it could be +called using its `.name` property, as "pig iron". + +The [example_recipies](api:evennia.contrib.crafting.example_recipes) module has +a full example of the components for creating a sword from base components. ---- """ from copy import copy -from evennia.utils.utils import ( - iter_to_str, callables_from_module, inherits_from, make_iter) +from evennia.utils.utils import iter_to_str, callables_from_module, inherits_from, make_iter from evennia.commands.cmdset import CmdSet from evennia.commands.command import Command from evennia.prototypes.spawner import spawn @@ -109,6 +136,7 @@ def _load_recipes(): """ from django.conf import settings + global _RECIPE_CLASSES if not _RECIPE_CLASSES: paths = ["evennia.contrib.crafting.example_recipes"] @@ -126,12 +154,14 @@ class CraftingError(RuntimeError): """ + class CraftingValidationError(CraftingError): """ Error if crafting validation failed. """ + class CraftingRecipeBase: """ The recipe handles all aspects of performing a 'craft' operation. This is @@ -164,6 +194,7 @@ class CraftingRecipeBase: """ + name = "recipe base" # if set, allow running `.craft` more than once on the same instance. @@ -436,6 +467,7 @@ class CraftingRecipe(CraftingRecipeBase): shown to the crafter automatically """ + name = "crafting recipe" # this define the overall category all material tags must have @@ -458,11 +490,13 @@ class CraftingRecipe(CraftingRecipeBase): error_tool_missing_message = "Could not craft {outputs} without {missing}." # error to show if tool-order matters and it was wrong. Missing is the first # tool out of order - error_tool_order_message = \ + error_tool_order_message = ( "Could not craft {outputs} since {missing} was added in the wrong order." + ) # if .exact_tools is set and there are more than needed - error_tool_excess_message = \ + error_tool_excess_message = ( "Could not craft {outputs} without the exact tools (extra {excess})." + ) # a list of tag-keys (of the `tag_category`). If more than one of each type # is needed, there should be multiple same-named entries in this list. @@ -483,11 +517,13 @@ class CraftingRecipe(CraftingRecipeBase): error_consumable_missing_message = "Could not craft {outputs} without {missing}." # error to show if consumable order matters and it was wrong. Missing is the first # consumable out of order - error_consumable_order_message = \ + error_consumable_order_message = ( "Could not craft {outputs} since {missing} was added in the wrong order." + ) # if .exact_consumables is set and there are more than needed - error_consumable_excess_message = \ + error_consumable_excess_message = ( "Could not craft {outputs} without the exact ingredients (extra {excess})." + ) # this is a list of one or more prototypes (prototype_keys to existing # prototypes or full prototype-dicts) to use to build the result. All of @@ -503,7 +539,7 @@ class CraftingRecipe(CraftingRecipeBase): # show after a successful craft success_message = "You successfully craft {outputs}!" - def __init__(self, crafter, *inputs, **kwargs): + def __init__(self, crafter, *inputs, **kwargs): """ Args: crafter (Object): The one doing the crafting. @@ -528,32 +564,37 @@ class CraftingRecipe(CraftingRecipeBase): # validate class properties if self.consumable_names: - assert len(self.consumable_names) == len(self.consumable_tags), \ - f"Crafting {self.__class__}.consumable_names list must " \ + assert len(self.consumable_names) == len(self.consumable_tags), ( + f"Crafting {self.__class__}.consumable_names list must " "have the same length as .consumable_tags." + ) else: self.consumable_names = self.consumable_tags if self.tool_names: - assert len(self.tool_names) == len(self.tool_tags), \ - f"Crafting {self.__class__}.tool_names list must " \ + assert len(self.tool_names) == len(self.tool_tags), ( + f"Crafting {self.__class__}.tool_names list must " "have the same length as .tool_tags." + ) else: self.tool_names = self.tool_tags if self.output_names: - assert len(self.consumable_names) == len(self.consumable_tags), \ - f"Crafting {self.__class__}.output_names list must " \ + assert len(self.consumable_names) == len(self.consumable_tags), ( + f"Crafting {self.__class__}.output_names list must " "have the same length as .output_prototypes." + ) else: self.output_names = [ prot.get("key", prot.get("typeclass", "unnamed")) - if isinstance(prot, dict) else str(prot) + if isinstance(prot, dict) + else str(prot) for prot in self.output_prototypes ] - assert isinstance(self.output_prototypes, (list, tuple)), \ - "Crafting {self.__class__}.output_prototypes must be a list or tuple." + assert isinstance( + self.output_prototypes, (list, tuple) + ), "Crafting {self.__class__}.output_prototypes must be a list or tuple." # don't allow reuse if we have consumables. If only tools we can reuse # over and over since nothing changes. @@ -568,14 +609,15 @@ class CraftingRecipe(CraftingRecipeBase): # build template context mapping = {"missing": missing, "excess": excess} - mapping.update({ - f"i{ind}": self.consumable_names[ind] - for ind, name in enumerate(self.consumable_names or self.consumable_tags) - }) - mapping.update({ - f"o{ind}": self.output_names[ind] - for ind, name in enumerate(self.output_names) - }) + mapping.update( + { + f"i{ind}": self.consumable_names[ind] + for ind, name in enumerate(self.consumable_names or self.consumable_tags) + } + ) + mapping.update( + {f"o{ind}": self.output_names[ind] for ind, name in enumerate(self.output_names)} + ) mapping["tools"] = involved_tools mapping["consumables"] = involved_cons @@ -633,18 +675,17 @@ class CraftingRecipe(CraftingRecipeBase): create_object( key=tool_key or (cls.tool_names[itag] if cls.tool_names else tag.capitalize()), tags=[(tag, cls.tool_tag_category), *tool_tags], - **tool_kwargs + **tool_kwargs, ) ) consumables = [] for itag, tag in enumerate(cls.consumable_tags): consumables.append( create_object( - key=cons_key or (cls.consumable_names[itag] if - cls.consumable_names else - tag.capitalize()), + key=cons_key + or (cls.consumable_names[itag] if cls.consumable_names else tag.capitalize()), tags=[(tag, cls.consumable_tag_category), *cons_tags], - **consumable_kwargs + **consumable_kwargs, ) ) return tools, consumables @@ -669,8 +710,15 @@ class CraftingRecipe(CraftingRecipeBase): """ def _check_completeness( - tagmap, taglist, namelist, exact_match, exact_order, - error_missing_message, error_order_message, error_excess_message): + tagmap, + taglist, + namelist, + exact_match, + exact_order, + error_missing_message, + error_order_message, + error_excess_message, + ): """Compare tagmap (inputs) to taglist (required)""" valids = [] for itag, tagkey in enumerate(taglist): @@ -682,8 +730,8 @@ class CraftingRecipe(CraftingRecipeBase): if exact_order: # if we get here order is wrong err = self._format_message( - error_order_message, - missing=obj.get_display_name(looker=self.crafter)) + error_order_message, missing=obj.get_display_name(looker=self.crafter) + ) self.msg(err) raise CraftingValidationError(err) @@ -694,7 +742,8 @@ class CraftingRecipe(CraftingRecipeBase): elif exact_match: err = self._format_message( error_missing_message, - missing=namelist[itag] if namelist else tagkey.capitalize()) + missing=namelist[itag] if namelist else tagkey.capitalize(), + ) self.msg(err) raise CraftingValidationError(err) @@ -703,21 +752,30 @@ class CraftingRecipe(CraftingRecipeBase): # thus this is not an exact match err = self._format_message( error_excess_message, - excess=[obj.get_display_name(looker=self.crafter) for obj in tagmap]) + excess=[obj.get_display_name(looker=self.crafter) for obj in tagmap], + ) self.msg(err) raise CraftingValidationError(err) return valids # get tools and consumables from self.inputs - tool_map = {obj: obj.tags.get(category=self.tool_tag_category, return_list=True) - for obj in self.inputs if obj and hasattr(obj, "tags") and - inherits_from(obj, "evennia.objects.models.ObjectDB")} + tool_map = { + obj: obj.tags.get(category=self.tool_tag_category, return_list=True) + for obj in self.inputs + if obj + and hasattr(obj, "tags") + and inherits_from(obj, "evennia.objects.models.ObjectDB") + } tool_map = {obj: tags for obj, tags in tool_map.items() if tags} - consumable_map = {obj: obj.tags.get(category=self.consumable_tag_category, return_list=True) - for obj in self.inputs - if obj and hasattr(obj, "tags") and obj not in tool_map and - inherits_from(obj, "evennia.objects.models.ObjectDB")} + consumable_map = { + obj: obj.tags.get(category=self.consumable_tag_category, return_list=True) + for obj in self.inputs + if obj + and hasattr(obj, "tags") + and obj not in tool_map + and inherits_from(obj, "evennia.objects.models.ObjectDB") + } consumable_map = {obj: tags for obj, tags in consumable_map.items() if tags} # we set these so they are available for error management at all times, @@ -750,11 +808,13 @@ class CraftingRecipe(CraftingRecipeBase): # all the recipe needs now. if len(tools) != len(self.tool_tags): raise CraftingValidationError( - f"Tools {tools}'s tags do not match expected tags {self.tool_tags}") + f"Tools {tools}'s tags do not match expected tags {self.tool_tags}" + ) if len(consumables) != len(self.consumable_tags): raise CraftingValidationError( f"Consumables {consumables}'s tags do not match " - f"expected tags {self.consumable_tags}") + f"expected tags {self.consumable_tags}" + ) self.validated_tools = tools self.validated_consumables = consumables @@ -816,25 +876,29 @@ class CraftingRecipe(CraftingRecipeBase): def craft(crafter, recipe_name, *inputs, raise_exception=False, **kwargs): """ - Craft a given recipe from a source recipe module. A recipe module is a - Python module containing recipe classes. Note that this requires - `settings.CRAFT_RECIPE_MODULES` to be added to a list of one or more - python-paths to modules holding Recipe-classes. + Access function. Craft a given recipe from a source recipe module. A + recipe module is a Python module containing recipe classes. Note that this + requires `settings.CRAFT_RECIPE_MODULES` to be added to a list of one or + more python-paths to modules holding Recipe-classes. Args: crafter (Object): The one doing the crafting. - recipe_name (str): The `CraftRecipe.name` to use. - *inputs: Suitable ingredients (Objects) to use in the crafting. + recipe_name (str): The `CraftRecipe.name` to use. This uses fuzzy-matching + if the result is unique. + *inputs: Suitable ingredients and/or tools (Objects) to use in the crafting. raise_exception (bool, optional): If crafting failed for whatever - reason, raise `CraftingError`. The user will still be informed by the recipe. - **kwargs: Optional kwargs to pass into the recipe (will passed into recipe.craft). + reason, raise `CraftingError`. The user will still be informed by the + recipe. + **kwargs: Optional kwargs to pass into the recipe (will passed into + recipe.craft). Returns: list: Crafted objects, if any. Raises: - CraftingError: If `raise_exception` is True and crafting failed to produce an output. - KeyError: If `recipe_name` failed to find a matching recipe class. + CraftingError: If `raise_exception` is True and crafting failed to + produce an output. KeyError: If `recipe_name` failed to find a + matching recipe class (or the hit was not precise enough.) Notes: If no recipe_module is given, will look for a list `settings.CRAFT_RECIPE_MODULES` and @@ -846,18 +910,30 @@ def craft(crafter, recipe_name, *inputs, raise_exception=False, **kwargs): RecipeClass = _RECIPE_CLASSES.get(recipe_name, None) if not RecipeClass: - raise KeyError("No recipe in settings.CRAFT_RECIPE_MODULES " - f"has a name matching {recipe_name}") + # try a startswith fuzzy match + matches = [key for key in _RECIPE_CLASSES if key.startswith(recipe_name)] + if not matches: + # try in-match + matches = [key for key in _RECIPE_CLASSES if recipe_name in key] + if len(matches) == 1: + RecipeClass = matches[0] + + if not RecipeClass: + raise KeyError( + f"No recipe in settings.CRAFT_RECIPE_MODULES has a name matching {recipe_name}" + ) recipe = RecipeClass(crafter, *inputs, **kwargs) return recipe.craft(raise_exception=raise_exception) # craft command/cmdset + class CraftingCmdSet(CmdSet): """ Store crafting command. """ + key = "Crafting cmdset" def at_cmdset_creation(self): @@ -883,22 +959,35 @@ class CmdCraft(Command): """ + key = "craft" + locks = "cmd:all()" + help_category = "General" + arg_regex = r"\s|$" + def parse(self): """ - Handle parsing of + Handle parsing of: :: [FROM ] [USING ] + Examples: + :: + + craft snowball from snow + craft puppet from piece of wood using knife + craft bread from flour, butter, water, yeast using owen, bowl, roller + craft fireball using wand, spellbook + """ self.args = args = self.args.strip().lower() recipe, ingredients, tools = "", "", "" - if 'from' in args: + if "from" in args: recipe, *rest = args.split(" from ", 1) rest = rest[0] if rest else "" ingredients, *tools = rest.split(" using ", 1) - elif 'using' in args: + elif "using" in args: recipe, *tools = args.split(" using ", 1) tools = tools[0] if tools else "" @@ -931,13 +1020,19 @@ class CmdCraft(Command): # try to include characters or accounts etc. if not obj: return - if (not inherits_from(obj, "evennia.objects.models.ObjectDB") - or obj.sessions.all() or not obj.access(caller, "craft", default=True)): + if ( + not inherits_from(obj, "evennia.objects.models.ObjectDB") + or obj.sessions.all() + or not obj.access(caller, "craft", default=True) + ): # We don't allow to include puppeted objects nor those with the # 'negative' permission 'nocraft'. - caller.msg(obj.attributes.get( - "crafting_consumable_err_msg", - default=f"{obj.get_display_name(looker=caller)} can't be used for this.")) + caller.msg( + obj.attributes.get( + "crafting_consumable_err_msg", + default=f"{obj.get_display_name(looker=caller)} can't be used for this.", + ) + ) return ingredients.append(obj) @@ -950,9 +1045,12 @@ class CmdCraft(Command): if not obj: return None if not obj.access(caller, "craft", default=True): - caller.msg(obj.attributes.get( - "crafting_tool_err_msg", - default=f"{obj.get_display_name(looker=caller)} can't be used for this.")) + caller.msg( + obj.attributes.get( + "crafting_tool_err_msg", + default=f"{obj.get_display_name(looker=caller)} can't be used for this.", + ) + ) return tools.append(obj) diff --git a/evennia/contrib/crafting/example_recipes.py b/evennia/contrib/crafting/example_recipes.py index c5431f7452..7ca9c5ffd3 100644 --- a/evennia/contrib/crafting/example_recipes.py +++ b/evennia/contrib/crafting/example_recipes.py @@ -1,18 +1,19 @@ """ -Example recipes for the crafting system - how to make a sword. +How to make a sword - example crafting tree for the crafting system. -See the _SwordSmithingBaseRecipe for an example of extendng the recipe with a -mocked 'skill' system (just random chance in our case). The skill system used -is game-specific but likely to be needed for most 'real' crafting systems. +See the `SwordSmithingBaseRecipe` in this module for an example of extendng the +recipe with a mocked 'skill' system (just random chance in our case). The skill +system used is game-specific but likely to be needed for most 'real' crafting +systems. Note that 'tools' are references to the tools used - they don't need to be in the inventory of the crafter. So when 'blast furnace' is given below, it is a reference to a blast furnace used, not suggesting the crafter is carrying it around with them. -:: +## Sword crafting tree - Sword crafting tree +:: # base materials (consumables) @@ -40,6 +41,7 @@ around with them. sword = sword blade + sword guard + sword pommel + sword handle + leather + knife[T] + hammer[T] + furnace[T] +---- """ @@ -52,13 +54,16 @@ class PigIronRecipe(CraftingRecipe): Pig iron is a high-carbon result of melting iron in a blast furnace. """ + name = "pig iron" tool_tags = ["blast furnace"] consumable_tags = ["iron ore", "coal", "coal"] output_prototypes = [ - {"key": "Pig Iron ingot", - "desc": "An ingot of crude pig iron.", - "tags": [("pig iron", "crafting_material")]} + { + "key": "Pig Iron ingot", + "desc": "An ingot of crude pig iron.", + "tags": [("pig iron", "crafting_material")], + } ] @@ -68,13 +73,16 @@ class CrucibleSteelRecipe(CraftingRecipe): crucible produces a medieval level of steel (like damascus steel). """ + name = "crucible steel" tool_tags = ["crucible"] consumable_tags = ["pig iron", "ash", "sand", "coal", "coal"] output_prototypes = [ - {"key": "Crucible steel ingot", - "desc": "An ingot of multi-colored crucible steel.", - "tags": [("crucible steel", "crafting_material")]} + { + "key": "Crucible steel ingot", + "desc": "An ingot of multi-colored crucible steel.", + "tags": [("crucible steel", "crafting_material")], + } ] @@ -87,8 +95,9 @@ class _SwordSmithingBaseRecipe(CraftingRecipe): """ success_message = "Your smithing work bears fruit and you craft {outputs}!" - failed_message = ("You work and work but you are not happy with the result. " - "You need to start over.") + failed_message = ( + "You work and work but you are not happy with the result. You need to start over." + ) def do_craft(self, **kwargs): """ @@ -130,28 +139,35 @@ class SwordBladeRecipe(_SwordSmithingBaseRecipe): part of the sword you hold on to). """ + name = "sword blade" tool_tags = ["hammer", "anvil", "furnace"] consumable_tags = ["crucible steel"] output_prototypes = [ - {"key": "Sword blade", - "desc": "A long blade that may one day become a sword.", - "tags": [("sword blade", "crafting_material")]} + { + "key": "Sword blade", + "desc": "A long blade that may one day become a sword.", + "tags": [("sword blade", "crafting_material")], + } ] + class SwordPommelRecipe(_SwordSmithingBaseRecipe): """ The pommel is the 'button' or 'ball' etc the end of the sword hilt, holding it together. """ + name = "sword pommel" tool_tags = ["hammer", "anvil", "furnace"] consumable_tags = ["crucible steel"] output_prototypes = [ - {"key": "Sword pommel", - "desc": "The pommel for a future sword.", - "tags": [("sword pommel", "crafting_material")]} + { + "key": "Sword pommel", + "desc": "The pommel for a future sword.", + "tags": [("sword pommel", "crafting_material")], + } ] @@ -161,13 +177,16 @@ class SwordGuardRecipe(_SwordSmithingBaseRecipe): sword's blade and also protects the hand when parrying. """ + name = "sword guard" tool_tags = ["hammer", "anvil", "furnace"] consumable_tags = ["crucible steel"] output_prototypes = [ - {"key": "Sword guard", - "desc": "The cross-guard for a future sword.", - "tags": [("sword guard", "crafting_material")]} + { + "key": "Sword guard", + "desc": "The cross-guard for a future sword.", + "tags": [("sword guard", "crafting_material")], + } ] @@ -176,13 +195,16 @@ class RawhideRecipe(CraftingRecipe): Rawhide is animal skin cleaned and stripped of hair. """ + name = "rawhide" tool_tags = ["knife"] consumable_tags = ["fur"] output_prototypes = [ - {"key": "Rawhide", - "desc": "Animal skin, cleaned and with hair removed.", - "tags": [("rawhide", "crafting_material")]} + { + "key": "Rawhide", + "desc": "Animal skin, cleaned and with hair removed.", + "tags": [("rawhide", "crafting_material")], + } ] @@ -193,17 +215,21 @@ class OakBarkRecipe(CraftingRecipe): This produces two outputs - the bark and the cleaned wood. """ + name = "oak bark" tool_tags = ["knife"] consumable_tags = ["oak wood"] output_prototypes = [ - {"key": "Oak bark", - "desc": "Bark of oak, stripped from the core wood.", - "tags": [("oak bark", "crafting_material")]}, - {"key": "Oak Wood (cleaned)", - "desc": "Oakwood core, stripped of bark.", - "tags": [("cleaned oak wood", "crafting_material")]}, - + { + "key": "Oak bark", + "desc": "Bark of oak, stripped from the core wood.", + "tags": [("oak bark", "crafting_material")], + }, + { + "key": "Oak Wood (cleaned)", + "desc": "Oakwood core, stripped of bark.", + "tags": [("cleaned oak wood", "crafting_material")], + }, ] @@ -214,13 +240,16 @@ class LeatherRecipe(CraftingRecipe): 'tanning rack' tool should be required too ... """ + name = "leather" tool_tags = ["cauldron"] consumable_tags = ["rawhide", "oak bark", "water"] output_prototypes = [ - {"key": "Piece of Leather", - "desc": "A piece of leather.", - "tags": [("leather", "crafting_material")]} + { + "key": "Piece of Leather", + "desc": "A piece of leather.", + "tags": [("leather", "crafting_material")], + } ] @@ -231,13 +260,16 @@ class SwordHandleRecipe(CraftingRecipe): is wrapped in leather, but that will be added at the end. """ + name = "sword handle" tool_tags = ["knife"] consumable_tags = ["cleaned oak wood"] output_prototypes = [ - {"key": "Sword handle", - "desc": "Two pieces of wood to be be fitted onto a sword's tang as its handle.", - "tags": [("sword handle", "crafting_material")]} + { + "key": "Sword handle", + "desc": "Two pieces of wood to be be fitted onto a sword's tang as its handle.", + "tags": [("sword handle", "crafting_material")], + } ] @@ -252,17 +284,19 @@ class SwordRecipe(_SwordSmithingBaseRecipe): This covers only a single 'sword' type. """ + name = "sword" tool_tags = ["hammer", "furnace", "knife"] - consumable_tags = ["sword blade", "sword guard", "sword pommel", "sword handle", - "leather"] + consumable_tags = ["sword blade", "sword guard", "sword pommel", "sword handle", "leather"] output_prototypes = [ - {"key": "Sword", - "desc": "A bladed weapon.", - # setting the tag as well - who knows if one can make something from this too! - "tags": [("sword", "crafting_material")]} - # obviously there would be other properties of a 'sword' added here - # too, depending on how combat works in the your game! + { + "key": "Sword", + "desc": "A bladed weapon.", + # setting the tag as well - who knows if one can make something from this too! + "tags": [("sword", "crafting_material")], + } + # obviously there would be other properties of a 'sword' added here + # too, depending on how combat works in the your game! ] # this requires more precision exact_consumable_order = True diff --git a/evennia/contrib/crafting/tests.py b/evennia/contrib/crafting/tests.py index f7727c30af..985ac0bdf9 100644 --- a/evennia/contrib/crafting/tests.py +++ b/evennia/contrib/crafting/tests.py @@ -18,6 +18,7 @@ class TestCraftUtils(TestCase): Test helper utils for crafting. """ + maxDiff = None @override_settings(CRAFT_RECIPE_MODULES=[]) @@ -28,17 +29,17 @@ class TestCraftUtils(TestCase): self.assertEqual( crafting._RECIPE_CLASSES, { - 'crucible steel': example_recipes.CrucibleSteelRecipe, - 'leather': example_recipes.LeatherRecipe, - 'oak bark': example_recipes.OakBarkRecipe, - 'pig iron': example_recipes.PigIronRecipe, - 'rawhide': example_recipes.RawhideRecipe, - 'sword': example_recipes.SwordRecipe, - 'sword blade': example_recipes.SwordBladeRecipe, - 'sword guard': example_recipes.SwordGuardRecipe, - 'sword handle': example_recipes.SwordHandleRecipe, - 'sword pommel': example_recipes.SwordPommelRecipe, - } + "crucible steel": example_recipes.CrucibleSteelRecipe, + "leather": example_recipes.LeatherRecipe, + "oak bark": example_recipes.OakBarkRecipe, + "pig iron": example_recipes.PigIronRecipe, + "rawhide": example_recipes.RawhideRecipe, + "sword": example_recipes.SwordRecipe, + "sword blade": example_recipes.SwordBladeRecipe, + "sword guard": example_recipes.SwordGuardRecipe, + "sword handle": example_recipes.SwordHandleRecipe, + "sword pommel": example_recipes.SwordPommelRecipe, + }, ) @@ -54,6 +55,7 @@ class TestCraftingRecipeBase(TestCase): """ Test the parent recipe class. """ + def setUp(self): self.crafter = mock.MagicMock() self.crafter.msg = mock.MagicMock() @@ -65,7 +67,8 @@ class TestCraftingRecipeBase(TestCase): self.kwargs = {"kw1": 1, "kw2": 2} self.recipe = crafting.CraftingRecipeBase( - self.crafter, self.inp1, self.inp2, self.inp3, **self.kwargs) + self.crafter, self.inp1, self.inp2, self.inp3, **self.kwargs + ) def test_msg(self): """Test messaging to crafter""" @@ -76,9 +79,7 @@ class TestCraftingRecipeBase(TestCase): def test_pre_craft(self): """Test validating hook""" self.recipe.pre_craft() - self.assertEqual( - self.recipe.validated_inputs, (self.inp1, self.inp2, self.inp3) - ) + self.assertEqual(self.recipe.validated_inputs, (self.inp1, self.inp2, self.inp3)) def test_pre_craft_fail(self): """Should rase error if validation fails""" @@ -126,9 +127,11 @@ class _MockRecipe(crafting.CraftingRecipe): tool_tags = ["tool1", "tool2"] consumable_tags = ["cons1", "cons2", "cons3"] output_prototypes = [ - {"key": "Result1", - "prototype_key": "resultprot", - "tags": [("result1", "crafting_material")]} + { + "key": "Result1", + "prototype_key": "resultprot", + "tags": [("result1", "crafting_material")], + } ] @@ -137,6 +140,7 @@ class TestCraftingRecipe(TestCase): """ Test the CraftingRecipe class with one recipe """ + maxDiff = None def setUp(self): @@ -162,19 +166,27 @@ class TestCraftingRecipe(TestCase): def test_error_format(self): """Test the automatic error formatter """ recipe = _MockRecipe( - self.crafter, - self.tool1, self.tool2, self.cons1, self.cons2, self.cons3 + self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3 ) - msg = ("{missing},{tools},{consumables},{inputs},{outputs}" - "{i0},{i1},{o0}") - kwargs = {"missing": "foo", "tools": ["bar", "bar2", "bar3"], - "consumables": ["cons1", "cons2"]} + msg = "{missing},{tools},{consumables},{inputs},{outputs}" "{i0},{i1},{o0}" + kwargs = { + "missing": "foo", + "tools": ["bar", "bar2", "bar3"], + "consumables": ["cons1", "cons2"], + } expected = { - 'missing': 'foo', 'i0': 'cons1', 'i1': 'cons2', 'i2': 'cons3', 'o0': - 'Result1', 'tools': 'bar, bar2 and bar3', 'consumables': 'cons1 and cons2', - 'inputs': 'cons1, cons2 and cons3', 'outputs': 'Result1'} + "missing": "foo", + "i0": "cons1", + "i1": "cons2", + "i2": "cons3", + "o0": "Result1", + "tools": "bar, bar2 and bar3", + "consumables": "cons1 and cons2", + "inputs": "cons1, cons2 and cons3", + "outputs": "Result1", + } result = recipe._format_message(msg, **kwargs) self.assertEqual(result, msg.format(**expected)) @@ -182,16 +194,16 @@ class TestCraftingRecipe(TestCase): def test_craft__success(self): """Test to create a result from the recipe""" recipe = _MockRecipe( - self.crafter, - self.tool1, self.tool2, self.cons1, self.cons2, self.cons3 + self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3 ) result = recipe.craft() self.assertEqual(result[0].key, "Result1") - self.assertEqual(result[0].tags.all(), ['result1', 'resultprot']) + self.assertEqual(result[0].tags.all(), ["result1", "resultprot"]) self.crafter.msg.assert_called_with( - recipe.success_message.format(outputs="Result1"), {"type": "crafting"}) + recipe.success_message.format(outputs="Result1"), {"type": "crafting"} + ) # make sure consumables are gone self.assertIsNone(self.cons1.pk) @@ -208,17 +220,15 @@ class TestCraftingRecipe(TestCase): tools, consumables = _MockRecipe.seed() # this should be a normal successful crafting - recipe = _MockRecipe( - self.crafter, - *(tools + consumables) - ) + recipe = _MockRecipe(self.crafter, *(tools + consumables)) result = recipe.craft() self.assertEqual(result[0].key, "Result1") - self.assertEqual(result[0].tags.all(), ['result1', 'resultprot']) + self.assertEqual(result[0].tags.all(), ["result1", "resultprot"]) self.crafter.msg.assert_called_with( - recipe.success_message.format(outputs="Result1"), {"type": "crafting"}) + recipe.success_message.format(outputs="Result1"), {"type": "crafting"} + ) # make sure consumables are gone for cons in consumables: @@ -229,15 +239,13 @@ class TestCraftingRecipe(TestCase): def test_craft_missing_tool__fail(self): """Fail craft by missing tool2""" - recipe = _MockRecipe( - self.crafter, - self.tool1, self.cons1, self.cons2, self.cons3 - ) + recipe = _MockRecipe(self.crafter, self.tool1, self.cons1, self.cons2, self.cons3) result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( - recipe.error_tool_missing_message.format(outputs="Result1", missing='tool2'), - {"type": "crafting"}) + recipe.error_tool_missing_message.format(outputs="Result1", missing="tool2"), + {"type": "crafting"}, + ) # make sure consumables are still there self.assertIsNotNone(self.cons1.pk) @@ -249,16 +257,13 @@ class TestCraftingRecipe(TestCase): def test_craft_missing_cons__fail(self): """Fail craft by missing cons3""" - recipe = _MockRecipe( - self.crafter, - self.tool1, self.tool2, self.cons1, self.cons2 - ) + recipe = _MockRecipe(self.crafter, self.tool1, self.tool2, self.cons1, self.cons2) result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( - recipe.error_consumable_missing_message.format( - outputs="Result1", missing='cons3'), - {"type": "crafting"}) + recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"), + {"type": "crafting"}, + ) # make sure consumables are still there self.assertIsNotNone(self.cons1.pk) @@ -273,19 +278,16 @@ class TestCraftingRecipe(TestCase): cons4 = create_object(key="cons4", tags=[("cons4", "crafting_material")], nohome=True) - recipe = _MockRecipe( - self.crafter, - self.tool1, self.tool2, self.cons1, self.cons2, cons4 - ) + recipe = _MockRecipe(self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, cons4) recipe.consume_on_fail = True result = recipe.craft() self.assertFalse(result) self.crafter.msg.assert_called_with( - recipe.error_consumable_missing_message.format( - outputs="Result1", missing='cons3'), - {"type": "crafting"}) + recipe.error_consumable_missing_message.format(outputs="Result1", missing="cons3"), + {"type": "crafting"}, + ) # make sure consumables are deleted even though we failed self.assertIsNone(self.cons1.pk) @@ -303,16 +305,15 @@ class TestCraftingRecipe(TestCase): wrong = create_object(key="wrong", tags=[("wrongtool", "crafting_tool")], nohome=True) - recipe = _MockRecipe( - self.crafter, - self.tool1, self.tool2, self.cons1, self.cons2, wrong - ) + recipe = _MockRecipe(self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, wrong) 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"}) + outputs="Result1", excess=wrong.get_display_name(looker=self.crafter) + ), + {"type": "crafting"}, + ) # make sure consumables are still there self.assertIsNotNone(self.cons1.pk) self.assertIsNotNone(self.cons2.pk) @@ -328,15 +329,16 @@ class TestCraftingRecipe(TestCase): tool3 = create_object(key="tool3", tags=[("tool2", "crafting_tool")], nohome=True) recipe = _MockRecipe( - self.crafter, - self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, tool3 + self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, tool3 ) 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"}) + outputs="Result1", excess=tool3.get_display_name(looker=self.crafter) + ), + {"type": "crafting"}, + ) # make sure consumables are still there self.assertIsNotNone(self.cons1.pk) @@ -354,15 +356,16 @@ class TestCraftingRecipe(TestCase): cons4 = create_object(key="cons4", tags=[("cons3", "crafting_material")], nohome=True) recipe = _MockRecipe( - self.crafter, - self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, cons4 + self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, cons4 ) 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"}) + outputs="Result1", excess=cons4.get_display_name(looker=self.crafter) + ), + {"type": "crafting"}, + ) # make sure consumables are still there self.assertIsNotNone(self.cons1.pk) @@ -379,14 +382,14 @@ class TestCraftingRecipe(TestCase): tool3 = create_object(key="tool3", tags=[("tool2", "crafting_tool")], nohome=True) recipe = _MockRecipe( - self.crafter, - self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, tool3 + self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, tool3 ) recipe.exact_tools = False result = recipe.craft() self.assertTrue(result) self.crafter.msg.assert_called_with( - recipe.success_message.format(outputs="Result1"), {"type": "crafting"}) + recipe.success_message.format(outputs="Result1"), {"type": "crafting"} + ) # make sure consumables are gone self.assertIsNone(self.cons1.pk) @@ -402,14 +405,14 @@ class TestCraftingRecipe(TestCase): cons4 = create_object(key="cons4", tags=[("cons3", "crafting_material")], nohome=True) recipe = _MockRecipe( - self.crafter, - self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, cons4 + self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3, cons4 ) recipe.exact_consumables = False result = recipe.craft() self.assertTrue(result) self.crafter.msg.assert_called_with( - recipe.success_message.format(outputs="Result1"), {"type": "crafting"}) + recipe.success_message.format(outputs="Result1"), {"type": "crafting"} + ) # make sure consumables are gone self.assertIsNone(self.cons1.pk) @@ -422,16 +425,17 @@ class TestCraftingRecipe(TestCase): def test_craft_tool_order__fail(self): """Strict tool-order recipe fail """ recipe = _MockRecipe( - self.crafter, - self.tool2, self.tool1, self.cons1, self.cons2, self.cons3 + self.crafter, self.tool2, self.tool1, self.cons1, self.cons2, self.cons3 ) recipe.exact_tool_order = True 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"}) + outputs="Result1", missing=self.tool2.get_display_name(looker=self.crafter) + ), + {"type": "crafting"}, + ) # make sure consumables are still there self.assertIsNotNone(self.cons1.pk) @@ -444,16 +448,17 @@ class TestCraftingRecipe(TestCase): def test_craft_cons_order__fail(self): """Strict tool-order recipe fail """ recipe = _MockRecipe( - self.crafter, - self.tool1, self.tool2, self.cons3, self.cons2, self.cons1 + self.crafter, self.tool1, self.tool2, self.cons3, self.cons2, self.cons1 ) recipe.exact_consumable_order = True 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"}) + outputs="Result1", missing=self.cons3.get_display_name(looker=self.crafter) + ), + {"type": "crafting"}, + ) # make sure consumables are still there self.assertIsNotNone(self.cons1.pk) @@ -469,6 +474,7 @@ class TestCraftSword(TestCase): Test the `craft` function by crafting the example sword. """ + def setUp(self): self.crafter = mock.MagicMock() self.crafter.msg = mock.MagicMock() @@ -578,8 +584,16 @@ class TestCraftSword(TestCase): sword_handle = _craft("sword handle", *inputs) # sword (order matters) - inputs = [sword_blade, sword_guard, sword_pommel, sword_handle, - leather, knife, hammer, furnace] + inputs = [ + sword_blade, + sword_guard, + sword_pommel, + sword_handle, + leather, + knife, + hammer, + furnace, + ] sword = _craft("sword", *inputs) self.assertEqual(sword.key, "Sword") @@ -633,10 +647,8 @@ class TestCraftSword(TestCase): self.assertIsNotNone(cauldron) -@mock.patch("evennia.contrib.crafting.crafting._load_recipes", - new=mock.MagicMock()) -@mock.patch("evennia.contrib.crafting.crafting._RECIPE_CLASSES", - new={"testrecipe": _MockRecipe}) +@mock.patch("evennia.contrib.crafting.crafting._load_recipes", new=mock.MagicMock()) +@mock.patch("evennia.contrib.crafting.crafting._RECIPE_CLASSES", new={"testrecipe": _MockRecipe}) @override_settings(CRAFT_RECIPE_MODULES=[], DEFAULT_HOME="#999999") class TestCraftCommand(CommandTest): """Test the crafting command""" @@ -645,15 +657,15 @@ class TestCraftCommand(CommandTest): super().setUp() tools, consumables = _MockRecipe.seed( - tool_kwargs={"location": self.char1}, - consumable_kwargs={"location": self.char1}) + tool_kwargs={"location": self.char1}, consumable_kwargs={"location": self.char1} + ) def test_craft__success(self): "Successfully craft using command" self.call( crafting.CmdCraft(), "testrecipe from cons1, cons2, cons3 using tool1, tool2", - _MockRecipe.success_message.format(outputs="Result1") + _MockRecipe.success_message.format(outputs="Result1"), ) def test_craft__notools__failure(self): @@ -661,12 +673,12 @@ class TestCraftCommand(CommandTest): self.call( crafting.CmdCraft(), "testrecipe from cons1, cons2, cons3", - _MockRecipe.error_tool_missing_message.format(outputs="Result1", missing="tool1") + _MockRecipe.error_tool_missing_message.format(outputs="Result1", missing="tool1"), ) def test_craft__nocons__failure(self): self.call( crafting.CmdCraft(), "testrecipe using tool1, tool2", - _MockRecipe.error_consumable_missing_message.format(outputs="Result1", missing="cons1") + _MockRecipe.error_consumable_missing_message.format(outputs="Result1", missing="cons1"), ) diff --git a/evennia/contrib/evscaperoom/__init__.py b/evennia/contrib/evscaperoom/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/evennia/contrib/evscaperoom/tests.py b/evennia/contrib/evscaperoom/tests.py index 16f7aecef2..2c839ae1ba 100644 --- a/evennia/contrib/evscaperoom/tests.py +++ b/evennia/contrib/evscaperoom/tests.py @@ -174,12 +174,11 @@ class TestEvscaperoomCommands(CommandTest): self.call(commands.CmdSpeak(), "", "What do you want to say?", cmdstring="") self.call(commands.CmdSpeak(), "Hello!", "You say: Hello!", cmdstring="") self.call(commands.CmdSpeak(), "", "What do you want to whisper?", cmdstring="whisper") - self.call(commands.CmdSpeak(), "Hi.", "You whisper: Hi.", cmdstring="whisper") - self.call(commands.CmdSpeak(), "Hi.", "You whisper: Hi.", cmdstring="whisper") + self.call(commands.CmdSpeak(), "Hi.", "You whisper: (Hi.)", cmdstring="whisper") self.call(commands.CmdSpeak(), "HELLO!", "You shout: HELLO!", cmdstring="shout") - self.call(commands.CmdSpeak(), "Hello to obj", "You say: Hello", cmdstring="say") - self.call(commands.CmdSpeak(), "Hello to obj", "You shout: Hello", cmdstring="shout") + self.call(commands.CmdSpeak(), "Hello", "You say: Hello", cmdstring="say") + self.call(commands.CmdSpeak(), "Hello", "You shout: HELLO", cmdstring="shout") def test_emote(self): self.call( @@ -272,7 +271,7 @@ class TestStates(EvenniaTest): dirname = path.join(path.dirname(__file__), "states") states = [] for imp, module, ispackage in pkgutil.walk_packages( - path=[dirname], prefix="evscaperoom.states." + path=[dirname], prefix="evennia.contrib.evscaperoom.states." ): mod = mod_import(module) states.append(mod)