385 lines
No EOL
11 KiB
Python
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.
|
|
|
|
""" |