From 5ef1b78c4204de0180c84396e09972dbdbcc4314 Mon Sep 17 00:00:00 2001 From: Francesco Cappelli Date: Thu, 17 Feb 2022 18:33:26 +0100 Subject: [PATCH] sistemati spells --- commands/command.py | 58 ------ commands/default_cmdsets.py | 11 +- commands/unloggedin.py | 36 ++++ menus/char_manager.py | 58 ++++++ menus/login.py | 203 +++++++++++++++++++ server/conf/settings.py | 1 + typeclasses/characters.py | 1 + typeclasses/exits.py | 7 + typeclasses/rooms.py | 54 ++++- utils/crafting.py | 77 +++++--- utils/spells.py | 385 ++++++++++++++++++++++++++++++++++++ utils/utils.py | 12 +- world/spells.py | 88 +++++---- 13 files changed, 865 insertions(+), 126 deletions(-) create mode 100644 commands/unloggedin.py create mode 100644 menus/char_manager.py create mode 100644 menus/login.py create mode 100644 utils/spells.py diff --git a/commands/command.py b/commands/command.py index e2b8692..a364c73 100644 --- a/commands/command.py +++ b/commands/command.py @@ -21,8 +21,6 @@ from typeclasses.rooms import IndoorRoom from typeclasses.objects import Item, EquippableItem from typeclasses.scripts import Script, CmdActionScript -from world import spells - CMD_SEARCH_TIME = 5 @@ -574,62 +572,6 @@ class CmdInventory(Command): self.caller.msg(string) -class CmdCast(Command): - """ - cast a spell. - - Usage: - cast [at ] - - Casts a spell. - """ - key = "cast" - aliases = ["cs"] - lock = "cmd:false()" - help_category = "General" - arg_regex = r"\s.+|$" - - def parse(self): - """ - Handle parsing of: - :: - [at|on ] - """ - 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 - - spell_id = self.spell_name.replace(' ', '_') - if self.spell_name not in caller.db.spells: - caller.msg("You cannot cast {}.".format(self.spell_name)) - return - - if hasattr(spells, "spell_" + spell_id): - spell = getattr(spells, "spell_" + spell_id) - else: - logger.log_err("Cannot find spell {}.".format("spell_" + spell_id)) - return - - spell(caller, self.target_name) - - class CmdTestPy(Command): key = "testpy" aliases = ["testpy"] diff --git a/commands/default_cmdsets.py b/commands/default_cmdsets.py index d68b3ab..c1d7a1a 100644 --- a/commands/default_cmdsets.py +++ b/commands/default_cmdsets.py @@ -24,7 +24,6 @@ from commands.command import CmdTestPy from commands.command import CmdSearch from commands.command import CmdEquip from commands.command import CmdInventory -from commands.command import CmdCast from commands.command import CmdPut from commands.command import CmdOpenCloseDoor @@ -35,7 +34,10 @@ from commands.builder import CmdZone from commands.builder import CmdAddToZone from commands.builder import CmdPopen -from utils.crafting import CmdCraft +from commands.unloggedin import CmdUnconnectedLook + +from utils.crafting import CraftingCmdSet +from utils.spells import CastingCmdSet class CharacterCmdSet(default_cmds.CharacterCmdSet): @@ -64,7 +66,6 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet): self.add(CmdSearch()) self.add(CmdEquip()) self.add(CmdInventory()) - self.add(CmdCast()) self.add(CmdPut()) self.add(CmdOpenCloseDoor()) @@ -75,7 +76,8 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet): self.add(CmdAddToZone()) self.add(CmdPopen()) - self.add(CmdCraft()) + self.add(CraftingCmdSet()) + self.add(CastingCmdSet()) class AccountCmdSet(default_cmds.AccountCmdSet): @@ -114,6 +116,7 @@ class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet): # # any commands you add below will overload the default ones. # + self.add(CmdUnconnectedLook()) class SessionCmdSet(default_cmds.SessionCmdSet): diff --git a/commands/unloggedin.py b/commands/unloggedin.py new file mode 100644 index 0000000..9ad2860 --- /dev/null +++ b/commands/unloggedin.py @@ -0,0 +1,36 @@ +from evennia import Command +from evennia.utils.evmenu import EvMenu +from evennia.commands.cmdhandler import CMD_LOGINSTART + +from menus.login import plain_node_formatter + + +class CmdUnconnectedLook(Command): + """ + look when in unlogged-in state + + Usage: + look + + This is an unconnected version of the look command for simplicity. + + This is called by the server and kicks everything in gear. + All it does is display the connect screen. + """ + + key = CMD_LOGINSTART + aliases = ["look", "l"] + locks = "cmd:all()" + + def func(self): + """Show the connect screen.""" + EvMenu( + self.caller, + "menus.login", + startnode="node_enter_username", + auto_look=False, + auto_quit=False, + cmd_on_exit=None, + node_formatter=plain_node_formatter, + ) + diff --git a/menus/char_manager.py b/menus/char_manager.py new file mode 100644 index 0000000..bee4cda --- /dev/null +++ b/menus/char_manager.py @@ -0,0 +1,58 @@ +_ACCOUNT_HELP = ( + "Enter the name you used to log into the game before, " "or a new account-name if you are new." +) +_PASSWORD_HELP = ( + "Password should be a minimum of 8 characters (preferably longer) and " + "can contain a mix of letters, spaces, digits and @/./+/-/_/'/, only." +) + + +def _show_help(caller, raw_string, **kwargs): + """Echo help message, then re-run node that triggered it""" + help_entry = kwargs["help_entry"] + caller.msg(help_entry) + return None # re-run calling node + + +def node_enter_char_management(caller, raw_string, **kwargs): + account = caller.ndb._menutree.account + + if not account: + raise Exception("No account found!") + + # TODO + # text = "{}".format(w_first_login) + # + # options = ( + # {"key": "", "goto": "node_enter_char_management"}, + # {"key": ("help", "h"), "goto": (_show_help, {"help_entry": _ACCOUNT_HELP, **kwargs})}, + # {"key": "_default", "goto": "node_enter_char_management"}, + # ) + # + # return text, options + + # TODO + character = account.db._last_puppet + character.db.health = 10 + character.db.mana = 10 + + character.db.spells = [] + character.db.spells.append('light') + character.db.recipes = [] + character.db.recipes.append('vial of blood') + character.db.recipes.append('torch') + + caller.msg("|gLogging in ...|n") + caller.sessionhandler.login(caller, account) + return "", {} + + +def plain_node_formatter(nodetext, optionstext, caller=None): + """Do not display the options, only the text. + + This function is used by EvMenu to format the text of nodes. The menu login + is just a series of prompts, so we disable all automatic display decoration + and let the nodes handle everything on their own. + + """ + return nodetext \ No newline at end of file diff --git a/menus/login.py b/menus/login.py new file mode 100644 index 0000000..28efa59 --- /dev/null +++ b/menus/login.py @@ -0,0 +1,203 @@ +from django.conf import settings +from evennia.utils import utils + +from evennia.utils.evmenu import EvMenu + +CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE +_GUEST_ENABLED = settings.GUEST_ENABLED +_ACCOUNT = utils.class_from_module(settings.BASE_ACCOUNT_TYPECLASS) +_GUEST = utils.class_from_module(settings.BASE_GUEST_TYPECLASS) + +_ACCOUNT_HELP = ( + "Enter the name you used to log into the game before, " "or a new account-name if you are new." +) +_PASSWORD_HELP = ( + "Password should be a minimum of 8 characters (preferably longer) and " + "can contain a mix of letters, spaces, digits and @/./+/-/_/'/, only." +) + + +def _show_help(caller, raw_string, **kwargs): + """Echo help message, then re-run node that triggered it""" + help_entry = kwargs["help_entry"] + caller.msg(help_entry) + return None # re-run calling node + + +def node_enter_username(caller, raw_text, **kwargs): + """ + Start node of menu + Start login by displaying the connection screen and ask for a user name. + + """ + def _check_input(caller, username, **kwargs): + """ + 'Goto-callable', set up to be called from the _default option below. + + Called when user enters a username string. Check if this username already exists and set the flag + 'new_user' if not. Will also directly login if the username is 'guest' + and GUEST_ENABLED is True. + + The return from this goto-callable determines which node we go to next + and what kwarg it will be called with. + + """ + username = username.rstrip("\n") + + if username == "guest" and _GUEST_ENABLED: + # do an immediate guest login + session = caller + address = session.address + account, errors = _GUEST.authenticate(ip=address) + if account: + return "node_quit_or_login", {"login": True, "account": account} + else: + session.msg("|R{}|n".format("\n".join(errors))) + return None # re-run the username node + + try: + _ACCOUNT.objects.get(username__iexact=username) + except _ACCOUNT.DoesNotExist: + new_user = True + else: + new_user = False + + # pass username/new_user into next node as kwargs + return "node_enter_password", {"new_user": new_user, "username": username} + + """Show the connect screen.""" + + callables = utils.callables_from_module(CONNECTION_SCREEN_MODULE) + if "connection_screen" in callables: + connection_screen = callables["connection_screen"]() + else: + connection_screen = utils.random_string_from_module(CONNECTION_SCREEN_MODULE) + if not connection_screen: + connection_screen = "No connection screen found. Please contact an admin." + + if _GUEST_ENABLED: + text = "Enter a new or existing user name to login (write 'guest' for a guest login):" + else: + text = "Enter a new or existing user name to login:" + + text = "{}\n\n{}".format(connection_screen, text) + + options = ( + {"key": "", "goto": "node_enter_username"}, + {"key": ("quit", "q"), "goto": "node_quit_or_login"}, + {"key": ("help", "h"), "goto": (_show_help, {"help_entry": _ACCOUNT_HELP, **kwargs})}, + {"key": "_default", "goto": _check_input}, + ) + return text, options + + +def node_enter_password(caller, raw_string, **kwargs): + """ + Handle password input. + + """ + + def _check_input(caller, password, **kwargs): + """ + 'Goto-callable', set up to be called from the _default option below. + + Called when user enters a password string. Check username + password + viability. If it passes, the account will have been created and login + will be initiated. + + The return from this goto-callable determines which node we go to next + and what kwarg it will be called with. + + """ + # these flags were set by the goto-callable + username = kwargs["username"] + new_user = kwargs["new_user"] + password = password.rstrip("\n") + + session = caller + address = session.address + if new_user: + # create a new account + account, errors = _ACCOUNT.create( + username=username, password=password, ip=address, session=session + ) + else: + # check password against existing account + account, errors = _ACCOUNT.authenticate( + username=username, password=password, ip=address, session=session + ) + + if account: + if new_user: + session.msg("|gA new account |c{}|g was created. Welcome!|n".format(username)) + # pass login info to login node + return "node_quit_or_login", {"login": True, "account": account} + else: + # restart due to errors + session.msg("|R{}".format("\n".join(errors))) + kwargs["retry_password"] = True + return "node_enter_password", kwargs + + def _restart_login(caller, *args, **kwargs): + caller.msg("|yCancelled login.|n") + return "node_enter_username" + + username = kwargs["username"] + if kwargs["new_user"]: + if kwargs.get("retry_password"): + # Attempting to fix password + text = "Enter a new password:" + else: + text = "Creating a new account |c{}|n. " "Enter a password (empty to abort):".format(username) + else: + text = "Enter the password for account |c{}|n (empty to abort):".format(username) + options = ( + {"key": "", "goto": _restart_login}, + {"key": ("quit", "q"), "goto": "node_quit_or_login"}, + {"key": ("help", "h"), "goto": (_show_help, {"help_entry": _PASSWORD_HELP, **kwargs})}, + {"key": "_default", "goto": (_check_input, kwargs)}, + ) + return text, options + + +def node_quit_or_login(caller, raw_text, **kwargs): + """ + Exit menu, either by disconnecting or logging in. + + """ + session = caller + if kwargs.get("login"): + account = kwargs.get("account") + # TODO + # session.msg("|gLogging in ...|n") + # session.sessionhandler.login(session, account) + + # # go OOC + # account.unpuppet_object(session) + + EvMenu( + caller, + "menus.char_manager", + startnode="node_enter_char_management", + auto_look=False, + auto_quit=False, + cmd_on_exit=None, + node_formatter=plain_node_formatter, + account=account, + ) + + + else: + session.sessionhandler.disconnect(session, "Goodbye! Logging off.") + return "", {} + + +def plain_node_formatter(nodetext, optionstext, caller=None): + """Do not display the options, only the text. + + This function is used by EvMenu to format the text of nodes. The menu login + is just a series of prompts, so we disable all automatic display decoration + and let the nodes handle everything on their own. + + """ + return nodetext diff --git a/server/conf/settings.py b/server/conf/settings.py index 7b387e8..372a21b 100644 --- a/server/conf/settings.py +++ b/server/conf/settings.py @@ -41,6 +41,7 @@ GLOBAL_SCRIPTS = { PROTOTYPE_MODULES += ["world.tutorial_prototypes"] CRAFT_RECIPE_MODULES = ['world.recipes_base'] +SPELL_MODULES = ['world.spells'] ###################################################################### # Settings given in secret_settings.py override those in this file. diff --git a/typeclasses/characters.py b/typeclasses/characters.py index 5169021..60c1107 100644 --- a/typeclasses/characters.py +++ b/typeclasses/characters.py @@ -56,6 +56,7 @@ class Character(DefaultCharacter): } self.db.spells = [] + self.db.recipes = [] self.db.current_action = None def get_health(self): diff --git a/typeclasses/exits.py b/typeclasses/exits.py index b698bd3..dea534d 100644 --- a/typeclasses/exits.py +++ b/typeclasses/exits.py @@ -39,6 +39,13 @@ class Exit(DefaultExit): defined, in which case that will simply be echoed. """ + def at_init(self): + # check if this exit has directional alias + self.ndb.directional_alias = None + directional_aliases = [ele for ele in ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'] if (ele in self.aliases.all())] + if directional_aliases: + self.ndb.directional_alias = directional_aliases[0] + def at_traverse(self, traversing_object, target_location, **kwargs): if has_effect(traversing_object, "is_busy"): traversing_object.msg("You are already busy {}.".format(traversing_object.db.current_action.busy_msg())) diff --git a/typeclasses/rooms.py b/typeclasses/rooms.py index 8a0db04..9640d20 100644 --- a/typeclasses/rooms.py +++ b/typeclasses/rooms.py @@ -33,8 +33,8 @@ class Room(DefaultRoom): def at_object_creation(self): self.locks.add("light:false()") - self.db.x = 0 - self.db.y = 0 + self.db.x = -1 + self.db.y = -1 self.db.map_icon = '|w⊡|n' @@ -68,6 +68,7 @@ class IndoorRoom(Room): """ Called when room is first recached (such as after a reload) """ + super().at_init() self.check_light_state() def return_appearance(self, looker, **kwargs): @@ -88,7 +89,10 @@ class IndoorRoom(Room): for con in visible: key = con.get_display_name(looker) if con.destination: - exits.append(key) + if con.ndb.directional_alias: + exits.append("|m{}|n|w)|n{}".format(con.ndb.directional_alias, key)) + else: + exits.append(key) elif con.has_account: users.append("|c%s|n" % key) elif con.db.feature_desc: @@ -326,3 +330,47 @@ class Zone(DefaultRoom): for ex in room.exits: # iterate exits in the room if not self.ndb.sp_graph.is_edge(room, ex.destination): self.ndb.sp_graph.add_edge(room, ex.destination, 1, ex.name) + + # if destination is already inserted in zone we can proceed, otherwise + # aliases are created later. + if ex.destination.db.x != - 1 and ex.destination.db.y != - 1: + self._update_exit_alias(ex) + if ex.db.return_exit: + self._update_exit_alias(ex.db.return_exit) + + def _update_exit_alias(self, exit_obj): + xl, yl = exit_obj.location.db.x, exit_obj.location.db.y + xd, yd = exit_obj.destination.db.x, exit_obj.destination.db.y + + # if aliases aren't already created, create them. + if not [ele for ele in ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'] if(ele in exit_obj.aliases.all())]: + aliases = [] + dx = xd - xl + dy = yd - yl + if dx == 0 and dy == -1: + aliases.extend(["n", "north"]) + exit_obj.ndb.directional_alias = "n" + elif dx == 0 and dy == 1: + aliases.extend(["s", "south"]) + exit_obj.ndb.directional_alias = "s" + elif dx == -1 and dy == 0: + aliases.extend(["w", "west"]) + exit_obj.ndb.directional_alias = "w" + elif dx == 1 and dy == 0: + aliases.extend(["e", "east"]) + exit_obj.ndb.directional_alias = "e" + elif dx == 1 and dy == -1: + aliases.extend(["ne", "northeast"]) + exit_obj.ndb.directional_alias = "ne" + elif dx == -1 and dy == -1: + aliases.extend(["nw", "northwest"]) + exit_obj.ndb.directional_alias = "nw" + elif dx == 1 and dy == 1: + aliases.extend(["se", "southeast"]) + exit_obj.ndb.directional_alias = "se" + elif dx == -1 and dy == 1: + aliases.extend(["sw", "southwest"]) + exit_obj.ndb.directional_alias = "sw" + + exit_obj.aliases.add(aliases) + diff --git a/utils/crafting.py b/utils/crafting.py index 52ad81f..f3c97a6 100644 --- a/utils/crafting.py +++ b/utils/crafting.py @@ -121,9 +121,10 @@ a full example of the components for creating a sword from base components. from copy import copy from evennia import logger -from evennia.utils.utils import callables_from_module, inherits_from, make_iter, iter_to_string +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 evennia.commands.command import Command +from commands.command import Command from evennia.prototypes.spawner import spawn from evennia.utils.create import create_object, create_script @@ -677,7 +678,6 @@ class CraftingRecipe(CraftingRecipeBase): tools = [] for itag, tag in enumerate(cls.tool_tags): - tools.append( create_object( key=tool_key or (cls.tool_names[itag] if cls.tool_names else tag.capitalize()), @@ -690,7 +690,7 @@ class CraftingRecipe(CraftingRecipeBase): consumables.append( create_object( key=cons_key - or (cls.consumable_names[itag] if cls.consumable_names else tag.capitalize()), + or (cls.consumable_names[itag] if cls.consumable_names else tag.capitalize()), tags=[(tag, cls.consumable_tag_category), *cons_tags], **consumable_kwargs, ) @@ -717,14 +717,14 @@ class CraftingRecipe(CraftingRecipeBase): """ def _check_completeness( - tagmap, - taglist, - namelist, - exact_match, - exact_order, - error_missing_message, - error_order_message, - error_excess_message, + tagmap, + taglist, + namelist, + exact_match, + exact_order, + error_missing_message, + error_order_message, + error_excess_message, ): """Compare tagmap (inputs) to taglist (required)""" valids = [] @@ -771,17 +771,17 @@ class CraftingRecipe(CraftingRecipeBase): obj: obj.tags.get(category=self.tool_tag_category, return_list=True) for obj in self.inputs if obj - and hasattr(obj, "tags") - and inherits_from(obj, "evennia.objects.models.ObjectDB") + and hasattr(obj, "tags") + and inherits_from(obj, "evennia.objects.models.ObjectDB") } tool_map = {obj: tags for obj, tags in tool_map.items() if tags} consumable_map = { obj: obj.tags.get(category=self.consumable_tag_category, return_list=True) for obj in self.inputs if obj - and hasattr(obj, "tags") - and obj not in tool_map - and inherits_from(obj, "evennia.objects.models.ObjectDB") + and hasattr(obj, "tags") + and obj not in tool_map + and inherits_from(obj, "evennia.objects.models.ObjectDB") } consumable_map = {obj: tags for obj, tags in consumable_map.items() if tags} @@ -1008,6 +1008,38 @@ class CraftingCmdSet(CmdSet): def at_cmdset_creation(self): self.add(CmdCraft()) + self.add(CmdRecipes()) + + +class CmdRecipes(Command): + """ + List known recipes. + + Usage: + recipes + """ + + key = "recipes" + locks = "cmd:all()" + help_category = "General" + arg_regex = r"\s|$" + + def func(self): + # delayed loading/caching of recipes + _load_recipes() + + caller = self.caller + + table = evtable.EvTable("|wName|n", "|wMaterials", "|wTools|n", + 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 recipe in _RECIPE_CLASSES.values(): + if (recipe.name in caller.db.recipes) or caller.is_superuser: + table.add_row("|W{}".format(recipe.name), "|M{}".format(list_to_string(recipe.tool_tags)), "|G{}".format(list_to_string(recipe.consumable_tags))) + + caller.msg(table) class CmdCraft(Command): @@ -1095,9 +1127,9 @@ class CmdCraft(Command): if not obj: return if ( - not inherits_from(obj, "evennia.objects.models.ObjectDB") - or obj.sessions.all() - or not obj.access(caller, "craft", default=True) + not inherits_from(obj, "evennia.objects.models.ObjectDB") + or obj.sessions.all() + or not obj.access(caller, "craft", default=True) ): # We don't allow to include puppeted objects nor those with the # 'negative' permission 'nocraft'. @@ -1139,7 +1171,9 @@ class CmdCraft(Command): toggle_effect(caller, "is_busy") caller.msg("You start crafting {} {}.".format(indefinite_article(recipe_cls.name), recipe_cls.name)) - action_script = create_script("utils.crafting.CmdCraftComplete", obj=caller, interval=recipe_cls.crafting_time, attributes=[("recipe", recipe_cls.name), ("tools_and_ingredients", tools_and_ingredients)]) + action_script = create_script("utils.crafting.CmdCraftComplete", obj=caller, interval=recipe_cls.crafting_time, + attributes=[("recipe", recipe_cls.name), + ("tools_and_ingredients", tools_and_ingredients)]) caller.db.current_action = action_script @@ -1171,4 +1205,3 @@ class CmdCraftComplete(CmdActionScript): def busy_msg(self): return "crafting {} {}".format(indefinite_article(self.db.recipe), self.db.recipe) - diff --git a/utils/spells.py b/utils/spells.py new file mode 100644 index 0000000..40d0940 --- /dev/null +++ b/utils/spells.py @@ -0,0 +1,385 @@ +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 [at ] + + Casts a spell. + """ + key = "cast" + aliases = ["cs"] + lock = "cmd:false()" + help_category = "General" + arg_regex = r"\s.+|$" + + def parse(self): + """ + Handle parsing of: + :: + [at|on ] + """ + 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. + + """ \ No newline at end of file diff --git a/utils/utils.py b/utils/utils.py index 4268073..046c808 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -1,26 +1,30 @@ -from evennia.prototypes import spawner - def has_tag(obj, key, category): - return obj.tags.get(key=key, category=category) != None; + return obj.tags.get(key=key, category=category) is not None; + def fmt_light(message): return "|r\u2600|y {} |r\u2600|n".format(message) + def fmt_dark(message): return "|w\u2600|=h {} |w\u2600|n".format(message) + def toggle_effect(obj, effect): if has_tag(obj, effect, "effect"): obj.tags.remove(effect, category="effect") else: obj.tags.add(effect, category="effect") + def has_effect(obj, effect): return has_tag(obj, effect, "effect") + def has_effect_in(obj, effects): return any(True for effect in effects if has_tag(obj, effect, "effect")) + def indefinite_article(name): """Return the right english indefinite article for given name.""" the_vowels = ["a","e","i","o","u"] @@ -30,4 +34,4 @@ def indefinite_article(name): else: value = "a" - return value \ No newline at end of file + return value diff --git a/world/spells.py b/world/spells.py index a075faf..90a36f4 100644 --- a/world/spells.py +++ b/world/spells.py @@ -1,45 +1,63 @@ -from evennia import utils, create_script, logger -from evennia.utils import inherits_from - +from evennia import create_script from typeclasses import effects -from typeclasses.mobs import Mob from utils.utils import has_effect - -def spell_light(caller, target, **kwargs): - if not target: - target_obj = caller - else: - target_obj = caller.search(target, location=[caller, caller.location]) - - if not target_obj: - return - - if has_effect(target_obj, "emit_magic_light"): - caller.msg("{} already has a magical light on itself.".format(target_obj.name)) - return - - light_script = create_script(effects.EffectMagicalLight, obj=target_obj) - caller.msg("You cast |wlight|n on {}.".format(target_obj.name)) +from utils.spells import Spell -def spell_charm(caller, target, **kwargs): - if not target: - caller.msg("You need someone to place your charm on.") - return +class LightSpell(Spell): + """Make the target emit magical light""" + name = "light" # name to refer to this recipe as + casting_time = 1 + duration = 60 + target_type = "SINGLE" + success_message = "You cast |wlight|n on {}." - target_obj = caller.search(target, location=[caller.location]) + def do_cast(self, **kwargs): + target_obj = self.target_obj + if has_effect(target_obj, "emit_magic_light"): + self.msg("{} already has a magical light on itself.".format(target_obj.name)) + return False - if not target_obj: - return + create_script(effects.EffectMagicalLight, obj=target_obj, interval=self.duration) + self.success_message = self.success_message.format(target_obj.name) - if not inherits_from(target_obj, Mob): - caller.msg("You cannot charm {}".format(target_obj.name)) - return + return True - if has_effect(target_obj, "charm"): - caller.msg("{} is already charmed.".format(target_obj.name)) - return +# def spell_light(caller, target, **kwargs): +# if not target: +# target_obj = caller +# else: +# target_obj = caller.search(target, location=[caller, caller.location]) +# +# if not target_obj: +# return +# +# if has_effect(target_obj, "emit_magic_light"): +# caller.msg("{} already has a magical light on itself.".format(target_obj.name)) +# return +# +# light_script = create_script(effects.EffectMagicalLight, obj=target_obj) +# caller.msg("You cast |wlight|n on {}.".format(target_obj.name)) - charm_script = create_script(effects.EffectCharm, obj=target_obj, attributes=[("source", caller.dbref)]) - caller.msg("You cast |wcharm|n on {}.".format(target_obj.name)) \ No newline at end of file + +# def spell_charm(caller, target, **kwargs): +# if not target: +# caller.msg("You need someone to place your charm on.") +# return +# +# target_obj = caller.search(target, location=[caller.location]) +# +# if not target_obj: +# return +# +# if not inherits_from(target_obj, Mob): +# caller.msg("You cannot charm {}".format(target_obj.name)) +# return +# +# if has_effect(target_obj, "charm"): +# caller.msg("{} is already charmed.".format(target_obj.name)) +# return +# +# charm_script = create_script(effects.EffectCharm, obj=target_obj, attributes=[("source", caller.dbref)]) +# caller.msg("You cast |wcharm|n on {}.".format(target_obj.name)) \ No newline at end of file