From d97106948bd18d6424df38e77a8767fe0d9ac63d Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 2 Aug 2022 11:48:06 +0200 Subject: [PATCH] Prep for shopkeepers --- .../tutorials/evadventure/characters.py | 2 +- .../tutorials/evadventure/combat_turnbased.py | 2 - evennia/contrib/tutorials/evadventure/npcs.py | 134 +++++++++++++++++- .../contrib/tutorials/evadventure/shops.py | 27 +++- evennia/objects/objects.py | 4 +- 5 files changed, 157 insertions(+), 12 deletions(-) diff --git a/evennia/contrib/tutorials/evadventure/characters.py b/evennia/contrib/tutorials/evadventure/characters.py index a63b02cf9e..9d866fa328 100644 --- a/evennia/contrib/tutorials/evadventure/characters.py +++ b/evennia/contrib/tutorials/evadventure/characters.py @@ -62,7 +62,7 @@ class LivingMixin: Called when attacked and taking damage. """ - pass + self.hp -= damage def at_defeat(self): """ diff --git a/evennia/contrib/tutorials/evadventure/combat_turnbased.py b/evennia/contrib/tutorials/evadventure/combat_turnbased.py index 7788a5ee8d..25dbc0cf86 100644 --- a/evennia/contrib/tutorials/evadventure/combat_turnbased.py +++ b/evennia/contrib/tutorials/evadventure/combat_turnbased.py @@ -288,8 +288,6 @@ class CombatActionAttack(CombatAction): message = f" $You() $conj(hit) $You({defender.key}) for |r{dmg}|n damage!" self.msg(message) - defender.hp -= dmg - # call hook defender.at_damage(dmg, attacker=attacker) diff --git a/evennia/contrib/tutorials/evadventure/npcs.py b/evennia/contrib/tutorials/evadventure/npcs.py index 12e97d08e1..a0a3ba7551 100644 --- a/evennia/contrib/tutorials/evadventure/npcs.py +++ b/evennia/contrib/tutorials/evadventure/npcs.py @@ -6,6 +6,8 @@ from random import choice from evennia import DefaultCharacter from evennia.typeclasses.attributes import AttributeProperty +from evennia.utils.evmenu import EvMenu +from evennia.utils.utils import make_iter from .characters import LivingMixin from .enums import Ability, WieldLocation @@ -95,18 +97,140 @@ class EvAdventureNPC(LivingMixin, DefaultCharacter): pass -class EvAdventureShopKeeper(EvAdventureNPC): +class EvAdventureTalkativeNPC(EvAdventureNPC): + """ + Talkative NPCs can be addressed by `talk [to] `. This opens a chat menu with + communication options. The menu is created with the npc and we override the .create + to allow passing in the menu nodes. + + """ + + menudata = AttributeProperty(None, autocreate=False) + menu_kwargs = AttributeProperty(None, autocreate=False) + # text shown when greeting at the start of a conversation. If this is an + # iterable, a random reply will be chosen by the menu + hi_text = AttributeProperty("Hi!", autocreate=False) + + def at_damage(self, damage, attacker=None): + """ + Talkative NPCs are generally immortal (we don't deduct HP here by default)." + + """ + attacker.msg(f'{self.key} dodges the damage and shouts "|wHey! What are you doing?|n"') + + @classmethod + def create(cls, key, account=None, **kwargs): + """ + Overriding the creation of the NPC, allowing some extra `**kwargs`. + + Args: + key (str): Name of the new object. + account (Account, optional): Account to attribute this object to. + + Keyword Args: + description (str): Brief description for this object (same as default Evennia) + ip (str): IP address of creator (for object auditing) (same as default Evennia). + menudata (dict or str): The `menudata` argument to `EvMenu`. This is either a dict of + `{"nodename": ,...}` or the python-path to a module containing + such nodes (see EvMenu docs). This will be used to generate the chat menu + chat menu for the character that talks to the NPC (which means the `at_talk` hook + is called (by our custom `talk` command). + menu_kwargs (dict): This will be passed as `**kwargs` into `EvMenu` when it + is created. Make sure this dict can be pickled to an Attribute. + + Returns: + tuple: `(new_character, errors)`. On error, the `new_character` is `None` and + `errors` is a `list` of error strings (an empty list otherwise). + + + """ + menudata = kwargs.pop("menudata", None) + menu_kwargs = kwargs.pop("menu_kwargs", {}) + + # since this is a @classmethod we can't use super() here + new_object, errors = EvAdventureNPC.create( + key, account=account, attributes=(("menudata", menudata), ("menu_kwargs", menu_kwargs)) + ) + + return new_object, errors + + def at_talk(self, talker, startnode="node_start", session=None, **kwargs): + """ + Called by the `talk` command when another entity addresses us. + + Args: + talker (Object): The one talking to us. + startnode (str, optional): Allows to start in a different location in the menu tree. + The given node must exist in the tree. + session (Session, optional): The talker's current session, allows for routing + correctly in multi-session modes. + **kwargs: This will be passed into the `EvMenu` creation and appended and `menu_kwargs` + given to the NPC at creation. + + Notes: + We pass `npc=self` into the EvMenu for easy back-reference. This will appear in the + `**kwargs` of the start node. + + """ + menu_kwargs = {**self.menu_kwargs, **kwargs} + EvMenu(talker, self.menudata, startnode=startnode, session=session, npc=self, **menu_kwargs) + + +def node_start(caller, raw_string, **kwargs): + """ + This is the intended start menu node for the Talkative NPC interface. It will + use on-npc Attributes to build its message and will also pick its options + based on nodes named `node_start_*` are available in the node tree. + + """ + # we presume a back-reference to the npc this is added when the menu is created + npc = kwargs["npc"] + + # grab a (possibly random) welcome text + text = choice(make_iter(npc.hi_text)) + + # determine options based on `node_start_*` nodes available + toplevel_node_keys = [ + node_key for node_key in caller.ndb._evmenu._menutree if node_key.startswith("node_start_") + ] + options = [] + for node_key in toplevel_node_keys: + option_name = node_key[11:].replace("_", " ").capitalized() + + # we let the menu number the choices, so we don't use key here + options.append({"desc": option_name, "goto": node_key}) + + return text, options + + +class EvAdventureQuestGiver(EvAdventureTalkativeNPC): + """ + An NPC that acts as a dispenser of quests. + + """ + + +class EvAdventureShopKeeper(EvAdventureTalkativeNPC): """ ShopKeeper NPC. """ + # how much extra the shopkeeper adds on top of the item cost + upsell_factor = AttributePropert(1.0, autocreate=False) + # how much of the raw cost the shopkeep is willing to pay when buying from character + miser_factor = Attribute(0.5, autocreate=False) + common_ware_prototypes = AttributeProperty([], autocreate=False) -class EvAdventureQuestGiver(EvAdventureNPC): - """ - An NPC that acts as a dispenser of quests. + def at_damage(self, damage, attacker=None): + """ + Immortal - we don't deduct any damage here. - """ + """ + attacker.msg( + f"{self.key} brushes off the hit and shouts " + '"|wHey! This is not the way to get a discount!|n"' + ) class EvAdventureMob(EvAdventureNPC): diff --git a/evennia/contrib/tutorials/evadventure/shops.py b/evennia/contrib/tutorials/evadventure/shops.py index 0026710e68..fe00e24a57 100644 --- a/evennia/contrib/tutorials/evadventure/shops.py +++ b/evennia/contrib/tutorials/evadventure/shops.py @@ -23,14 +23,37 @@ A shop is run by an NPC. It can provide one or more of several possible services All shops are menu-driven. One starts talking to the npc and will then end up in their shop interface. + +This is a series of menu nodes meant to be added as a mapping via +`EvAdventureShopKeeper.create(menudata={},...)`. + +To make this pluggable, the shopkeeper start page will analyze the available nodes +and auto-add options to all nodes in the three named `node_start_*`. The last part of the +node name will be the name of the option capitalized, with underscores replaced by spaces, so +`node_start_sell_items` will become a top-level option `Sell items`. + + + """ +from random import choice + from evennia.utils.evmenu import EvMenu +from evennia.utils.utils import make_iter + +from .npcs import EvAdventureShopKeeper + +# shop menu nodes to use for building a Shopkeeper npc -def start_npc_menu(caller, shopkeeper, **kwargs): +def node_start_buy(caller, raw_string, **kwargs): """ - Access function - start the NPC interaction/shop interface. + Menu node for the caller to buy items from the shopkeep. This assumes `**kwargs` contains + a kwarg `npc` referencing the npc/shopkeep being talked to. + Items available to sell are a combination of items in the shopkeep's inventory and prototypes + the list of `prototypes` stored in the Shopkeep's "common_ware_prototypes` Attribute. In the + latter case, the properties will be extracted from the prototype when inspecting it (object will + only spawn when bought). """ diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index f467b6d2e9..1825cd9f8b 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -2419,8 +2419,8 @@ class DefaultCharacter(DefaultObject): All other kwargs will be passed into the create_object call. Returns: - character (Object): A newly created Character of the given typeclass. - errors (list): A list of errors in string form, if any. + tuple: `(new_character, errors)`. On error, the `new_character` is `None` and + `errors` is a `list` of error strings (an empty list otherwise). """ errors = []