dkmud/utils/spells.py
Francesco Cappelli 5ef1b78c42 sistemati spells
2022-02-17 18:33:26 +01:00

385 lines
No EOL
11 KiB
Python

from copy import copy
from evennia import logger
from evennia.utils import evtable
from evennia.utils.utils import callables_from_module, inherits_from, make_iter, iter_to_string, list_to_string
from evennia.commands.cmdset import CmdSet
from commands.command import Command
from typeclasses.scripts import CmdActionScript
from utils.utils import toggle_effect, has_effect
from evennia.utils.create import create_object, create_script
_SPELL_CLASSES = {}
def _load_spells():
"""
Delayed loading of recipe classes. This parses
`settings.SPELL_MODULES`.
"""
from django.conf import settings
global _SPELL_CLASSES
if not _SPELL_CLASSES:
paths = []
if hasattr(settings, "SPELL_MODULES"):
paths += make_iter(settings.SPELL_MODULES)
for path in paths:
for cls in callables_from_module(path).values():
if inherits_from(cls, Spell):
_SPELL_CLASSES[cls.name] = cls
class Spell:
name = "spell base"
casting_time = 1
duration = 1
target_type = None
# general cast-failure msg to show after other error-messages.
failure_message = ""
# show after a successful cast
success_message = "You cast {}.".format(name)
def __init__(self, caster, **kwargs):
self.caster = caster
self.target_obj = None
self.cast_kwargs = kwargs
self.allow_cast = True
def msg(self, message, **kwargs):
"""
Send message to caster. This is a central point to override if wanting
to change casting return style in some way.
Args:
message(str): The message to send.
**kwargs: Any optional properties relevant to this send.
"""
self.caster.msg(message, oob=({"type": "casting"}))
def pre_cast(self, **kwargs):
"""
Hook to override.
This is called just before casting operation.
Args:
**kwargs: Any optional extra kwargs passed during initialization of
the spell class.
Raises:
CastingError: If validation fails. At this point the caster
is expected to have been informed of the problem already.
"""
caster = self.caster
self.target_obj = None
if self.target_type:
if not self.cast_kwargs.get('target_name', None):
self.msg("You need a target to cast {} on.".format(self.name))
raise CastingError("You need a target to cast {} on.".format(self.name))
target_name = self.cast_kwargs.get('target_name')
self.target_obj = caster.search(target_name, location=[caster, caster.location])
if not self.target_obj:
self.msg("{} is not a valid target for {}.".format(target_name, self.name))
raise CastingError("{} is not a valid target for {}.".format(target_name, self.name))
# TODO
# gestire i vari tipi di target_type
def do_cast(self, **kwargs):
"""
Hook to override. This will not be called if validation in `pre_cast`
fails.
This performs the actual casting. At this point the inputs are
expected to have been verified already.
Returns:
any: The result of casting if any.
Notes:
This method should use `self.msg` to inform the user about the
specific reason of failure immediately.
"""
return True
def post_cast(self, cast_result, **kwargs):
"""
Hook to override.
This is called just after casting has finished.
"""
if cast_result:
self.msg(self.success_message)
elif self.failure_message:
self.msg(self.failure_message)
return cast_result
def cast(self, raise_exception=False, **kwargs):
"""
Main casting call method. Call this to cast a spell and make
sure all hooks run correctly.
Args:
raise_exception (bool): If casting would go wrong, raise
exception instead.
**kwargs (any): Any other parameters that is relevant
for this particular cast operation. This will temporarily
override same-named kwargs given at the creation of this recipe
and be passed into all the casting hooks.
Returns:
any: The result of the cast, or `None`.
Raises:
CastingError: If casting validation failed and
`raise_exception` is True.
"""
cast_result = None
if self.allow_cast:
# override/extend cast_kwargs from initialization.
cast_kwargs = copy(self.cast_kwargs)
cast_kwargs.update(kwargs)
try:
try:
# this assigns to self.validated_inputs
self.pre_cast(**cast_kwargs)
except CastingError:
if raise_exception:
raise
else:
cast_result = self.do_cast(**cast_kwargs)
finally:
cast_result = self.post_cast(cast_result, **cast_kwargs)
except CastingError:
if raise_exception:
raise
if cast_result is None and raise_exception:
raise CastingError(f"Casting of {self.name} failed.")
return cast_result
def cast(caster, spell_name, raise_exception=False, **kwargs):
# delayed loading/caching of spells
_load_spells()
SpellClass = search_spell(caster, spell_name)
if not SpellClass:
raise KeyError(
f"No spell in settings.SPELL_MODULES has a name matching {spell_name}"
)
spell = SpellClass(caster, **kwargs)
return spell.cast(raise_exception=raise_exception)
def can_cast(caster, spell_name, **kwargs):
"""
Access function.Check if crafter can craft a given recipe from a source recipe module.
Args:
caster (Object): The one doing the crafting.
spell_name (str): The `Spell.name` to use. This uses fuzzy-matching
if the result is unique.
**kwargs: Optional kwargs to pass into the casting.
Returns:
list: Error messages, if any.
"""
# delayed loading/caching of spells
_load_spells()
SpellClass = search_spell(caster, spell_name)
if not SpellClass:
raise KeyError(
f"No spell in settings.SPELL_MODULES has a name matching {spell_name}"
)
spell = SpellClass(caster, **kwargs)
if spell.allow_cast:
# override/extend craft_kwargs from initialization.
cast_kwargs = copy(spell.cast_kwargs)
cast_kwargs.update(kwargs)
try:
spell.pre_cast(**cast_kwargs)
except CastingError:
logger.log_err(CastingError.args)
return False
else:
return True
return False
def search_spell(caster, spell_name):
# delayed loading/caching of spells
_load_spells()
spell_class = _SPELL_CLASSES.get(spell_name, None)
if not spell_class:
# try a startswith fuzzy match
matches = [key for key in _SPELL_CLASSES if key.startswith(spell_name)]
if not matches:
# try in-match
matches = [key for key in _SPELL_CLASSES if spell_name in key]
if len(matches) == 1:
spell_class = _SPELL_CLASSES.get(matches[0], None)
return spell_class
class CastingCmdSet(CmdSet):
"""
Store Casting command.
"""
key = "Casting cmdset"
def at_cmdset_creation(self):
self.add(CmdCast())
self.add(CmdSpells())
class CmdSpells(Command):
"""
List known spells.
Usage:
spells
"""
key = "spells"
locks = "cmd:all()"
help_category = "General"
arg_regex = r"\s|$"
def func(self):
_load_spells()
caller = self.caller
table = evtable.EvTable("|wName|n", "|wTarget", "|wCasting time", "|wDuration",
border_left_char="|y|||n", border_right_char="|y|||n",
border_top_char="|y-|n", border_bottom_char="|y-|n",
corner_char="|y+|n")
for spell in _SPELL_CLASSES.values():
if (spell.name in caller.db.spells) or caller.is_superuser:
table.add_row("|W{}".format(spell.name), "|M{}".format(spell.target_type), "|G{}".format(spell.casting_time), "|M{}".format(spell.duration))
caller.msg(table)
class CmdCast(Command):
"""
cast a spell.
Usage:
cast <spell> [at <target>]
Casts a spell.
"""
key = "cast"
aliases = ["cs"]
lock = "cmd:false()"
help_category = "General"
arg_regex = r"\s.+|$"
def parse(self):
"""
Handle parsing of:
::
<spell> [at|on <target>]
"""
self.args = args = self.args.strip().lower()
if " at " in args:
spell_name, *rest = args.split(" at ", 1)
elif " on " in args:
spell_name, *rest = args.split(" on ", 1)
else:
spell_name, rest = args, []
target_name = rest[0] if rest else ""
self.spell_name = spell_name.strip()
self.target_name = target_name.strip()
def func(self):
caller = self.caller
if not self.args or not self.spell_name:
caller.msg(self.get_help(caller, self.cmdset))
return
if self.spell_name not in caller.db.spells:
caller.msg("You don't know how to cast {}.".format(self.spell_name))
return
if has_effect(caller, "is_busy"):
caller.msg("You are already busy {}.".format(caller.db.current_action.busy_msg()))
return
spell_cls = search_spell(caller, self.spell_name)
if not spell_cls:
caller.msg("You don't know how to cast {}.".format(self.spell_name))
return
if not can_cast(caller, spell_cls.name, target_name=self.target_name):
return
toggle_effect(caller, "is_busy")
caller.msg("You start casting {}.".format(spell_cls.name))
action_script = create_script("utils.spells.CastCompleteScript", obj=caller, interval=spell_cls.casting_time,
attributes=[("spell", spell_cls.name),
("target_name", self.target_name)])
caller.db.current_action = action_script
class CastCompleteScript(CmdActionScript):
def at_script_creation(self):
super().at_script_creation()
self.key = "cmd_cast_complete"
self.desc = ""
self.db.spell = ""
self.db.target_name = ""
def at_repeat(self):
caller = self.obj
if has_effect(caller, "is_busy"):
toggle_effect(caller, "is_busy")
# perform casting (the recipe handles all returns to caller).
result = cast(caller, self.db.spell, target_name=self.db.target_name)
if result and not isinstance(result, bool):
for obj in result:
if inherits_from(obj, "typeclasses.objects.Feature"):
obj.location = caller.location
else:
obj.location = caller
def busy_msg(self):
return "casting {}.".format(self.db.spell)
class CastingError(RuntimeError):
"""
Casting error.
"""