Start refactor contrib folder
This commit is contained in:
parent
7f0d314e7f
commit
f5f75bd04d
107 changed files with 34 additions and 2 deletions
102
evennia/contrib/game_systems/crafting/README.md
Normal file
102
evennia/contrib/game_systems/crafting/README.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Crafting system
|
||||
|
||||
Contrib - Griatch 2020
|
||||
|
||||
This implements a full crafting system. The principle is that of a 'recipe':
|
||||
|
||||
ingredient1 + ingredient2 + ... + tool1 + tool2 + ... + craft_recipe -> objectA, objectB, ...
|
||||
|
||||
Here, 'ingredients' are consumed by the crafting process, whereas 'tools' are
|
||||
necessary for the process by will not be destroyed by it.
|
||||
|
||||
An example would be to use the tools 'bowl' and 'oven' to use the ingredients
|
||||
'flour', 'salt', 'yeast' and 'water' to create 'bread' using the 'bread recipe'.
|
||||
|
||||
A recipe does not have to use tools, like 'snow' + 'snowball-recipe' becomes
|
||||
'snowball'. Conversely one could also imagine using tools without consumables,
|
||||
like using 'spell book' and 'wand' to produce 'fireball' by having the recipe
|
||||
check some magic skill on the character.
|
||||
|
||||
The system is generic enough to be used also for adventure-like puzzles, like
|
||||
combining 'stick', 'string' and 'hook' to get a 'makeshift fishing rod' that
|
||||
you can use with 'storm drain' (treated as a tool) to get 'key' ...
|
||||
|
||||
## Intallation and Usage
|
||||
|
||||
Import the `CmdCraft` command from evennia/contrib/crafting/crafting.py and
|
||||
add it to your Character cmdset. Reload and the `craft` command will be
|
||||
available to you:
|
||||
|
||||
craft <recipe> [from <ingredient>,...] [using <tool>, ...]
|
||||
|
||||
For example
|
||||
|
||||
craft toy car from plank, wooden wheels, nails using saw, hammer
|
||||
|
||||
To use crafting you need recipes. Add a new variable to `mygame/server/conf/settings.py`:
|
||||
|
||||
CRAFT_RECIPE_MODULES = ['world.recipes']
|
||||
|
||||
All top-level classes in these modules (whose name does not start with `_`)
|
||||
will be parsed by Evennia as recipes to make available to the crafting system.
|
||||
Using the above example, create `mygame/world/recipes.py` and add your recipies
|
||||
in there:
|
||||
|
||||
```python
|
||||
|
||||
from evennia.contrib.crafting.crafting import CraftingRecipe, CraftingValidationError
|
||||
|
||||
|
||||
class RecipeBread(CraftingRecipe):
|
||||
"""
|
||||
Bread is good for making sandwitches!
|
||||
|
||||
"""
|
||||
|
||||
name = "bread" # used to identify this recipe in 'craft' command
|
||||
tool_tags = ["bowl", "oven"]
|
||||
consumable_tags = ["flour", "salt", "yeast", "water"]
|
||||
output_prototypes = [
|
||||
{"key": "Loaf of Bread",
|
||||
"aliases": ["bread"],
|
||||
"desc": "A nice load of bread.",
|
||||
"typeclass": "typeclasses.objects.Food", # assuming this exists
|
||||
"tags": [("bread", "crafting_material")] # this makes it usable in other recipes ...
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
def pre_craft(self, **kwargs):
|
||||
# validates inputs etc. Raise `CraftingValidationError` if fails
|
||||
|
||||
def craft(self, **kwargs):
|
||||
# performs the craft - but it can still fail (check skills etc here)
|
||||
|
||||
def craft(self, result, **kwargs):
|
||||
# any post-crafting effects. Always called, even if crafting failed (be
|
||||
# result would be None then)
|
||||
|
||||
```
|
||||
|
||||
## Technical
|
||||
|
||||
The Recipe is a class that specifies the consumables, tools and output along
|
||||
with various methods (that you can override) to do the the validation of inputs
|
||||
and perform the crafting itself.
|
||||
|
||||
By default the input is a list of object-tags (using the "crafting_material"
|
||||
and "crafting_tool" tag-categories respectively). Providing a set of objects
|
||||
matching these tags are required for the crafting to be done. The use of tags
|
||||
means that multiple different objects could all work for the same recipe, as
|
||||
long as they have the right tag. This can be very useful for allowing players
|
||||
to experiment and explore alternative ways to create things!
|
||||
|
||||
The output is given by a set of prototype-dicts. If the input is correct and
|
||||
other checks are passed (such as crafting skill, for example), these prototypes
|
||||
will be used to generate the new object(s) being crafted.
|
||||
|
||||
Each recipe is a stand-alone entity which allows for very advanced
|
||||
customization for every recipe - for example one could have a recipe that
|
||||
checks other properties of the inputs (like quality, color etc) and have that
|
||||
affect the result. Your recipes could also (and likely would) tie into your
|
||||
game's skill system to determine the success or outcome of the crafting.
|
||||
0
evennia/contrib/game_systems/crafting/__init__.py
Normal file
0
evennia/contrib/game_systems/crafting/__init__.py
Normal file
1068
evennia/contrib/game_systems/crafting/crafting.py
Normal file
1068
evennia/contrib/game_systems/crafting/crafting.py
Normal file
File diff suppressed because it is too large
Load diff
530
evennia/contrib/game_systems/crafting/example_recipes.py
Normal file
530
evennia/contrib/game_systems/crafting/example_recipes.py
Normal file
|
|
@ -0,0 +1,530 @@
|
|||
"""
|
||||
How to make a sword - example crafting tree for the crafting system.
|
||||
|
||||
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
|
||||
|
||||
::
|
||||
|
||||
# base materials (consumables)
|
||||
|
||||
iron ore, ash, sand, coal, oak wood, water, fur
|
||||
|
||||
# base tools (marked with [T] for clarity and assumed to already exist)
|
||||
|
||||
blast furnace[T], furnace[T], crucible[T], anvil[T],
|
||||
hammer[T], knife[T], cauldron[T]
|
||||
|
||||
# recipes for making a sword
|
||||
|
||||
pig iron = iron ore + 2xcoal + blast furnace[T]
|
||||
crucible_steel = pig iron + ash + sand + 2xcoal + crucible[T]
|
||||
sword blade = crucible steel + hammer[T] + anvil[T] + furnace[T]
|
||||
sword pommel = crucible steel + hammer[T] + anvil[T] + furnace[T]
|
||||
sword guard = crucible steel + hammer[T] + anvil[T] + furnace[T]
|
||||
|
||||
rawhide = fur + knife[T]
|
||||
oak bark + cleaned oak wood = oak wood + knife[T]
|
||||
leather = rawhide + oak bark + water + cauldron[T]
|
||||
|
||||
sword handle = cleaned oak wood + knife[T]
|
||||
|
||||
sword = sword blade + sword guard + sword pommel
|
||||
+ sword handle + leather + knife[T] + hammer[T] + furnace[T]
|
||||
|
||||
|
||||
## Recipes used for spell casting
|
||||
|
||||
This is a simple example modifying the base Recipe to use as a way
|
||||
to describe magical spells instead. It combines tools with
|
||||
a skill (an attribute on the caster) in order to produce a magical effect.
|
||||
|
||||
The example `CmdCast` command can be added to the CharacterCmdset in
|
||||
`mygame/commands/default_cmdsets` to test it out. The 'effects' are
|
||||
just mocked for the example.
|
||||
|
||||
::
|
||||
# base tools (assumed to already exist)
|
||||
|
||||
spellbook[T], wand[T]
|
||||
|
||||
# skill (stored as Attribute on caster)
|
||||
|
||||
firemagic skill level10+
|
||||
|
||||
# recipe for fireball
|
||||
|
||||
fireball = spellbook[T] + wand[T] + [firemagic skill lvl10+]
|
||||
|
||||
----
|
||||
|
||||
"""
|
||||
|
||||
from random import random, randint
|
||||
from evennia.commands.command import Command, InterruptCommand
|
||||
from .crafting import craft, CraftingRecipe, CraftingValidationError
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Sword recipe
|
||||
#------------------------------------------------------------
|
||||
|
||||
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")],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class CrucibleSteelRecipe(CraftingRecipe):
|
||||
"""
|
||||
Mixing pig iron with impurities like ash and sand and melting it in a
|
||||
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")],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class _SwordSmithingBaseRecipe(CraftingRecipe):
|
||||
"""
|
||||
A parent for all metallurgy sword-creation recipes. Those have a chance to
|
||||
failure but since steel is not lost in the process you can always try
|
||||
again.
|
||||
|
||||
"""
|
||||
|
||||
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."
|
||||
)
|
||||
|
||||
def craft(self, **kwargs):
|
||||
"""
|
||||
Making a sword blade takes skill. Here we emulate this by introducing a
|
||||
random chance of failure (in a real game this could be a skill check
|
||||
against a skill found on `self.crafter`). In this case you can always
|
||||
start over since steel is not lost but can be re-smelted again for
|
||||
another try.
|
||||
|
||||
Args:
|
||||
validated_inputs (list): all consumables/tools being used.
|
||||
**kwargs: any extra kwargs passed during crafting.
|
||||
|
||||
Returns:
|
||||
any: The result of the craft, or None if a failure.
|
||||
|
||||
Notes:
|
||||
Depending on if we return a crafting result from this
|
||||
method or not, `success_message` or `failure_message`
|
||||
will be echoed to the crafter.
|
||||
|
||||
(for more control we could also message directly and raise
|
||||
crafting.CraftingError to abort craft process on failure).
|
||||
|
||||
"""
|
||||
if random.random() < 0.8:
|
||||
# 80% chance of success. This will spawn the sword and show
|
||||
# success-message.
|
||||
return super().craft(**kwargs)
|
||||
else:
|
||||
# fail and show failed message
|
||||
return None
|
||||
|
||||
|
||||
class SwordBladeRecipe(_SwordSmithingBaseRecipe):
|
||||
"""
|
||||
A [sword]blade requires hammering the steel out into shape using heat and
|
||||
force. This also includes the tang, which is the base for the hilt (the
|
||||
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")],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
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")],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class SwordGuardRecipe(_SwordSmithingBaseRecipe):
|
||||
"""
|
||||
The guard stops the hand from accidentally sliding off the hilt onto the
|
||||
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")],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
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")],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class OakBarkRecipe(CraftingRecipe):
|
||||
"""
|
||||
The actual thing needed for tanning leather is Tannin, but we skip
|
||||
the step of refining tannin from the bark and use the bark as-is.
|
||||
|
||||
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")],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class LeatherRecipe(CraftingRecipe):
|
||||
"""
|
||||
Leather is produced by tanning rawhide in a process traditionally involving
|
||||
the chemical Tannin. Here we abbreviate this process a bit. Maybe a
|
||||
'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")],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class SwordHandleRecipe(CraftingRecipe):
|
||||
"""
|
||||
The handle is the part of the hilt between the guard and the pommel where
|
||||
you hold the sword. It consists of wooden pieces around the steel tang. It
|
||||
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")],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class SwordRecipe(_SwordSmithingBaseRecipe):
|
||||
"""
|
||||
A finished sword consists of a Blade ending in a non-sharp part called the
|
||||
Tang. The cross Guard is put over the tang against the edge of the blade.
|
||||
The Handle is put over the tang to give something easier to hold. The
|
||||
Pommel locks everything in place. The handle is wrapped in leather
|
||||
strips for better grip.
|
||||
|
||||
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"]
|
||||
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!
|
||||
]
|
||||
# this requires more precision
|
||||
exact_consumable_order = True
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Recipes for spell casting
|
||||
#------------------------------------------------------------
|
||||
|
||||
|
||||
class _MagicRecipe(CraftingRecipe):
|
||||
"""
|
||||
A base 'recipe' to represent magical spells.
|
||||
|
||||
We *could* treat this just like the sword above - by combining the wand and spellbook to make a
|
||||
fireball object that the user can then throw with another command. For this example we instead
|
||||
generate 'magical effects' as strings+values that we would then supposedly inject into a
|
||||
combat system or other resolution system.
|
||||
|
||||
We also assume that the crafter has skills set on itself as plain Attributes.
|
||||
|
||||
"""
|
||||
name = ""
|
||||
# all spells require a spellbook and a wand (so there!)
|
||||
tool_tags = ["spellbook", "wand"]
|
||||
|
||||
error_tool_missing_message = "Cannot cast spells without {missing}."
|
||||
success_message = "You successfully cast the spell!"
|
||||
# custom properties
|
||||
skill_requirement = [] # this should be on the form [(skillname, min_level)]
|
||||
skill_roll = "" # skill to roll for success
|
||||
desired_effects = [] # on the form [(effect, value), ...]
|
||||
failure_effects = [] # ''
|
||||
error_too_low_skill_level = "Your skill {skill_name} is too low to cast {spell}."
|
||||
error_no_skill_roll = "You must have the skill {skill_name} to cast the spell {spell}."
|
||||
|
||||
def pre_craft(self, **kwargs):
|
||||
"""
|
||||
This is where we do input validation. We want to do the
|
||||
normal validation of the tools, but also check for a skill
|
||||
on the crafter. This must set the result on `self.validated_inputs`.
|
||||
We also set the crafter's relevant skill value on `self.skill_roll_value`.
|
||||
|
||||
Args:
|
||||
**kwargs: Any optional extra kwargs passed during initialization of
|
||||
the recipe class.
|
||||
|
||||
Raises:
|
||||
CraftingValidationError: If validation fails. At this point the crafter
|
||||
is expected to have been informed of the problem already.
|
||||
|
||||
"""
|
||||
# this will check so the spellbook and wand are at hand.
|
||||
super().pre_craft(**kwargs)
|
||||
|
||||
# at this point we have the items available, let's also check for the skill. We
|
||||
# assume the crafter has the skill available as an Attribute
|
||||
# on itself.
|
||||
|
||||
crafter = self.crafter
|
||||
for skill_name, min_value in self.skill_requirements:
|
||||
skill_value = crafter.attributes.get(skill_name)
|
||||
|
||||
if skill_value is None or skill_value < min_value:
|
||||
self.msg(self.error_too_low_skill_level.format(skill_name=skill_name,
|
||||
spell=self.name))
|
||||
raise CraftingValidationError
|
||||
|
||||
# get the value of the skill to roll
|
||||
self.skill_roll_value = self.crafter.attributes.get(self.skill_roll)
|
||||
if self.skill_roll_value is None:
|
||||
self.msg(self.error_no_skill_roll.format(skill_name=self.skill_roll,
|
||||
spell=self.name))
|
||||
raise CraftingValidationError
|
||||
|
||||
def do_craft(self, **kwargs):
|
||||
"""
|
||||
'Craft' the magical effect. When we get to this point we already know we have all the
|
||||
prequisite for creating the effect. In this example we will store the effect on the crafter;
|
||||
maybe this enhances the crafter or makes a new attack available to them in combat.
|
||||
|
||||
An alternative to this would of course be to spawn an actual object for the effect, like
|
||||
creating a potion or an actual fireball-object to throw (this depends on how your combat
|
||||
works).
|
||||
|
||||
"""
|
||||
# we do a simple skill check here.
|
||||
if randint(1, 18) <= self.skill_roll_value:
|
||||
# a success!
|
||||
return True, self.desired_effects
|
||||
else:
|
||||
# a failure!
|
||||
return False, self.failure_effects
|
||||
|
||||
def post_craft(self, craft_result, **kwargs):
|
||||
"""
|
||||
Always called at the end of crafting, regardless of successful or not.
|
||||
|
||||
Since we get a custom craft result (True/False, effects) we need to
|
||||
wrap the original post_craft to output the error messages for us
|
||||
correctly.
|
||||
|
||||
"""
|
||||
success = False
|
||||
if craft_result:
|
||||
success, _ = craft_result
|
||||
# default post_craft just checks if craft_result is truthy or not.
|
||||
# we don't care about its return value since we already have craft_result.
|
||||
super().post_craft(success, **kwargs)
|
||||
return craft_result
|
||||
|
||||
|
||||
class FireballRecipe(_MagicRecipe):
|
||||
"""
|
||||
A Fireball is a magical effect that can be thrown at a target to cause damage.
|
||||
|
||||
Note that the magic-effects are just examples, an actual rule system would
|
||||
need to be created to understand what they mean when used.
|
||||
|
||||
"""
|
||||
name = "fireball"
|
||||
skill_requirements = [('firemagic', 10)] # skill 'firemagic' lvl 10 or higher
|
||||
skill_roll = "firemagic"
|
||||
success_message = "A ball of flame appears!"
|
||||
desired_effects = [('target_fire_damage', 25), ('ranged_attack', -2), ('mana_cost', 12)]
|
||||
failure_effects = [('self_fire_damage', 5), ('mana_cost', 5)]
|
||||
|
||||
|
||||
class HealingRecipe(_MagicRecipe):
|
||||
"""
|
||||
Healing magic will restore a certain amount of health to the target over time.
|
||||
|
||||
Note that the magic-effects are just examples, an actual rule system would
|
||||
need to be created to understand what they mean.
|
||||
|
||||
"""
|
||||
name = "heal"
|
||||
skill_requirements = [('bodymagic', 5), ("empathy", 10)]
|
||||
skill_roll = "bodymagic"
|
||||
success_message = "You successfully extend your healing aura."
|
||||
desired_effects = [('healing', 15), ('mana_cost', 5)]
|
||||
failure_effects = []
|
||||
|
||||
|
||||
class CmdCast(Command):
|
||||
"""
|
||||
Cast a magical spell.
|
||||
|
||||
Usage:
|
||||
cast <spell> <target>
|
||||
|
||||
"""
|
||||
key = 'cast'
|
||||
|
||||
def parse(self):
|
||||
"""
|
||||
Simple parser, assuming spellname doesn't have spaces.
|
||||
Stores result in self.target and self.spellname.
|
||||
|
||||
"""
|
||||
args = self.args.strip().lower()
|
||||
target = None
|
||||
if ' ' in args:
|
||||
self.spellname, *target = args.split(' ', 1)
|
||||
else:
|
||||
self.spellname = args
|
||||
|
||||
if not self.spellname:
|
||||
self.caller.msg("You must specify a spell name.")
|
||||
raise InterruptCommand
|
||||
|
||||
if target:
|
||||
self.target = self.caller.search(target[0].strip())
|
||||
if not self.target:
|
||||
raise InterruptCommand
|
||||
else:
|
||||
self.target = self.caller
|
||||
|
||||
def func(self):
|
||||
|
||||
# all items carried by the caller could work
|
||||
possible_tools = self.caller.contents
|
||||
|
||||
try:
|
||||
# if this completes without an exception, the caster will have
|
||||
# a new magic_effect set on themselves, ready to use or apply in some way.
|
||||
success, effects = craft(self.caller, self.spellname, *possible_tools,
|
||||
raise_exception=True)
|
||||
except CraftingValidationError:
|
||||
return
|
||||
except KeyError:
|
||||
self.caller.msg(f"You don't know of a spell called '{self.spellname}'")
|
||||
return
|
||||
|
||||
# Applying the magical effect to target would happen below.
|
||||
# self.caller.db.active_spells[self.spellname] holds all the effects
|
||||
# of this particular prepared spell. For a fireball you could perform
|
||||
# an attack roll here and apply damage if you hit. For healing you would heal the target
|
||||
# (which could be yourself) by a number of health points given by the recipe.
|
||||
effect_txt = ", ".join(f"{eff[0]}({eff[1]})" for eff in effects)
|
||||
success_txt = "|gsucceeded|n" if success else "|rfailed|n"
|
||||
self.caller.msg(f"Casting the spell {self.spellname} on {self.target} {success_txt}, "
|
||||
f"causing the following effects: {effect_txt}.")
|
||||
690
evennia/contrib/game_systems/crafting/tests.py
Normal file
690
evennia/contrib/game_systems/crafting/tests.py
Normal file
|
|
@ -0,0 +1,690 @@
|
|||
"""
|
||||
Unit tests for the crafting system contrib.
|
||||
|
||||
"""
|
||||
|
||||
from unittest import mock
|
||||
from anything import Something
|
||||
from django.test import override_settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from evennia.commands.default.tests import CommandTest
|
||||
from evennia.utils.test_resources import TestCase, EvenniaTest
|
||||
from evennia.utils.create import create_object
|
||||
from . import crafting, example_recipes
|
||||
|
||||
|
||||
class TestCraftUtils(TestCase):
|
||||
"""
|
||||
Test helper utils for crafting.
|
||||
|
||||
"""
|
||||
|
||||
maxDiff = None
|
||||
|
||||
@override_settings(CRAFT_RECIPE_MODULES=[])
|
||||
def test_load_recipes(self):
|
||||
"""This should only load the example module now"""
|
||||
|
||||
crafting._load_recipes()
|
||||
self.assertEqual(
|
||||
crafting._RECIPE_CLASSES,
|
||||
{
|
||||
"crucible steel": example_recipes.CrucibleSteelRecipe,
|
||||
"leather": example_recipes.LeatherRecipe,
|
||||
"fireball": example_recipes.FireballRecipe,
|
||||
"heal": example_recipes.HealingRecipe,
|
||||
"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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class _TestMaterial:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class TestCraftingRecipeBase(TestCase):
|
||||
"""
|
||||
Test the parent recipe class.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.crafter = mock.MagicMock()
|
||||
self.crafter.msg = mock.MagicMock()
|
||||
|
||||
self.inp1 = _TestMaterial("test1")
|
||||
self.inp2 = _TestMaterial("test2")
|
||||
self.inp3 = _TestMaterial("test3")
|
||||
|
||||
self.kwargs = {"kw1": 1, "kw2": 2}
|
||||
|
||||
self.recipe = crafting.CraftingRecipeBase(
|
||||
self.crafter, self.inp1, self.inp2, self.inp3, **self.kwargs
|
||||
)
|
||||
|
||||
def test_msg(self):
|
||||
"""Test messaging to crafter"""
|
||||
|
||||
self.recipe.msg("message")
|
||||
self.crafter.msg.assert_called_with("message", {"type": "crafting"})
|
||||
|
||||
def test_pre_craft(self):
|
||||
"""Test validating hook"""
|
||||
self.recipe.pre_craft()
|
||||
self.assertEqual(self.recipe.validated_inputs, (self.inp1, self.inp2, self.inp3))
|
||||
|
||||
def test_pre_craft_fail(self):
|
||||
"""Should rase error if validation fails"""
|
||||
self.recipe.allow_craft = False
|
||||
with self.assertRaises(crafting.CraftingValidationError):
|
||||
self.recipe.pre_craft()
|
||||
|
||||
def test_craft_hook__succeed(self):
|
||||
"""Test craft hook, the main access method."""
|
||||
|
||||
expected_result = _TestMaterial("test_result")
|
||||
self.recipe.do_craft = mock.MagicMock(return_value=expected_result)
|
||||
|
||||
self.assertTrue(self.recipe.allow_craft)
|
||||
|
||||
result = self.recipe.craft()
|
||||
|
||||
# check result
|
||||
self.assertEqual(result, expected_result)
|
||||
self.recipe.do_craft.assert_called_with(kw1=1, kw2=2)
|
||||
|
||||
# since allow_reuse is False, this usage should now be turned off
|
||||
self.assertFalse(self.recipe.allow_craft)
|
||||
# trying to re-run again should fail since rerun is False
|
||||
with self.assertRaises(crafting.CraftingError):
|
||||
self.recipe.craft()
|
||||
|
||||
def test_craft_hook__fail(self):
|
||||
"""Test failing the call"""
|
||||
|
||||
self.recipe.do_craft = mock.MagicMock(return_value=None)
|
||||
|
||||
# trigger exception
|
||||
with self.assertRaises(crafting.CraftingError):
|
||||
self.recipe.craft(raise_exception=True)
|
||||
|
||||
# reset and try again without exception
|
||||
self.recipe.allow_craft = True
|
||||
result = self.recipe.craft()
|
||||
self.assertEqual(result, None)
|
||||
|
||||
|
||||
class _MockRecipe(crafting.CraftingRecipe):
|
||||
name = "testrecipe"
|
||||
tool_tags = ["tool1", "tool2"]
|
||||
consumable_tags = ["cons1", "cons2", "cons3"]
|
||||
output_prototypes = [
|
||||
{
|
||||
"key": "Result1",
|
||||
"prototype_key": "resultprot",
|
||||
"tags": [("result1", "crafting_material")],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@override_settings(CRAFT_RECIPE_MODULES=[])
|
||||
class TestCraftingRecipe(TestCase):
|
||||
"""
|
||||
Test the CraftingRecipe class with one recipe
|
||||
"""
|
||||
|
||||
maxDiff = None
|
||||
|
||||
def setUp(self):
|
||||
self.crafter = mock.MagicMock()
|
||||
self.crafter.msg = mock.MagicMock()
|
||||
|
||||
self.tool1 = create_object(key="tool1", tags=[("tool1", "crafting_tool")], nohome=True)
|
||||
self.tool2 = create_object(key="tool2", tags=[("tool2", "crafting_tool")], nohome=True)
|
||||
self.cons1 = create_object(key="cons1", tags=[("cons1", "crafting_material")], nohome=True)
|
||||
self.cons2 = create_object(key="cons2", tags=[("cons2", "crafting_material")], nohome=True)
|
||||
self.cons3 = create_object(key="cons3", tags=[("cons3", "crafting_material")], nohome=True)
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
self.tool1.delete()
|
||||
self.tool2.delete()
|
||||
self.cons1.delete()
|
||||
self.cons2.delete()
|
||||
self.cons3.delete()
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
def test_error_format(self):
|
||||
"""Test the automatic error formatter """
|
||||
recipe = _MockRecipe(
|
||||
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"],
|
||||
}
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
result = recipe._format_message(msg, **kwargs)
|
||||
self.assertEqual(result, msg.format(**expected))
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
result = recipe.craft()
|
||||
|
||||
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"}
|
||||
)
|
||||
|
||||
# make sure consumables are gone
|
||||
self.assertIsNone(self.cons1.pk)
|
||||
self.assertIsNone(self.cons2.pk)
|
||||
self.assertIsNone(self.cons3.pk)
|
||||
# make sure tools remain
|
||||
self.assertIsNotNone(self.tool1.pk)
|
||||
self.assertIsNotNone(self.tool2.pk)
|
||||
|
||||
def test_seed__success(self):
|
||||
"""Test seed helper classmethod"""
|
||||
|
||||
# needed for other dbs to pass seed
|
||||
homeroom = create_object(key="HomeRoom", nohome=True)
|
||||
|
||||
# call classmethod directly
|
||||
with override_settings(DEFAULT_HOME=f"#{homeroom.id}"):
|
||||
tools, consumables = _MockRecipe.seed()
|
||||
|
||||
# this should be a normal successful crafting
|
||||
recipe = _MockRecipe(self.crafter, *(tools + consumables))
|
||||
|
||||
result = recipe.craft()
|
||||
|
||||
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"}
|
||||
)
|
||||
|
||||
# make sure consumables are gone
|
||||
for cons in consumables:
|
||||
self.assertIsNone(cons.pk)
|
||||
# make sure tools remain
|
||||
for tool in tools:
|
||||
self.assertIsNotNone(tool.pk)
|
||||
|
||||
def test_craft_missing_tool__fail(self):
|
||||
"""Fail craft by missing tool2"""
|
||||
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"},
|
||||
)
|
||||
|
||||
# make sure consumables are still there
|
||||
self.assertIsNotNone(self.cons1.pk)
|
||||
self.assertIsNotNone(self.cons2.pk)
|
||||
self.assertIsNotNone(self.cons3.pk)
|
||||
# make sure tools remain
|
||||
self.assertIsNotNone(self.tool1.pk)
|
||||
self.assertIsNotNone(self.tool2.pk)
|
||||
|
||||
def test_craft_missing_cons__fail(self):
|
||||
"""Fail craft by missing cons3"""
|
||||
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"},
|
||||
)
|
||||
|
||||
# make sure consumables are still there
|
||||
self.assertIsNotNone(self.cons1.pk)
|
||||
self.assertIsNotNone(self.cons2.pk)
|
||||
self.assertIsNotNone(self.cons3.pk)
|
||||
# make sure tools remain
|
||||
self.assertIsNotNone(self.tool1.pk)
|
||||
self.assertIsNotNone(self.tool2.pk)
|
||||
|
||||
def test_craft_missing_cons__always_consume__fail(self):
|
||||
"""Fail craft by missing cons3, with always-consume flag"""
|
||||
|
||||
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.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"},
|
||||
)
|
||||
|
||||
# make sure consumables are deleted even though we failed
|
||||
self.assertIsNone(self.cons1.pk)
|
||||
self.assertIsNone(self.cons2.pk)
|
||||
# the extra should also be gone
|
||||
self.assertIsNone(cons4.pk)
|
||||
# but cons3 should be fine since it was not included
|
||||
self.assertIsNotNone(self.cons3.pk)
|
||||
# make sure tools remain as normal
|
||||
self.assertIsNotNone(self.tool1.pk)
|
||||
self.assertIsNotNone(self.tool2.pk)
|
||||
|
||||
def test_craft_wrong_tool__fail(self):
|
||||
"""Fail craft by including a wrong tool"""
|
||||
|
||||
wrong = create_object(key="wrong", tags=[("wrongtool", "crafting_tool")], nohome=True)
|
||||
|
||||
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"},
|
||||
)
|
||||
# make sure consumables are still there
|
||||
self.assertIsNotNone(self.cons1.pk)
|
||||
self.assertIsNotNone(self.cons2.pk)
|
||||
self.assertIsNotNone(self.cons3.pk)
|
||||
# make sure tools remain
|
||||
self.assertIsNotNone(self.tool1.pk)
|
||||
self.assertIsNotNone(self.tool2.pk)
|
||||
|
||||
def test_craft_tool_excess__fail(self):
|
||||
"""Fail by too many consumables"""
|
||||
|
||||
# note that this is a valid tag!
|
||||
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
|
||||
)
|
||||
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"},
|
||||
)
|
||||
|
||||
# make sure consumables are still there
|
||||
self.assertIsNotNone(self.cons1.pk)
|
||||
self.assertIsNotNone(self.cons2.pk)
|
||||
self.assertIsNotNone(self.cons3.pk)
|
||||
# make sure tools remain
|
||||
self.assertIsNotNone(self.tool1.pk)
|
||||
self.assertIsNotNone(self.tool2.pk)
|
||||
self.assertIsNotNone(tool3.pk)
|
||||
|
||||
def test_craft_cons_excess__fail(self):
|
||||
"""Fail by too many consumables"""
|
||||
|
||||
# note that this is a valid tag!
|
||||
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
|
||||
)
|
||||
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"},
|
||||
)
|
||||
|
||||
# make sure consumables are still there
|
||||
self.assertIsNotNone(self.cons1.pk)
|
||||
self.assertIsNotNone(self.cons2.pk)
|
||||
self.assertIsNotNone(self.cons3.pk)
|
||||
self.assertIsNotNone(cons4.pk)
|
||||
# make sure tools remain
|
||||
self.assertIsNotNone(self.tool1.pk)
|
||||
self.assertIsNotNone(self.tool2.pk)
|
||||
|
||||
def test_craft_tool_excess__sucess(self):
|
||||
"""Allow too many consumables"""
|
||||
|
||||
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
|
||||
)
|
||||
recipe.exact_tools = False
|
||||
result = recipe.craft()
|
||||
self.assertTrue(result)
|
||||
self.crafter.msg.assert_called_with(
|
||||
recipe.success_message.format(outputs="Result1"), {"type": "crafting"}
|
||||
)
|
||||
|
||||
# make sure consumables are gone
|
||||
self.assertIsNone(self.cons1.pk)
|
||||
self.assertIsNone(self.cons2.pk)
|
||||
self.assertIsNone(self.cons3.pk)
|
||||
# make sure tools remain
|
||||
self.assertIsNotNone(self.tool1.pk)
|
||||
self.assertIsNotNone(self.tool2.pk)
|
||||
|
||||
def test_craft_cons_excess__sucess(self):
|
||||
"""Allow too many consumables"""
|
||||
|
||||
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
|
||||
)
|
||||
recipe.exact_consumables = False
|
||||
result = recipe.craft()
|
||||
self.assertTrue(result)
|
||||
self.crafter.msg.assert_called_with(
|
||||
recipe.success_message.format(outputs="Result1"), {"type": "crafting"}
|
||||
)
|
||||
|
||||
# make sure consumables are gone
|
||||
self.assertIsNone(self.cons1.pk)
|
||||
self.assertIsNone(self.cons2.pk)
|
||||
self.assertIsNone(self.cons3.pk)
|
||||
# make sure tools remain
|
||||
self.assertIsNotNone(self.tool1.pk)
|
||||
self.assertIsNotNone(self.tool2.pk)
|
||||
|
||||
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
|
||||
)
|
||||
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"},
|
||||
)
|
||||
|
||||
# make sure consumables are still there
|
||||
self.assertIsNotNone(self.cons1.pk)
|
||||
self.assertIsNotNone(self.cons2.pk)
|
||||
self.assertIsNotNone(self.cons3.pk)
|
||||
# make sure tools remain
|
||||
self.assertIsNotNone(self.tool1.pk)
|
||||
self.assertIsNotNone(self.tool2.pk)
|
||||
|
||||
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
|
||||
)
|
||||
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"},
|
||||
)
|
||||
|
||||
# make sure consumables are still there
|
||||
self.assertIsNotNone(self.cons1.pk)
|
||||
self.assertIsNotNone(self.cons2.pk)
|
||||
self.assertIsNotNone(self.cons3.pk)
|
||||
# make sure tools remain
|
||||
self.assertIsNotNone(self.tool1.pk)
|
||||
self.assertIsNotNone(self.tool2.pk)
|
||||
|
||||
|
||||
class TestCraftSword(TestCase):
|
||||
"""
|
||||
Test the `craft` function by crafting the example sword.
|
||||
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.crafter = mock.MagicMock()
|
||||
self.crafter.msg = mock.MagicMock()
|
||||
|
||||
@override_settings(CRAFT_RECIPE_MODULES=[], DEFAULT_HOME="#999999")
|
||||
@mock.patch("evennia.contrib.crafting.example_recipes.random")
|
||||
def test_craft_sword(self, mockrandom):
|
||||
"""
|
||||
Craft example sword. For the test, every crafting works.
|
||||
|
||||
"""
|
||||
# make sure every craft succeeds
|
||||
mockrandom.random = mock.MagicMock(return_value=0.2)
|
||||
|
||||
def _co(key, tagkey, is_tool=False):
|
||||
tagcat = "crafting_tool" if is_tool else "crafting_material"
|
||||
return create_object(key=key, tags=[(tagkey, tagcat)], nohome=True)
|
||||
|
||||
def _craft(recipe_name, *inputs):
|
||||
"""shortcut to shorten and return only one element"""
|
||||
result = crafting.craft(self.crafter, recipe_name, *inputs, raise_exception=True)
|
||||
return result[0] if len(result) == 1 else result
|
||||
|
||||
# generate base materials
|
||||
iron_ore1 = _co("Iron ore ingot", "iron ore")
|
||||
iron_ore2 = _co("Iron ore ingot", "iron ore")
|
||||
iron_ore3 = _co("Iron ore ingot", "iron ore")
|
||||
|
||||
ash1 = _co("Pile of Ash", "ash")
|
||||
ash2 = _co("Pile of Ash", "ash")
|
||||
ash3 = _co("Pile of Ash", "ash")
|
||||
|
||||
sand1 = _co("Pile of sand", "sand")
|
||||
sand2 = _co("Pile of sand", "sand")
|
||||
sand3 = _co("Pile of sand", "sand")
|
||||
|
||||
coal01 = _co("Pile of coal", "coal")
|
||||
coal02 = _co("Pile of coal", "coal")
|
||||
coal03 = _co("Pile of coal", "coal")
|
||||
coal04 = _co("Pile of coal", "coal")
|
||||
coal05 = _co("Pile of coal", "coal")
|
||||
coal06 = _co("Pile of coal", "coal")
|
||||
coal07 = _co("Pile of coal", "coal")
|
||||
coal08 = _co("Pile of coal", "coal")
|
||||
coal09 = _co("Pile of coal", "coal")
|
||||
coal10 = _co("Pile of coal", "coal")
|
||||
coal11 = _co("Pile of coal", "coal")
|
||||
coal12 = _co("Pile of coal", "coal")
|
||||
|
||||
oak_wood = _co("Pile of oak wood", "oak wood")
|
||||
water = _co("Bucket of water", "water")
|
||||
fur = _co("Bundle of Animal fur", "fur")
|
||||
|
||||
# tools
|
||||
blast_furnace = _co("Blast furnace", "blast furnace", is_tool=True)
|
||||
furnace = _co("Smithing furnace", "furnace", is_tool=True)
|
||||
crucible = _co("Smelting crucible", "crucible", is_tool=True)
|
||||
anvil = _co("Smithing anvil", "anvil", is_tool=True)
|
||||
hammer = _co("Smithing hammer", "hammer", is_tool=True)
|
||||
knife = _co("Working knife", "knife", is_tool=True)
|
||||
cauldron = _co("Cauldron", "cauldron", is_tool=True)
|
||||
|
||||
# making pig iron
|
||||
inputs = [iron_ore1, coal01, coal02, blast_furnace]
|
||||
pig_iron1 = _craft("pig iron", *inputs)
|
||||
|
||||
inputs = [iron_ore2, coal03, coal04, blast_furnace]
|
||||
pig_iron2 = _craft("pig iron", *inputs)
|
||||
|
||||
inputs = [iron_ore3, coal05, coal06, blast_furnace]
|
||||
pig_iron3 = _craft("pig iron", *inputs)
|
||||
|
||||
# making crucible steel
|
||||
inputs = [pig_iron1, ash1, sand1, coal07, coal08, crucible]
|
||||
crucible_steel1 = _craft("crucible steel", *inputs)
|
||||
|
||||
inputs = [pig_iron2, ash2, sand2, coal09, coal10, crucible]
|
||||
crucible_steel2 = _craft("crucible steel", *inputs)
|
||||
|
||||
inputs = [pig_iron3, ash3, sand3, coal11, coal12, crucible]
|
||||
crucible_steel3 = _craft("crucible steel", *inputs)
|
||||
|
||||
# smithing
|
||||
inputs = [crucible_steel1, hammer, anvil, furnace]
|
||||
sword_blade = _craft("sword blade", *inputs)
|
||||
|
||||
inputs = [crucible_steel2, hammer, anvil, furnace]
|
||||
sword_pommel = _craft("sword pommel", *inputs)
|
||||
|
||||
inputs = [crucible_steel3, hammer, anvil, furnace]
|
||||
sword_guard = _craft("sword guard", *inputs)
|
||||
|
||||
# stripping fur
|
||||
inputs = [fur, knife]
|
||||
rawhide = _craft("rawhide", *inputs)
|
||||
|
||||
# making bark (tannin) and cleaned wood
|
||||
inputs = [oak_wood, knife]
|
||||
oak_bark, cleaned_oak_wood = _craft("oak bark", *inputs)
|
||||
|
||||
# leathermaking
|
||||
inputs = [rawhide, oak_bark, water, cauldron]
|
||||
leather = _craft("leather", *inputs)
|
||||
|
||||
# sword handle
|
||||
inputs = [cleaned_oak_wood, knife]
|
||||
sword_handle = _craft("sword handle", *inputs)
|
||||
|
||||
# sword (order matters)
|
||||
inputs = [
|
||||
sword_blade,
|
||||
sword_guard,
|
||||
sword_pommel,
|
||||
sword_handle,
|
||||
leather,
|
||||
knife,
|
||||
hammer,
|
||||
furnace,
|
||||
]
|
||||
sword = _craft("sword", *inputs)
|
||||
|
||||
self.assertEqual(sword.key, "Sword")
|
||||
|
||||
# make sure all materials and intermediaries are deleted
|
||||
self.assertIsNone(iron_ore1.pk)
|
||||
self.assertIsNone(iron_ore2.pk)
|
||||
self.assertIsNone(iron_ore3.pk)
|
||||
self.assertIsNone(ash1.pk)
|
||||
self.assertIsNone(ash2.pk)
|
||||
self.assertIsNone(ash3.pk)
|
||||
self.assertIsNone(sand1.pk)
|
||||
self.assertIsNone(sand2.pk)
|
||||
self.assertIsNone(sand3.pk)
|
||||
self.assertIsNone(coal01.pk)
|
||||
self.assertIsNone(coal02.pk)
|
||||
self.assertIsNone(coal03.pk)
|
||||
self.assertIsNone(coal04.pk)
|
||||
self.assertIsNone(coal05.pk)
|
||||
self.assertIsNone(coal06.pk)
|
||||
self.assertIsNone(coal07.pk)
|
||||
self.assertIsNone(coal08.pk)
|
||||
self.assertIsNone(coal09.pk)
|
||||
self.assertIsNone(coal10.pk)
|
||||
self.assertIsNone(coal11.pk)
|
||||
self.assertIsNone(coal12.pk)
|
||||
self.assertIsNone(oak_wood.pk)
|
||||
self.assertIsNone(water.pk)
|
||||
self.assertIsNone(fur.pk)
|
||||
self.assertIsNone(pig_iron1.pk)
|
||||
self.assertIsNone(pig_iron2.pk)
|
||||
self.assertIsNone(pig_iron3.pk)
|
||||
self.assertIsNone(crucible_steel1.pk)
|
||||
self.assertIsNone(crucible_steel2.pk)
|
||||
self.assertIsNone(crucible_steel3.pk)
|
||||
self.assertIsNone(sword_blade.pk)
|
||||
self.assertIsNone(sword_pommel.pk)
|
||||
self.assertIsNone(sword_guard.pk)
|
||||
self.assertIsNone(rawhide.pk)
|
||||
self.assertIsNone(oak_bark.pk)
|
||||
self.assertIsNone(leather.pk)
|
||||
self.assertIsNone(sword_handle.pk)
|
||||
|
||||
# make sure all tools remain
|
||||
self.assertIsNotNone(blast_furnace)
|
||||
self.assertIsNotNone(furnace)
|
||||
self.assertIsNotNone(crucible)
|
||||
self.assertIsNotNone(anvil)
|
||||
self.assertIsNotNone(hammer)
|
||||
self.assertIsNotNone(knife)
|
||||
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})
|
||||
@override_settings(CRAFT_RECIPE_MODULES=[], DEFAULT_HOME="#999999")
|
||||
class TestCraftCommand(CommandTest):
|
||||
"""Test the crafting command"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
tools, consumables = _MockRecipe.seed(
|
||||
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"),
|
||||
)
|
||||
|
||||
def test_craft__notools__failure(self):
|
||||
"Craft fail no tools"
|
||||
self.call(
|
||||
crafting.CmdCraft(),
|
||||
"testrecipe from cons1, cons2, cons3",
|
||||
_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"),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue