2020-04-07 23:13:24 +02:00
|
|
|
# NPC shop Tutorial
|
|
|
|
|
|
2021-10-21 21:04:14 +02:00
|
|
|
This tutorial will describe how to make an NPC-run shop. We will make use of the [EvMenu](../Components/EvMenu.md)
|
2020-06-16 16:53:35 +02:00
|
|
|
system to present shoppers with a menu where they can buy things from the store's stock.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
Our shop extends over two rooms - a "front" room open to the shop's customers and a locked "store
|
|
|
|
|
room" holding the wares the shop should be able to sell. We aim for the following features:
|
2020-04-07 23:13:24 +02:00
|
|
|
|
|
|
|
|
- The front room should have an Attribute `storeroom` that points to the store room.
|
2020-06-16 16:53:35 +02:00
|
|
|
- Inside the front room, the customer should have a command `buy` or `browse`. This will open a
|
|
|
|
|
menu listing all items available to buy from the store room.
|
2020-04-07 23:13:24 +02:00
|
|
|
- A customer should be able to look at individual items before buying.
|
2020-06-16 16:53:35 +02:00
|
|
|
- We use "gold" as an example currency. To determine cost, the system will look for an Attribute
|
|
|
|
|
`gold_value` on the items in the store room. If not found, a fixed base value of 1 will be assumed.
|
|
|
|
|
The wealth of the customer should be set as an Attribute `gold` on the Character. If not set, they
|
|
|
|
|
have no gold and can't buy anything.
|
|
|
|
|
- When the customer makes a purchase, the system will check the `gold_value` of the goods and
|
|
|
|
|
compare it to the `gold` Attribute of the customer. If enough gold is available, this will be
|
|
|
|
|
deducted and the goods transferred from the store room to the inventory of the customer.
|
2020-04-07 23:13:24 +02:00
|
|
|
- We will lock the store room so that only people with the right key can get in there.
|
|
|
|
|
|
2021-10-21 21:04:14 +02:00
|
|
|
## The shop menu
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
We want to show a menu to the customer where they can list, examine and buy items in the store. This
|
|
|
|
|
menu should change depending on what is currently for sale. Evennia's *EvMenu* utility will manage
|
2021-10-21 21:04:14 +02:00
|
|
|
the menu for us. It's a good idea to [read up on EvMenu](../Components/EvMenu.md) if you are not familiar with it.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2021-10-21 21:04:14 +02:00
|
|
|
### Designing the menu
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
The shopping menu's design is straightforward. First we want the main screen. You get this when you
|
|
|
|
|
enter a shop and use the `browse` or `buy` command:
|
2020-04-07 23:13:24 +02:00
|
|
|
|
|
|
|
|
```
|
|
|
|
|
*** 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)
|
|
|
|
|
```
|
|
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
There are only three items to buy in this example but the menu should expand to however many items
|
|
|
|
|
are needed. When you make a selection you will get a new screen showing the options for that
|
|
|
|
|
particular item:
|
2020-04-07 23:13:24 +02:00
|
|
|
|
|
|
|
|
```
|
|
|
|
|
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.
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Finally, when you buy something, a brief message should pop up:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
You pay 5 gold and purchase A rusty sword!
|
|
|
|
|
```
|
|
|
|
|
or
|
|
|
|
|
```
|
|
|
|
|
You cannot afford 5 gold for A rusty sword!
|
|
|
|
|
```
|
|
|
|
|
After this you should be back to the top level of the shopping menu again and can continue browsing.
|
|
|
|
|
|
2021-10-21 21:04:14 +02:00
|
|
|
### Coding the menu
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
EvMenu defines the *nodes* (each menu screen with options) as normal Python functions. Each node
|
|
|
|
|
must be able to change on the fly depending on what items are currently for sale. EvMenu will
|
|
|
|
|
automatically make the `quit` command available to us so we won't add that manually. For compactness
|
|
|
|
|
we will put everything needed for our shop in one module, `mygame/typeclasses/npcshop.py`.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
# mygame/typeclasses/npcshop.py
|
|
|
|
|
|
|
|
|
|
from evennia.utils import evmenu
|
|
|
|
|
|
|
|
|
|
def menunode_shopfront(caller):
|
|
|
|
|
"This is the top-menu screen."
|
|
|
|
|
|
|
|
|
|
shopname = caller.location.key
|
|
|
|
|
wares = caller.location.db.storeroom.contents
|
|
|
|
|
|
|
|
|
|
# Wares includes all items inside the storeroom, including the
|
|
|
|
|
# door! Let's remove that from our for sale list.
|
|
|
|
|
wares = [ware for ware in wares if ware.key.lower() != "door"]
|
|
|
|
|
|
2021-10-12 12:13:42 -06:00
|
|
|
text = f"*** Welcome to {shopname}! ***\n"
|
2020-04-07 23:13:24 +02:00
|
|
|
if wares:
|
2021-10-12 12:13:42 -06:00
|
|
|
text += f" Things for sale (choose 1-{len(wares)} to inspect); quit to exit:"
|
2020-04-07 23:13:24 +02:00
|
|
|
else:
|
|
|
|
|
text += " There is nothing for sale; quit to exit."
|
|
|
|
|
|
|
|
|
|
options = []
|
|
|
|
|
for ware in wares:
|
|
|
|
|
# add an option for every ware in store
|
2021-10-12 12:13:42 -06:00
|
|
|
gold_val = ware.db.gold_value or 1
|
|
|
|
|
options.append({"desc": f"{ware.key} ({gold_val} gold)",
|
2020-04-07 23:13:24 +02:00
|
|
|
"goto": "menunode_inspect_and_buy"})
|
|
|
|
|
return text, options
|
|
|
|
|
```
|
|
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
In this code we assume the caller to be *inside* the shop when accessing the menu. This means we can
|
|
|
|
|
access the shop room via `caller.location` and get its `key` to display as the shop's name. We also
|
|
|
|
|
assume the shop has an Attribute `storeroom` we can use to get to our stock. We loop over our goods
|
|
|
|
|
to build up the menu's options.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
Note that *all options point to the same menu node* called `menunode_inspect_and_buy`! We can't know
|
|
|
|
|
which goods will be available to sale so we rely on this node to modify itself depending on the
|
|
|
|
|
circumstances. Let's create it now.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
# further down in mygame/typeclasses/npcshop.py
|
|
|
|
|
|
|
|
|
|
def menunode_inspect_and_buy(caller, raw_string):
|
|
|
|
|
"Sets up the buy menu screen."
|
|
|
|
|
|
|
|
|
|
wares = caller.location.db.storeroom.contents
|
|
|
|
|
# Don't forget, we will need to remove that pesky door again!
|
|
|
|
|
wares = [ware for ware in wares if ware.key.lower() != "door"]
|
|
|
|
|
iware = int(raw_string) - 1
|
|
|
|
|
ware = wares[iware]
|
|
|
|
|
value = ware.db.gold_value or 1
|
|
|
|
|
wealth = caller.db.gold or 0
|
2021-10-12 12:13:42 -06:00
|
|
|
text = f"You inspect {ware.key}:\n\n{ware.db.desc}"
|
2020-04-07 23:13:24 +02:00
|
|
|
|
|
|
|
|
def buy_ware_result(caller):
|
|
|
|
|
"This will be executed first when choosing to buy."
|
|
|
|
|
if wealth >= value:
|
2021-10-12 12:13:42 -06:00
|
|
|
rtext = f"You pay {value} gold and purchase {ware.key}!"
|
2020-04-07 23:13:24 +02:00
|
|
|
caller.db.gold -= value
|
|
|
|
|
ware.move_to(caller, quiet=True)
|
|
|
|
|
else:
|
2021-10-12 12:13:42 -06:00
|
|
|
rtext = f"You cannot afford {value} gold for {ware.key}!"
|
2020-04-07 23:13:24 +02:00
|
|
|
caller.msg(rtext)
|
|
|
|
|
|
2021-10-12 12:13:42 -06:00
|
|
|
gold_val = ware.db.gold_value or 1
|
|
|
|
|
options = ({
|
|
|
|
|
"desc": f"Buy {ware.key} for {gold_val} gold",
|
|
|
|
|
"goto": "menunode_shopfront",
|
|
|
|
|
"exec": buy_ware_result,
|
|
|
|
|
}, {
|
|
|
|
|
"desc": "Look for something else",
|
|
|
|
|
"goto": "menunode_shopfront",
|
|
|
|
|
})
|
2020-04-07 23:13:24 +02:00
|
|
|
|
|
|
|
|
return text, options
|
|
|
|
|
```
|
|
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
In this menu node we make use of the `raw_string` argument to the node. This is the text the menu
|
|
|
|
|
user entered on the *previous* node to get here. Since we only allow numbered options in our menu,
|
|
|
|
|
`raw_input` must be an number for the player to get to this point. So we convert it to an integer
|
|
|
|
|
index (menu lists start from 1, whereas Python indices always starts at 0, so we need to subtract
|
|
|
|
|
1). We then use the index to get the corresponding item from storage.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
We just show the customer the `desc` of the item. In a more elaborate setup you might want to show
|
|
|
|
|
things like weapon damage and special stats here as well.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
When the user choose the "buy" option, EvMenu will execute the `exec` instruction *before* we go
|
|
|
|
|
back to the top node (the `goto` instruction). For this we make a little inline function
|
|
|
|
|
`buy_ware_result`. EvMenu will call the function given to `exec` like any menu node but it does not
|
|
|
|
|
need to return anything. In `buy_ware_result` we determine if the customer can afford the cost and
|
|
|
|
|
give proper return messages. This is also where we actually move the bought item into the inventory
|
|
|
|
|
of the customer.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2021-10-21 21:04:14 +02:00
|
|
|
### The command to start the menu
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
We could *in principle* launch the shopping menu the moment a customer steps into our shop room, but
|
2021-10-21 21:04:14 +02:00
|
|
|
this would probably be considered pretty annoying. It's better to create a [Command](../Components/Commands.md) for
|
2020-06-16 16:53:35 +02:00
|
|
|
customers to explicitly wanting to shop around.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
# mygame/typeclasses/npcshop.py
|
|
|
|
|
|
|
|
|
|
from evennia import Command
|
|
|
|
|
|
|
|
|
|
class CmdBuy(Command):
|
|
|
|
|
"""
|
|
|
|
|
Start to do some shopping
|
|
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
|
buy
|
|
|
|
|
shop
|
|
|
|
|
browse
|
|
|
|
|
|
|
|
|
|
This will allow you to browse the wares of the
|
|
|
|
|
current shop and buy items you want.
|
|
|
|
|
"""
|
|
|
|
|
key = "buy"
|
|
|
|
|
aliases = ("shop", "browse")
|
|
|
|
|
|
|
|
|
|
def func(self):
|
|
|
|
|
"Starts the shop EvMenu instance"
|
|
|
|
|
evmenu.EvMenu(self.caller,
|
|
|
|
|
"typeclasses.npcshop",
|
|
|
|
|
startnode="menunode_shopfront")
|
|
|
|
|
```
|
|
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
This will launch the menu. The `EvMenu` instance is initialized with the path to this very module -
|
|
|
|
|
since the only global functions available in this module are our menu nodes, this will work fine
|
|
|
|
|
(you could also have put those in a separate module). We now just need to put this command in a
|
2021-10-21 21:04:14 +02:00
|
|
|
[CmdSet](../Components/Command-Sets.md) so we can add it correctly to the game:
|
2020-04-07 23:13:24 +02:00
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from evennia import CmdSet
|
|
|
|
|
|
|
|
|
|
class ShopCmdSet(CmdSet):
|
|
|
|
|
def at_cmdset_creation(self):
|
|
|
|
|
self.add(CmdBuy())
|
|
|
|
|
```
|
|
|
|
|
|
2021-10-21 21:04:14 +02:00
|
|
|
## Building the shop
|
2020-04-07 23:13:24 +02:00
|
|
|
|
|
|
|
|
There are really only two things that separate our shop from any other Room:
|
|
|
|
|
|
|
|
|
|
- The shop has the `storeroom` Attribute set on it, pointing to a second (completely normal) room.
|
2020-06-16 16:53:35 +02:00
|
|
|
- It has the `ShopCmdSet` stored on itself. This makes the `buy` command available to users entering
|
|
|
|
|
the shop.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
For testing we could easily add these features manually to a room using `@py` or other admin
|
2021-10-21 21:04:14 +02:00
|
|
|
commands. Just to show how it can be done we'll instead make a custom [Typeclass](../Components/Typeclasses.md) for
|
2020-06-16 16:53:35 +02:00
|
|
|
the shop room and make a small command that builders can use to build both the shop and the
|
|
|
|
|
storeroom at once.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
# bottom of mygame/typeclasses/npcshop.py
|
|
|
|
|
|
|
|
|
|
from evennia import DefaultRoom, DefaultExit, DefaultObject
|
|
|
|
|
from evennia.utils.create import create_object
|
|
|
|
|
|
|
|
|
|
# class for our front shop room
|
|
|
|
|
class NPCShop(DefaultRoom):
|
|
|
|
|
def at_object_creation(self):
|
2021-08-06 17:16:44 +02:00
|
|
|
# we could also use add(ShopCmdSet, persistent=True)
|
2020-04-07 23:13:24 +02:00
|
|
|
self.cmdset.add_default(ShopCmdSet)
|
|
|
|
|
self.db.storeroom = None
|
|
|
|
|
|
|
|
|
|
# command to build a complete shop (the Command base class
|
|
|
|
|
# should already have been imported earlier in this file)
|
|
|
|
|
class CmdBuildShop(Command):
|
|
|
|
|
"""
|
|
|
|
|
Build a new shop
|
|
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
|
@buildshop shopname
|
|
|
|
|
|
|
|
|
|
This will create a new NPCshop room
|
|
|
|
|
as well as a linked store room (named
|
|
|
|
|
simply <storename>-storage) for the
|
|
|
|
|
wares on sale. The store room will be
|
|
|
|
|
accessed through a locked door in
|
|
|
|
|
the shop.
|
|
|
|
|
"""
|
|
|
|
|
key = "@buildshop"
|
|
|
|
|
locks = "cmd:perm(Builders)"
|
|
|
|
|
help_category = "Builders"
|
|
|
|
|
|
|
|
|
|
def func(self):
|
|
|
|
|
"Create the shop rooms"
|
|
|
|
|
if not self.args:
|
|
|
|
|
self.msg("Usage: @buildshop <storename>")
|
|
|
|
|
return
|
|
|
|
|
# create the shop and storeroom
|
|
|
|
|
shopname = self.args.strip()
|
|
|
|
|
shop = create_object(NPCShop,
|
|
|
|
|
key=shopname,
|
|
|
|
|
location=None)
|
|
|
|
|
storeroom = create_object(DefaultRoom,
|
2021-10-12 12:13:42 -06:00
|
|
|
key=f"{shopname}-storage",
|
2020-04-07 23:13:24 +02:00
|
|
|
location=None)
|
|
|
|
|
shop.db.storeroom = storeroom
|
|
|
|
|
# create a door between the two
|
|
|
|
|
shop_exit = create_object(DefaultExit,
|
|
|
|
|
key="back door",
|
|
|
|
|
aliases=["storage", "store room"],
|
|
|
|
|
location=shop,
|
|
|
|
|
destination=storeroom)
|
|
|
|
|
storeroom_exit = create_object(DefaultExit,
|
|
|
|
|
key="door",
|
|
|
|
|
location=storeroom,
|
|
|
|
|
destination=shop)
|
|
|
|
|
# make a key for accessing the store room
|
2021-10-12 12:13:42 -06:00
|
|
|
storeroom_key_name = f"{shopname}-storekey"
|
2020-04-07 23:13:24 +02:00
|
|
|
storeroom_key = create_object(DefaultObject,
|
|
|
|
|
key=storeroom_key_name,
|
|
|
|
|
location=shop)
|
|
|
|
|
# only allow chars with this key to enter the store room
|
2021-10-12 12:13:42 -06:00
|
|
|
shop_exit.locks.add(f"traverse:holds({storeroom_key_name})")
|
2020-04-07 23:13:24 +02:00
|
|
|
|
|
|
|
|
# inform the builder about progress
|
2021-10-12 12:13:42 -06:00
|
|
|
self.caller.msg(f"The shop {shop} was created!")
|
2020-04-07 23:13:24 +02:00
|
|
|
```
|
|
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
Our typeclass is simple and so is our `buildshop` command. The command (which is for Builders only)
|
|
|
|
|
just takes the name of the shop and builds the front room and a store room to go with it (always
|
|
|
|
|
named `"<shopname>-storage"`. It connects the rooms with a two-way exit. You need to add
|
2020-06-19 15:56:02 +02:00
|
|
|
`CmdBuildShop` [to the default cmdset](Starting/Adding-Command-Tutorial#step-2-adding-the-command-to-a-
|
2020-06-16 16:53:35 +02:00
|
|
|
default-cmdset) before you can use it. Once having created the shop you can now `@teleport` to it or
|
|
|
|
|
`@open` a new exit to it. You could also easily expand the above command to automatically create
|
|
|
|
|
exits to and from the new shop from your current location.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2021-10-21 21:04:14 +02:00
|
|
|
To avoid customers walking in and stealing everything, we create a [Lock](../Components/Locks.md) on the storage
|
2020-06-16 16:53:35 +02:00
|
|
|
door. It's a simple lock that requires the one entering to carry an object named
|
|
|
|
|
`<shopname>-storekey`. We even create such a key object and drop it in the shop for the new shop
|
|
|
|
|
keeper to pick up.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
> If players are given the right to name their own objects, this simple lock is not very secure and
|
|
|
|
|
you need to come up with a more robust lock-key solution.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
> We don't add any descriptions to all these objects so looking "at" them will not be too thrilling.
|
|
|
|
|
You could add better default descriptions as part of the `@buildshop` command or leave descriptions
|
|
|
|
|
this up to the Builder.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2021-10-21 21:04:14 +02:00
|
|
|
## The shop is open for business!
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
We now have a functioning shop and an easy way for Builders to create it. All you need now is to
|
|
|
|
|
`@open` a new exit from the rest of the game into the shop and put some sell-able items in the store
|
|
|
|
|
room. Our shop does have some shortcomings:
|
2020-04-07 23:13:24 +02:00
|
|
|
|
2020-06-16 16:53:35 +02:00
|
|
|
- For Characters to be able to buy stuff they need to also have the `gold` Attribute set on
|
|
|
|
|
themselves.
|
|
|
|
|
- We manually remove the "door" exit from our items for sale. But what if there are other unsellable
|
|
|
|
|
items in the store room? What if the shop owner walks in there for example - anyone in the store
|
|
|
|
|
could then buy them for 1 gold.
|
|
|
|
|
- What if someone else were to buy the item we're looking at just before we decide to buy it? It
|
|
|
|
|
would then be gone and the counter be wrong - the shop would pass us the next item in the list.
|
2020-04-07 23:13:24 +02:00
|
|
|
|
|
|
|
|
Fixing these issues are left as an exercise.
|
|
|
|
|
|
2021-10-21 21:04:14 +02:00
|
|
|
If you want to keep the shop fully NPC-run you could add a [Script](../Components/Scripts.md) to restock the shop's
|
2020-06-16 16:53:35 +02:00
|
|
|
store room regularly. This shop example could also easily be owned by a human Player (run for them
|
|
|
|
|
by a hired NPC) - the shop owner would get the key to the store room and be responsible for keeping
|
2021-08-06 17:16:44 +02:00
|
|
|
it well stocked.
|