diff --git a/evennia/contrib/tutorials/evadventure/characters.py b/evennia/contrib/tutorials/evadventure/characters.py index 6c6f11d18d..c527146de8 100644 --- a/evennia/contrib/tutorials/evadventure/characters.py +++ b/evennia/contrib/tutorials/evadventure/characters.py @@ -3,13 +3,13 @@ Base Character and NPCs. """ -from evennia.objects.objects import DefaultCharacter, DefaultObject +from evennia.objects.objects import DefaultCharacter from evennia.typeclasses.attributes import AttributeProperty -from evennia.utils.utils import int2str, lazy_property +from evennia.utils.utils import lazy_property from . import rules from .enums import Ability, WieldLocation -from .objects import EvAdventureObject, WeaponEmptyHand +from .objects import WeaponEmptyHand class EquipmentError(TypeError): @@ -163,7 +163,6 @@ class EquipmentHandler: """ slots = self.slots - one_hand = None weapon_str = "You are fighting with your bare fists" shield_str = " and have no shield." armor_str = "You wear no armor" @@ -172,7 +171,7 @@ class EquipmentHandler: two_hands = slots[WieldLocation.TWO_HANDS] if two_hands: weapon_str = f"You wield {two_hands} with both hands" - shield_str = f" (you can't hold a shield at the same time)." + shield_str = " (you can't hold a shield at the same time)." else: one_hands = slots[WieldLocation.WEAPON_HAND] if one_hands: @@ -306,7 +305,7 @@ class EquipmentHandler: """ return [ obj - for obj in slots[WieldLocation.BACKPACK] + for obj in self.slots[WieldLocation.BACKPACK] if obj.inventory_use_slot in (WieldLocation.WEAPON_HAND, WieldLocation.TWO_HANDS, WieldLocation.SHIELD_HAND) ] @@ -324,7 +323,7 @@ class EquipmentHandler: """ return [ obj - for obj in slots[WieldLocation.BACKPACK] + for obj in self.slots[WieldLocation.BACKPACK] if obj.inventory_use_slot in (WieldLocation.BODY, WieldLocation.HEAD) ] @@ -338,7 +337,7 @@ class EquipmentHandler: """ character = self.obj - return [obj for obj in slots[WieldLocation.BACKPACK] if obj.at_pre_use(character)] + return [obj for obj in self.slots[WieldLocation.BACKPACK] if obj.at_pre_use(character)] class LivingMixin: @@ -549,7 +548,8 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter): if self.hp > 0: # still alive, but lost some stats self.location.msg_contents( - f"|y$You() $conj(stagger) back and fall to the ground - alive, but unable to move.|n", + "|y$You() $conj(stagger) back and fall to the ground - alive, " + "but unable to move.|n", from_obj=self, ) @@ -559,6 +559,6 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter): """ self.location.msg_contents( - f"|r$You() $conj(collapse) in a heap.\nDeath embraces you ...|n", + "|r$You() $conj(collapse) in a heap.\nDeath embraces you ...|n", from_obj=self, ) diff --git a/evennia/contrib/tutorials/evadventure/combat_turnbased.py b/evennia/contrib/tutorials/evadventure/combat_turnbased.py index 8502c88009..e4694d05a1 100644 --- a/evennia/contrib/tutorials/evadventure/combat_turnbased.py +++ b/evennia/contrib/tutorials/evadventure/combat_turnbased.py @@ -100,15 +100,13 @@ Choose who to block: """ from collections import defaultdict -from datetime import datetime from evennia.scripts.scripts import DefaultScript from evennia.typeclasses.attributes import AttributeProperty from evennia.utils import dbserialize, delay, evmenu, evtable, logger -from evennia.utils.utils import inherits_from, make_iter +from evennia.utils.utils import inherits_from from . import rules -from .characters import EvAdventureCharacter from .enums import Ability from .npcs import EvAdventureNPC @@ -135,7 +133,8 @@ class CombatAction: Note: We want to store initialized version of this objects in the CombatHandler (in order to track usages, time limits etc), so we need to make sure we can serialize it into an Attribute. See - `Attribute` documentation for more about `__serialize_dbobjs__` and `__deserialize_dbobjs__`. + `Attribute` documentation for more about `__serialize_dbobjs__` and + `__deserialize_dbobjs__`. """ @@ -366,8 +365,8 @@ class CombatActionStunt(CombatAction): else: self.combathandler.gain_disadvantage(defender, attacker) self.msg( - f"%You(defender.key) $conj(suffer) disadvantage against $You(). " - f"Lasts next attack, or until 3 turns passed." + f"$You({defender.key}) $conj(suffer) disadvantage against $You(). " + "Lasts next attack, or until 3 turns passed." ) # only spend a use after being successful @@ -735,7 +734,7 @@ class EvAdventureCombatHandler(DefaultScript): for combatant in to_flee: # combatant leaving combat by fleeing - self.msg(f"|y$You() successfully $conj(flee) from combat.|n", combatant=combatant) + self.msg("|y$You() successfully $conj(flee) from combat.|n", combatant=combatant) self.remove_combatant(combatant) for combatant in to_defeat: @@ -932,7 +931,7 @@ class EvAdventureCombatHandler(DefaultScript): targets = [target for target in combatants if inherits_from(target, EvAdventureNPC)] if extra: - targets = list(set(target + extra)) + targets = list(set(targets + extra)) return targets @@ -1120,6 +1119,7 @@ def node_confirm_register_action(caller, raw_string, **kwargs): }, {"key": ("Abort/Cancel", "abort", "cancel", "a", "no", "n"), "goto": "node_select_action"}, ) + return text, options def _select_target_helper(caller, raw_string, targets, **kwargs): @@ -1127,9 +1127,7 @@ def _select_target_helper(caller, raw_string, targets, **kwargs): Helper to select among only friendly or enemy targets (given by the calling node). """ - combat = caller.ndb._evmenu.combathandler action_key = kwargs["action_key"] - friendly_target = kwargs.get("target_friendly", False) text = f"Select target for |w{action_key}|n." # make the apply-self option always the first one, give it key 0 @@ -1154,6 +1152,7 @@ def node_select_enemy_target(caller, raw_string, **kwargs): with all other actions. """ + combat = caller.ndb._evmenu.combathandler targets = combat.get_enemy_targets(caller) return _select_target_helper(caller, raw_string, targets, **kwargs) @@ -1163,6 +1162,7 @@ def node_select_friendly_target(caller, raw_string, **kwargs): Menu node for selecting a friendly target among combatants (including oneself). """ + combat = caller.ndb._evmenu.combathandler targets = combat.get_friendly_targets(caller) return _select_target_helper(caller, raw_string, targets, **kwargs) @@ -1177,7 +1177,6 @@ def node_select_wield_from_inventory(caller, raw_string, **kwargs): Menu node allowing for wielding item(s) from inventory. """ - combat = caller.ndb._evmenu.combathandler loadout = caller.equipment.display_loadout() text = ( f"{loadout}\nSelect weapon, spell or shield to draw. It will swap out " @@ -1191,7 +1190,7 @@ def node_select_wield_from_inventory(caller, raw_string, **kwargs): # object is broken options.append( { - "desc": f"|Rstr(obj)|n", + "desc": "|Rstr(obj)|n", "goto": _item_broken, } ) @@ -1213,7 +1212,6 @@ def node_select_use_item_from_inventory(caller, raw_string, **kwargs): Menu item allowing for using usable items (like potions) from inventory. """ - combat = caller.ndb._evmenu.combathandler text = "Select an item to use." # get a list of all suitable weapons/spells/shields @@ -1223,7 +1221,7 @@ def node_select_use_item_from_inventory(caller, raw_string, **kwargs): # object is broken options.append( { - "desc": f"|Rstr(obj)|n", + "desc": "|Rstr(obj)|n", "goto": _item_broken, } ) diff --git a/evennia/contrib/tutorials/evadventure/quests.py b/evennia/contrib/tutorials/evadventure/quests.py index 54fcb52423..ec95e8cc4e 100644 --- a/evennia/contrib/tutorials/evadventure/quests.py +++ b/evennia/contrib/tutorials/evadventure/quests.py @@ -27,26 +27,99 @@ class EvAdventureQuest: The last step is always the end of the quest. It is possible to abort the quest before it ends - it then pauses after the last completed step. - each step is represented by two methods on this object: - check_ and complete_ + Each step is represented by three methods on this object: + `check_` and `complete_`. `help_` is used to get + a guide/reminder on what you are supposed to do. """ # name + category must be globally unique. They are # queried as name:category or just name, if category is empty. - name = "" - category = "" - # example: steps = ["start", "step1", "step2", "end"] - steps = [] + key = "basequest" + desc = "This is the base quest. It will just step through its steps immediately." + start_step = "start" - def __init__(self): - step = 0 + # help entries for quests + help_start = "You need to start first" + help_end = "You need to end the quest" - def check(): - pass + def __init__(self, questhandler, start_step="start"): + if " " in self.key: + raise TypeError("The Quest name must not have spaces in it.") - def progress(self, quester, *args, **kwargs): - """ """ + self.questhandler = questhandler + self.current_step = start_step + + @property + def quester(self): + return self.questhandler.obj + + def end_quest(self): + """ + Call this to end the quest. + + """ + self.current_step + + def progress(self): + """ + This is called whenever the environment expects a quest may be complete. + This will determine which quest-step we are on, run check_, and if it + succeeds, continue with complete_ + + """ + if getattr(self, f"check_{self.current_step}")(): + getattr(self, f"complete_{self.current_step}")() + + def help(self): + """ + This is used to get help (or a reminder) of what needs to be done to complete the current + quest-step. + + Returns: + str: The help text for the current step. + + """ + help_resource = ( + getattr(self, f"help_{self.current_step}", None) + or "You need to {self.current_step} ..." + ) + if callable(help_resource): + # the help_ can be a method to call + return help_resource() + else: + # normally it's just a string + return str(help_resource) + + # step methods + + def check_start(self): + """ + Check if the starting conditions are met. + + Returns: + bool: If this step is complete or not. If complete, the `complete_start` + method will fire. + + """ + return True + + def complete_start(self): + """ + Completed start. This should change `.current_step` to the next step to complete + and call `self.progress()` just in case the next step is already completed too. + + """ + self.quester.msg("Completed the first step of the quest.") + self.current_step = "end" + self.progress() + + def check_end(self): + return True + + def complete_end(self): + self.quester.msg("Quest complete!") + self.end_quest() class EvAdventureQuestHandler: @@ -63,46 +136,54 @@ class EvAdventureQuestHandler: """ - quest_storage_attribute = "_quests" + quest_storage_attribute_key = "_quests" quest_storage_attribute_category = "evadventure" def __init__(self, obj): self.obj = obj - self.storage = obj.attributes.get( - self.quest_storage_attribute, category=self.quest_storage_attribute_category, default={} + self._load() + + def _load(self): + self.storage = self.obj.attributes.get( + self.quest_storage_attribute_key, + category=self.quest_storage_attribute_category, + default={}, ) - def quest_storage_key(self, name, category): - return f"{name}:{category}" + def _save(self): + self.obj.attributes.add( + self.quest_storage_attribute_key, + self.storage, + category=self.quest_storage_attribute_category, + ) - def has(self, quest_name, quest_category=""): + def has(self, quest_key): """ Check if a given quest is registered with the Character. Args: - quest_name (str): The name of the quest to check for. + quest_key (str): The name of the quest to check for. quest_category (str, optional): Quest category, if any. Returns: bool: If the character is following this quest or not. """ - return bool(self.get(quest_name, quest_category)) + return bool(self.storage.get(quest_key)) - def get(self, quest_name, quest_category=""): + def get(self, quest_key): """ Get the quest stored on character, if any. Args: - quest_name (str): The name of the quest to check for. - quest_category (str, optional): Quest category, if any. + quest_key (str): The name of the quest to check for. Returns: EvAdventureQuest or None: The quest stored, or None if Character is not on this quest. """ - return self.storage.get(self.quest_key(quest_storage_name, quest_category)) + return self.storage.get(quest_key) def add(self, quest, autostart=True): """ @@ -114,3 +195,48 @@ class EvAdventureQuestHandler: start immediately. """ + self.storage[quest.key] = quest + self._save() + + def remove(self, quest_key): + """ + Remove a quest. + + Args: + quest_key (str): The quest to remove. + + """ + self.storage.pop(quest_key, None) + self._save() + + def help(self, quest_key=None): + """ + Get help text for a quest or for all quests. The help text is + a combination of the description of the quest and the help-text + of the current step. + + """ + help_text = [] + if quest_key in self.storage: + quests = [self.storage[quest_key]] + + for quest in quests: + help_text.append(f"|c{quest.key}|n\n {quest.desc}\n\n - {quest.help}") + return "---".join(help_text) + + def progress(self, quest_key=None): + """ + Check progress of a given quest or all quests. + + Args: + quest_key (str, optional): If given, check the progress of this quest (if we have it), + otherwise check progress on all quests. + + """ + if quest_key in self.storage: + quests = [self.storage[quest_key]] + else: + quests = self.storage.values() + + for quest in quests: + quest.progress() diff --git a/evennia/contrib/tutorials/evadventure/rooms.py b/evennia/contrib/tutorials/evadventure/rooms.py index 326010d5fc..241b063633 100644 --- a/evennia/contrib/tutorials/evadventure/rooms.py +++ b/evennia/contrib/tutorials/evadventure/rooms.py @@ -5,7 +5,7 @@ EvAdventure rooms. """ -from evennia import AttributeProperty, DefaultRoom +from evennia import DefaultRoom class EvAdventureRoom(DefaultRoom): diff --git a/evennia/contrib/tutorials/evadventure/rules.py b/evennia/contrib/tutorials/evadventure/rules.py index 2f0c561072..f37459cfdd 100644 --- a/evennia/contrib/tutorials/evadventure/rules.py +++ b/evennia/contrib/tutorials/evadventure/rules.py @@ -164,7 +164,7 @@ class EvAdventureRollEngine: modtxt = f" + {modifier}" if modifier > 0 else f" - {abs(modifier)}" qualtxt = f" ({quality.value}!)" if quality else "" - txt = f"{dice_roll} + {bonus_type.value}{bontxt}{modtxt} -> |w{result}{qualtxt}|n" + txt = f"{rolltxt}={dice_roll} + {bonus_type.value}{bontxt}{modtxt} -> |w{result}{qualtxt}|n" return (dice_roll + bonus + modifier) > target, quality, txt @@ -563,7 +563,7 @@ class EvAdventureImprovement: """ character.xp += xp - next_level_xp = character.level * xp_per_level + next_level_xp = character.level * EvAdventureImprovement.xp_per_level return character.xp >= next_level_xp @staticmethod @@ -583,18 +583,20 @@ class EvAdventureImprovement: roll_engine = EvAdventureRollEngine() character.level += 1 - for ability in set(abilities[:amount_of_abilities_to_upgrades]): + for ability in set(abilities[: EvAdventureImprovement.amount_of_abilities_to_upgrades]): # limit to max amount allowed, each one unique try: # set at most to the max bonus current_bonus = getattr(character, ability) - setattr(character, ability, min(max_ability_bonus, current_bonus + 1)) + setattr( + character, + ability, + min(EvAdventureImprovement.max_ability_bonus, current_bonus + 1), + ) except AttributeError: pass - character.hp_max = max( - character.max_hp + 1, EvAdventureRollEngine.roll(f"{character.level}d8") - ) + character.hp_max = max(character.max_hp + 1, roll_engine.roll(f"{character.level}d8")) # character sheet visualization @@ -646,7 +648,7 @@ class EvAdventureCharacterSheet: equipment_table = EvTable( table=[equipment[i : i + 10] for i in range(0, len(equipment), 10)] ) - form = EvForm({"FORMCHAR": "x", "TABLECHAR": "c", "SHEET": sheet}) + form = EvForm({"FORMCHAR": "x", "TABLECHAR": "c", "SHEET": EvAdventureCharacterSheet.sheet}) form.map( cells={ 1: character.key,