NPC merchants

*** Welcome to ye Old Sword shop! ***
   Things for sale (choose 1-3 to inspect, quit to exit):
_________________________________________________________
1. A rusty sword (5 gold)
2. A sword with a leather handle (10 gold)
3. Excalibur (100 gold)

This will introduce an NPC able to sell things. In practice this means that when you interact with them you’ll get shown a menu of choices. Evennia provides the EvMenu utility to easily create in-game menus.

We will store all the merchant’s wares in their inventory. This means that they may stand in an actual shop room, at a market or wander the road. We will also use ‘gold’ as an example currency.
To enter the shop, you’ll just need to stand in the same room and use the buy/shop command.

Making the merchant class

The merchant will respond to you giving the shop or buy command in their presence.

# in for example mygame/typeclasses/merchants.py 

from typeclasses.objects import Object
from evennia import Command, CmdSet, EvMenu

class CmdOpenShop(Command): 
    """
    Open the shop! 

    Usage:
        shop/buy 

    """
    key = "shop"
    aliases = ["buy"]

    def func(self):
        # this will sit on the Merchant, which is self.obj. 
        # the self.caller is the player wanting to buy stuff.    
        self.obj.open_shop(self.caller)
        

class MerchantCmdSet(CmdSet):
    def at_cmdset_creation(self):
        self.add(CmdOpenShop())


class NPCMerchant(Object):

     def at_object_creation(self):
         self.cmdset.add_default(MerchantCmdSet)

     def open_shop(self, shopper):
         menunodes = {}  # TODO! 
         shopname = self.db.shopname or "The shop"
         EvMenu(shopper, menunodes, startnode="shopfront", 
                shopname=shopname, shopkeeper=self, wares=self.contents)

We could also have put the commands in a separate module, but for compactness, we put it all with the merchant typeclass.

Note that we make the merchant an Object! Since we don’t give them any other commands, it makes little sense to let them be a Character.

We make a very simple shop/buy Command and make sure to add it on the merchant in its own cmdset.

We initialize EvMenu on the shopper but we haven’t created any menunodes yet, so this will not actually do much at this point. It’s important that we we pass shopname, shopkeeper and wares into the menu, it means they will be made available as properties on the EvMenu instance - we will be able to access them from inside the menu.

Coding the shopping menu

EvMenu splits the menu into nodes represented by Python functions. Each node represents a stop in the menu where the user has to make a choice.

For simplicity, we’ll code the shop interface above the NPCMerchant class in the same module.

The start node of the shop named “ye Old Sword shop!” will look like this if there are only 3 wares to sell:

*** Welcome to ye Old Sword shop! ***
   Things for sale (choose 1-3 to inspect, quit to exit):
_________________________________________________________
1. A rusty sword (5 gold)
2. A sword with a leather handle (10 gold)
3. Excalibur (100 gold)
# in mygame/typeclasses/merchants.py

# top of module, above NPCMerchant class.

def node_shopfront(caller, raw_string, **kwargs):
    "This is the top-menu screen."

    # made available since we passed them to EvMenu on start 
    menu = caller.ndb._evmenu
    shopname = menu.shopname
    shopkeeper = menu.shopkeeper 
    wares = menu.wares

    text = f"*** Welcome to {shopname}! ***\n"
    if wares:
        text += f"   Things for sale (choose 1-{len(wares)} to inspect); quit to exit:"
    else:
        text += "   There is nothing for sale; quit to exit."

    options = []
    for ware in wares:
        # add an option for every ware in store
        gold_val = ware.db.gold_value or 1
        options.append({"desc": f"{ware.key} ({gold_val} gold)",
                        "goto": ("inspect_and_buy", 
                                 {"selected_ware": ware})
                       })
                       
    return text, options

Inside the node we can access the menu on the caller as caller.ndb._evmenu. The extra keywords we passed into EvMenu are available on this menu instance. Armed with this we can easily present a shop interface. Each option will become a numbered choice on this screen.

Note how we pass the ware with each option and label it selected_ware. This will be accessible in the next node’s **kwargs argument

If a player choose one of the wares, they should be able to inspect it. Here’s how it should look if they selected 1 in ye Old Sword shop:

You inspect A rusty sword:

