evennia/docs/source/Howtos/Tutorial-NPC-Merchants.md

276 lines
9.2 KiB
Markdown

# 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](../Components/EvMenu.md) 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.
```python
# 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="shop_start",
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](../Components/EvMenu.md) 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)
```
```python
# 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:
```python
# 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:
```python
# 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="shop_start",
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)
```