mirror of
https://github.com/evennia/evennia.git
synced 2026-03-17 21:36:30 +01:00
277 lines
9.2 KiB
Markdown
277 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)
|
||
|
|
|
||
|
|
```
|
||
|
|
|