NPC 商人

*** 欢迎来到旧剑店! ***
   可供出售的物品(选择 1-3 检查,quit 退出):
_________________________________________________________
1. 一把生锈的剑(5 金)
2. 一把皮革把手的剑(10 金)
3. Excalibur(100 金)

本文将介绍一个能够出售物品的 NPC。在实践中,这意味着当你与他们互动时,你将看到一个 菜单 选项。Evennia 提供了 EvMenu 工具,以便轻松创建游戏内菜单。

我们将把商人的商品存放在他们的库存中。这意味着他们可能站在一个实际的商店房间里、在市场中或在路上漫游。我们还将使用“金”作为示例货币。要进入商店,只需在同一房间内使用 buy/shop 命令即可。

创建商人类

商人会对你在他们身边给出的 shopbuy 命令做出反应。

# 在例如 mygame/typeclasses/merchants.py 中

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

class CmdOpenShop(Command): 
    """
    打开商店! 

    用法:
        shop/buy 

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

    def func(self):
        # 这将位于商人上,即 self.obj。 
        # self.caller 是想要购买东西的玩家。    
        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 "商店"
         EvMenu(shopper, menunodes, startnode="shopfront", 
                shopname=shopname, shopkeeper=self, wares=self.contents)

我们也可以将命令放在单独的模块中,但为了紧凑性,我们将其与商人类型类放在一起。

注意我们将商人设置为一个 Object!因为我们没有给他们任何其他命令,所以将他们设为 Character 并没有太大意义。

我们创建了一个非常简单的 shop/buy 命令,并确保将其添加到商人的命令集中。

我们在 shopper 上初始化 EvMenu,但我们还没有创建任何 menunodes,所以此时不会实际做什么。重要的是,我们将 shopnameshopkeeperwares 传递给菜单,这意味着它们将作为属性可用于 EvMenu 实例 - 我们可以在菜单内部访问它们。

编写购物菜单

EvMenu 将菜单拆分为由 Python 函数表示的 节点。每个节点表示菜单中的一个停靠点,用户必须在此做出选择。

为了简单起见,我们将在同一模块中的 NPCMerchant 类上方编写商店接口。

商店的起始节点名为“旧剑店!”如果有 3 种商品出售,它将如下所示:

*** 欢迎来到旧剑店! ***
   可供出售的物品(选择 1-3 检查,quit 退出):
_________________________________________________________
1. 一把生锈的剑(5 金)
2. 一把皮革把手的剑(10 金)
3. Excalibur(100 金)
# 在 mygame/typeclasses/merchants.py 中

# 模块顶部,在 NPCMerchant 类之上。

def node_shopfront(caller, raw_string, **kwargs):
    "这是顶部菜单屏幕。"

    # 由于我们传递给 EvMenu,所以可用的菜单 
    menu = caller.ndb._evmenu
    shopname = menu.shopname
    shopkeeper = menu.shopkeeper 
    wares = menu.wares

    text = f"*** 欢迎来到 {shopname}! ***\n"
    if wares:
        text += f"   可供出售的物品(选择 1-{len(wares)} 检查);quit 退出:"
    else:
        text += "   没有什么可出售的;quit 退出。"

    options = []
    for ware in wares:
        # 为商店中的每个物品添加一个选项
        gold_val = ware.db.gold_value or 1
        options.append({"desc": f"{ware.key} ({gold_val} 金)",
                        "goto": ("inspect_and_buy", 
                                 {"selected_ware": ware})
                       })
                       
    return text, options

在节点内部,我们可以通过 caller.ndb._evmenu 访问菜单。我们传递给 EvMenu 的额外关键字在此菜单实例上可用。借助这个信息,我们可以轻松呈现一个商店界面。每个选项将成为此屏幕上的一个编号选择。

注意我们将 ware 与每个选项一起传递,并将其标记为 selected_ware。这将在下一个节点的 **kwargs 参数中可访问。

如果玩家选择了一个商品,他们应该能够进行检查。如果他们在旧剑店选择了 1,它将如下所示:

你检查了一把生锈的剑:

这是一件旧武器,也许曾被一些
被遗忘的军队的士兵使用。它生锈了,状况不佳。
__________________________________________________________
1. 购买一把生锈的剑(5 金)
2. 寻找其他东西。

如果你购买,你会看到

你支付 5 金并购买了一把生锈的剑!

或者

你无法负担 5 金购买一把生锈的剑!

无论哪种情况,你都应该最终返回购物菜单的顶层,继续浏览或使用 quit 退出菜单。

以下是代码示例:

# 在 mygame/typeclasses/merchants.py 中 

# 在其他节点之后

def _buy_item(caller, raw_string, **kwargs):
    "当买家选择购买时调用"
    selected_ware = kwargs["selected_ware"]
    value = selected_ware.db.gold_value or 1
    wealth = caller.db.gold or 0

    if wealth >= value:
        rtext = f"你支付 {value} 金并购买了 {selected_ware.key}!"
        caller.db.gold -= value
        move_to(caller, quiet=True, move_type="buy")
    else:
        rtext = f"你无法负担 {value} 金购买 {selected_ware.key}!"
    caller.msg(rtext)
    # 无论如何,返回商店的顶层
    return "shopfront"

def node_inspect_and_buy(caller, raw_string, **kwargs):
    "设置购买菜单屏幕。"

    # 从我们选择的选项中传递 
    selected_ware = kwargs["selected_ware"]

    value = selected_ware.db.gold_value or 1
    text = f"你检查了 {selected_ware.key}:\n\n{selected_ware.db.desc}"
    gold_val = selected_ware.db.gold_value or 1

    options = ({
        "desc": f"以 {gold_val} 金购买 {selected_ware.key}",
        "goto": (_buy_item, kwargs)
    }, {
        "desc": "寻找其他东西",
        "goto": "shopfront",
    })
    return text, options

在此节点中,我们从 kwargs 中获取 selected_ware - 这是我们从上一个节点的选项中传递过来的。我们展示它的描述和价值。如果用户购买,我们通过 _buy_item 辅助函数重新路由(这不是一个节点,它只是一个调用内存的函数,必须返回要转到的下一个节点的名称)。在 _buy_item 中,我们检查买家是否能负担商品,如果可以,我们将其移动到他们的库存中。无论如何,该方法返回 shopfront 作为下一个节点。

我们一直在提到两个节点:“shopfront”和“inspect_and_buy”,我们应该将它们映射到菜单中的代码。滚动到同一模块中的 NPCMerchant 类并找到之前未完成的 open_shop 方法:

# 在 /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 "商店"
         EvMenu(shopper, menunodes, startnode="shopfront", 
                shopname=shopname, shopkeeper=self, wares=self.contents)

我们现在将节点添加到 Evmenu 中,并使用它们的正确标签。商人现在准备好了!

商店开张营业!

确保 reload

让我们通过在游戏中创建商人和一些商品来尝试一下。请记住,我们还必须创建一些金币以推动经济发展。

> set self/gold = 8

> create/drop Stan S. Stanman;stan:typeclasses.merchants.NPCMerchant
> set stan/shopname = Stan 的二手船只

> create/drop A proud vessel;ship 
> set ship/desc = 这东西有洞。
> set ship/gold_value = 5

> create/drop A classic speedster;rowboat 
> set rowboat/gold_value = 2
> set rowboat/desc = 它不会快速离开这个地方。

请注意,没有任何 Python 代码访问权限的建造者现在可以仅使用游戏内命令设置个性化商人。商店一切都设置好后,我们只需待在同一个房间即可开始消费!

> buy
*** 欢迎来到 Stan 的二手船只! ***
   可供出售的物品(选择 1-3 检查,quit 退出):
_________________________________________________________
1. 一艘骄傲的船(5 金)
2. 一艘经典极速船(2 金)

> 1 

你检查了 A proud vessel:

这东西有洞。
__________________________________________________________
1. 购买一艘骄傲的船(5 金)
2. 寻找其他东西。

> 1
你支付 5 金并购买了一艘骄傲的船!

*** 欢迎来到 Stan 的二手船只! ***
   可供出售的物品(选择 1-3 检查,quit 退出):
_________________________________________________________
1. 一艘经典极速船(2 金)