Add magic-crafting example to crafting readme. Resolve #2554

This commit is contained in:
Griatch 2021-10-12 23:52:52 +02:00
parent 798fb7d92d
commit fd2c0d9fb7
2 changed files with 244 additions and 9 deletions

View file

@ -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": <obj>} 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", [])

View file

@ -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 <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}.")