This is an old weapon maybe once used by soldiers in some
long forgotten army. It is rusty and in bad condition.
__________________________________________________________
1. Buy A rusty sword (5 gold)
2. Look for something else.

If you buy, you’ll see

You pay 5 gold and purchase A rusty sword!

or

You cannot afford 5 gold for A rusty sword!

Either way you should end up back at the top level of the shopping menu again and can continue browsing or quit the menu with quit.

Here’s how it looks in code:

# in mygame/typeclasses/merchants.py 

# right after the other node

def _buy_item(caller, raw_string, **kwargs):
    "Called if buyer chooses to buy"
    selected_ware = kwargs["selected_ware"]
    value = selected_ware.db.gold_value or 1
    wealth = caller.db.gold or 0

    if wealth >= value:
        rtext = f"You pay {value} gold and purchase {ware.key}!"
        caller.db.gold -= value
        move_to(caller, quiet=True, move_type="buy")
    else:
        rtext = f"You cannot afford {value} gold for {ware.key}!"
    caller.msg(rtext)
    # no matter what, we return to the top level of the shop
    return "shopfront"

def node_inspect_and_buy(caller, raw_string, **kwargs):
    "Sets up the buy menu screen."

    # passed from the option we chose 
    selected_ware = kwargs["selected_ware"]

    value = selected_ware.db.gold_value or 1
    text = f"You inspect {ware.key}:\n\n{ware.db.desc}"
    gold_val = ware.db.gold_value or 1

    options = ({
        "desc": f"Buy {ware.key} for {gold_val} gold",
        "goto": (_buy_item, kwargs)
    }, {
        "desc": "Look for something else",
        "goto": "shopfront",
    })
    return text, options

In this node we grab the selected_ware from kwargs - this we pased along from the option on the previous node. We display its description and value. If the user buys, we reroute through the _buy_item helper function (this is not a node, it’s just a callable that must return the name of the next node to go to.). In _buy_item we check if the buyer can affort the ware, and if it can we move it to their inventory. Either way, this method returns shop_front as the next node.

We have been referring to two nodes here: "shopfront" and "inspect_and_buy" , we should map them to the code in the menu. Scroll down to the NPCMerchant class in the same module and find that unfinished open_shop method again:

# in /mygame/typeclasses/merchants.py

def node_shopfront(caller, raw_string, **kwargs):
    # ... 

def _buy_item(caller, raw_string, **kwargs):
    # ...

def node_inspect_and_buy(caller, raw_string, **kwargs):
    # ... 

class NPCMerchant(Object):

     # ...

     def open_shop(self, shopper):
         menunodes = {
             "shopfront": node_shopfront,
             "inspect_and_buy": node_inspect_and_buy
         }
         shopname = self.db.shopname or "The shop"
         EvMenu(shopper, menunodes, startnode="shopfront", 
                shopname=shopname, shopkeeper=self, wares=self.contents)

We now added the nodes to the Evmenu under their right labels. The merchant is now ready!

The shop is open for business!

Make sure to reload.

Let’s try it out by creating the merchant and a few wares in-game. Remember that we also must create some gold get this economy going.

> set self/gold = 8

> create/drop Stan S. Stanman;stan:typeclasses.merchants.NPCMerchant
> set stan/shopname = Stan's previously owned vessles

> create/drop A proud vessel;ship 
> set ship/desc = The thing has holes in it.
> set ship/gold_value = 5

> create/drop A classic speedster;rowboat 
> set rowboat/gold_value = 2
> set rowboat/desc = It's not going anywhere fast.

Note that a builder without any access to Python code can now set up a personalized merchant with just in-game commands. With the shop all set up, we just need to be in the same room to start consuming!

> buy
*** Welcome to Stan's previously owned vessels! ***
   Things for sale (choose 1-3 to inspect, quit to exit):
_________________________________________________________
1. A proud vessel (5 gold)
2. A classic speedster (2 gold)

> 1 

You inspect A proud vessel:

The thing has holes in it.
__________________________________________________________
1. Buy A proud vessel (5 gold)
2. Look for something else.

> 1
You pay 5 gold and purchase A proud vessel!

*** Welcome to Stan's previously owned vessels! ***
   Things for sale (choose 1-3 to inspect, quit to exit):
_________________________________________________________
1. A classic speedster (2 gold)