diff --git a/evennia/contrib/tutorials/evadventure/npcs.py b/evennia/contrib/tutorials/evadventure/npcs.py index a0a3ba7551..d02f0ea580 100644 --- a/evennia/contrib/tutorials/evadventure/npcs.py +++ b/evennia/contrib/tutorials/evadventure/npcs.py @@ -220,6 +220,7 @@ class EvAdventureShopKeeper(EvAdventureTalkativeNPC): 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) + # prototypes of common wares common_ware_prototypes = AttributeProperty([], autocreate=False) def at_damage(self, damage, attacker=None): diff --git a/evennia/contrib/tutorials/evadventure/shops.py b/evennia/contrib/tutorials/evadventure/shops.py index fe00e24a57..0d8c7a7e69 100644 --- a/evennia/contrib/tutorials/evadventure/shops.py +++ b/evennia/contrib/tutorials/evadventure/shops.py @@ -36,22 +36,213 @@ node name will be the name of the option capitalized, with underscores replaced """ +from dataclasses import dataclass from random import choice -from evennia.utils.evmenu import EvMenu +from evennia.prototypes.prototypes import search_prototype +from evennia.prototypes.spawner import flatten_prototype +from evennia.utils.evmenu import EvMenu, list_node +from evennia.utils.logger import log_err, log_trace from evennia.utils.utils import make_iter +from .enums import Ability, ObjType, WieldLocation from .npcs import EvAdventureShopKeeper + +@dataclass +class BuyItem: + """ + Storage container for storing generic info about an item for sale. This means it can be used + both for real objects and for prototypes without constantly having to track which is which. + + """ + + # skipping typehints here since we are not using them anywhere else + + # available for all buyable items + key = "" + desc = "" + obj_type = ObjType.GEAR + size = 1 + value = 0 + use_slot = WieldLocation.BACKPACK + + uses = None + quality = None + attack_type = None + defense_type = None + damage_roll = None + + # references the original (always only one of the two) + obj = None + prototype = None + + @staticmethod + def create_from_obj(obj, shopkeeper): + """ + Build a new BuyItem container from a real db obj. + + Args: + obj (EvAdventureObject): An object to analyze. + shopkeeper (EvAdventureShopKeeper): The shopkeeper. + + Returns: + BuyItem: A general representation of the original data. + + """ + try: + # mandatory + key = obj.key + desc = obj.db.desc + obj_type = obj.obj_type + size = obj.size + use_slot = obj.use_slot + value = obj.value * shopkeeper.upsell_factor + except AttributeError: + # not a buyable item + log_trace("Not a buyable item") + return None + + # getting optional properties + + return BuyItem( + key=key, + desc=desc, + obj_type=obj_type, + size=size, + use_slot=use_slot, + value=value, + # optional fields + uses=getattr(obj, "uses", None), + quality=getattr(obj, "quality", None), + attack_type=getattr(obj, "attack_type", None), + defense_type=getattr(obj, "defense_type", None), + damage_roll=getattr(obj, "damage_roll", None), + # back-reference (don't set prototype) + obj=obj, + ) + + @staticmethod + def create_from_prototype(self, prototype_or_key, shopkeeper): + """ + Build a new BuyItem container from a prototype. + + Args: + prototype (dict or key): An Evennia prototype dict or the key of one + registered with the system. This is assumed to be a full prototype, + including having parsed and included parentage. + + Returns: + BuyItem: A general representation of the original data. + + """ + + def _get_attr_value(key, prot, optional=True): + """ + We want the attribute's value, which is always in the `attrs` field of + the prototype. + + """ + attr = [tup for tup in prot.get("attrs", ()) if tup[0] == key] + try: + return attr[0][1] + except IndexError: + if optional: + return None + raise + + if isinstance(prototype_or_key, dict): + prototype = prototype_or_key + else: + # make sure to generate a 'full' prototype with all inheritance applied ('flattened'), + # otherwise we will not get inherited data when we analyze it. + prototype = flatten_prototype(search_prototype(key=prototype_or_key)) + + if not prototype: + log_err(f"No valid prototype '{prototype_or_key}' found") + return None + + try: + # at this point we should have a full, flattened prototype ready to spawn. It must + # contain all fields needed for buying + key = prototype["key"] + desc = _get_attr_value("desc", prototype, optional=False) + obj_type = _get_attr_value("obj_type", prototype, optional=False) + size = _get_attr_value("size", prototype, optional=False) + use_slot = _get_attr_value("use_slot", prototype, optional=False) + value = int(_get_attr_value("value", prototype, optional=False) + * shopkeeper.upsell_factor) + except (KeyError, IndexError): + # not a buyable item + log_trace("Not a buyable item") + return None + + return BuyItem( + key=key, + desc=desc, + obj_type=obj_type, + size=size, + use_slot=use_slot, + value=value, + # optional fields + uses=_get_attr_value("uses", prototype), + quality=_get_attr_value("quality", prototype), + attack_type=_get_attr_value("attack_type", prototype), + defense_type=_get_attr_value("defense_type", prototype), + damage_roll=_get_attr_value("damage_roll", prototype), + # back-reference (don't set obj) + prototype=prototype, + ) + + def get_sdesc(self): + """ + Get the short description to show in buy list. + + """ + return self.key + + def get_detail(self): + """ + Get more info when looking at the item. + + """ + return f""" + |c{self.key}|n + {self.desc} + + Slots: {self.size} Used from: {self.use_slot.value} + + + +# Helper functions for building the shop listings and select a ware to buy +def _get_all_wares_to_buy(caller, raw_string, **kwargs): + """ + This helper is used by `EvMenu.list_node` to build the list of items to buy. + + We rely on `**kwargs` being forwarded from `node_start_buy`, which in turns contains + the `npc` kwarg pointing to the shopkeeper (`caller` is the one doing the buying). + + """ + shopkeep = kwargs["npc"] + # items carried by the shopkeep are sellable (these are items already created, such as + # things sold to the shopkeep earlier). We + wares = [BuyItem.create_from_obj(obj) for obj in list(shopkeep.contents)] + [ + BuyItem.create_from_prototype(prototype) for prototype in shopkeep.common_ware_prototypes + ] + # clean out any ByItems that failed to create for some reason + wares = [ware for ware in wares if ware] + + # shop menu nodes to use for building a Shopkeeper npc +@list_node(_get_all_wares_to_buy, select=_select_ware_to_buy, pagesize=10) def node_start_buy(caller, raw_string, **kwargs): """ 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 + Items available to sell are a combination of items in the shopkeep's inventory and 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).