diff --git a/evennia/contrib/crafting/crafting.py b/evennia/contrib/crafting/crafting.py index ab6283627c..2911230fe2 100644 --- a/evennia/contrib/crafting/crafting.py +++ b/evennia/contrib/crafting/crafting.py @@ -480,7 +480,7 @@ class CraftingRecipe(CraftingRecipeBase): # there should be multiple entries in this list. tool_tags = [] # human-readable names for the tools. This will be used for informative messages - # or when usage fails. If empty + # or when usage fails. If empty, use tag-names. tool_names = [] # if we must have exactly the right tools, no more exact_tools = True @@ -628,20 +628,23 @@ class CraftingRecipe(CraftingRecipeBase): return message.format(**mapping) @classmethod - def seed(cls, tool_kwargs=None, consumable_kwargs=None): + def seed(cls, tool_kwargs=None, consumable_kwargs=None, location=None): """ This is a helper class-method for easy testing and application of this recipe. When called, it will create simple dummy ingredients with names and tags needed by this recipe. Args: + tool_kwargs (dict, optional): Will be passed as `**tool_kwargs` into the `create_object` + call for each tool. If not given, the matching + `tool_name` or `tool_tag` will be used for key. consumable_kwargs (dict, optional): This will be passed as `**consumable_kwargs` into the `create_object` call for each consumable. If not given, matching `consumable_name` or `consumable_tag` will be used for key. - tool_kwargs (dict, optional): Will be passed as `**tool_kwargs` into the `create_object` - call for each tool. If not given, the matching - `tool_name` or `tool_tag` will be used for key. + location (Object, optional): If given, the created items will be created in this + location. This is a shortcut for adding {"location": } to both the + consumable/tool kwargs (and will *override* any such setting in those kwargs). Returns: tuple: A tuple `(tools, consumables)` with newly created dummy @@ -649,8 +652,7 @@ class CraftingRecipe(CraftingRecipeBase): Example: :: - - tools, consumables = SwordRecipe.seed() + tools, consumables = SwordRecipe.seed(location=caller) recipe = SwordRecipe(caller, *(tools + consumables)) result = recipe.craft() @@ -663,6 +665,11 @@ class CraftingRecipe(CraftingRecipeBase): tool_kwargs = {} if not consumable_kwargs: consumable_kwargs = {} + + if location: + tool_kwargs['location'] = location + consumable_kwargs['location'] = location + tool_key = tool_kwargs.pop("key", None) cons_key = consumable_kwargs.pop("key", None) tool_tags = tool_kwargs.pop("tags", []) diff --git a/evennia/contrib/crafting/example_recipes.py b/evennia/contrib/crafting/example_recipes.py index a7432d59bb..899dc0750c 100644 --- a/evennia/contrib/crafting/example_recipes.py +++ b/evennia/contrib/crafting/example_recipes.py @@ -41,13 +41,41 @@ around with them. 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 level3+ + + # recipe for fireball + + fireball = spellbook[T] + wand[T] + [firemagic skill lvl3+] + ---- """ -from random import random -from .crafting import CraftingRecipe +from random import random, randint +from evennia.commands.command import Command, InterruptCommand +from .crafting import craft, CraftingRecipe, CraftingValidationError +#------------------------------------------------------------ +# Sword recipe +#------------------------------------------------------------ class PigIronRecipe(CraftingRecipe): """ @@ -300,3 +328,203 @@ class SwordRecipe(_SwordSmithingBaseRecipe): ] # 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 + + """ + 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}.")