4. 游戏内对象和物品

在上一节中,我们定义了游戏中的“角色”。在继续之前,我们还需要了解“物品”或“对象”是什么。

查看 Knave 的物品列表,我们可以得到关于需要跟踪的内容的一些想法:

  • size - 这是物品在角色库存中占用的“插槽”数量。

  • value - 如果我们想出售或购买物品的基本价值。

  • inventory_use_slot - 某些物品可以被穿戴或使用。例如,头盔需要佩戴在头上,盾牌需要在盾手。某些物品根本不能以这种方式使用,只能放在背包中。

  • obj_type - 该物品的“类型”。

4.1. 新枚举

我们在工具教程中为能力添加了一些枚举。在继续之前,让我们为使用槽和对象类型扩展枚举。

# mygame/evadventure/enums.py

class WieldLocation(Enum):
    BACKPACK = "backpack"
    WEAPON_HAND = "weapon_hand"
    SHIELD_HAND = "shield_hand"
    TWO_HANDS = "two_handed_weapons"
    BODY = "body"  # 盔甲
    HEAD = "head"  # 头盔

class ObjType(Enum):
    WEAPON = "weapon"
    ARMOR = "armor"
    SHIELD = "shield"
    HELMET = "helmet"
    CONSUMABLE = "consumable"
    GEAR = "gear"
    MAGIC = "magic"
    QUEST = "quest"
    TREASURE = "treasure"

一旦我们有了这些枚举,就可以用来引用事物。

4.2. 基本对象

创建新模块 mygame/evadventure/objects.py

我们将基于 Evennia 的标准 DefaultObject 创建一个基础 EvAdventureObject 类。然后,我们将添加子类,以表示相关的类型:

# mygame/evadventure/objects.py

from evennia import AttributeProperty, DefaultObject 
from evennia.utils.utils import make_iter
from .utils import get_obj_stats 
from .enums import WieldLocation, ObjType

class EvAdventureObject(DefaultObject): 
    """ 
    EvAdventure 对象的基础类。 
    """ 
    inventory_use_slot = WieldLocation.BACKPACK
    size = AttributeProperty(1, autocreate=False)
    value = AttributeProperty(0, autocreate=False)

    # 这可以是单一类型或多个类型的列表(对于能够充当多个角色的对象)。这用于在创建时标记该对象。
    obj_type = ObjType.GEAR

    # 默认的 Evennia hooks

    def at_object_creation(self): 
        """当该对象首次创建时调用。我们将 .obj_type 属性转换为数据库标记。"""
        
        for obj_type in make_iter(self.obj_type):
            self.tags.add(self.obj_type.value, category="obj_type")

    def get_display_header(self, looker, **kwargs):
        """描述顶部""" 
        return "" 

    def get_display_desc(self, looker, **kwargs):
        """主要展示 - 显示对象统计数据""" 
        return get_obj_stats(self, owner=looker)

    # 自定义 EvAdventure 方法

    def has_obj_type(self, objtype): 
        """检查对象是否为某种类型""" 
        return objtype.value in make_iter(self.obj_type)

    def at_pre_use(self, *args, **kwargs): 
        """使用前调用。如果返回 False,则无法使用""" 
        return True 

    def use(self, *args, **kwargs): 
        """使用此对象,无论其含义是什么""" 
        pass 

    def post_use(self, *args, **kwargs): 
        """使用后总是调用。""" 
        pass

    def get_help(self):
        """获取该物品的任何帮助文本"""
        return "该物品没有帮助说明"

4.2.1. 使用属性与否

理论上,sizevalue 不会改变,可以简单地设置为类上的常规 Python 属性:

class EvAdventureObject(DefaultObject):
    inventory_use_slot = WieldLocation.BACKPACK 
    size = 1 
    value = 0 

这样的问题是,如果我们想创建一个 size 3value 20 的新对象,我们必须为它创建一个新类。我们不能随意更改,因为改变将仅在内存中进行,并在下次服务器重新加载时丢失。

由于我们使用了 AttributeProperties,我们可以在创建对象时(或稍后)将 sizevalue 设置为我们想要的任何值,并且这些属性将永久记住我们对该对象的更改。

为了提高效率,我们使用了 autocreate=False。通常,当您使用定义的 AttributeProperties 创建新对象时,会同时立即创建一个匹配的 Attribute。因此,通常情况下,对象将与两个属性 sizevalue 一起创建。使用 autocreate=False,不会创建任何属性,除非更改了默认值。也就是说,只要你的对象的 size=1,根本不会创建数据库 Attribute。这样可以节省创建大量对象时的时间和资源。

缺点是,由于没有创建属性,因此你无法使用 obj.db.sizeobj.attributes.get("size") 引用它,除非你更改了它的默认值。你也不能查询出所有 size=1 的对象,因为大多数对象尚未在数据库中拥有 size 属性。

在我们的案例中,我们将仅将这些属性作为 obj.size 等来引用,并且不需要查找特定大小的所有对象。因此,我们认为这样是安全的。

4.2.2. at_object_creation 中创建标签

at_object_creation 是 Evennia 在每个子类化 DefaultObject 的对象首次创建时调用的方法。

我们在这里做一个巧妙的事情,将我们的 .obj_type 转换为一个或多个 标签。以这种方式标记对象意味着以后可以通过 Evennia 的搜索功能有效地查找所有给定类型(或多种类型)的对象:

from .enums import ObjType 
from evennia.utils import search 

# 获取游戏中的所有盾牌
all_shields = search.search_object_by_tag(ObjType.SHIELD.value, category="obj_type")

我们允许 .obj_type 作为单个值或多个值给出。我们使用 make_iter 来确保我们不会在任何情况下出现问题。这意味着您可以拥有一个同时也是魔法的盾牌。

4.3. 其他对象类型

某些其他对象类型现在非常简单。

# mygame/evadventure/objects.py 

from evennia import AttributeProperty, DefaultObject
from .enums import ObjType 

class EvAdventureObject(DefaultObject): 
    # ... 
    
class EvAdventureQuestObject(EvAdventureObject):
    """任务对象通常不应可以出售或交易。"""
    obj_type = ObjType.QUEST
 
class EvAdventureTreasure(EvAdventureObject):
    """宝藏通常只是为了出售以获得金币"""
    obj_type = ObjType.TREASURE
    value = AttributeProperty(100, autocreate=False)

4.4. 消耗品

“消耗品”是具有一定数量“使用次数”的物品。使用完毕后,便无法再次使用。例如,健康药水。

# mygame/evadventure/objects.py 

# ... 

class EvAdventureConsumable(EvAdventureObject): 
    """可以消耗的物品""" 
    
    obj_type = ObjType.CONSUMABLE
    value = AttributeProperty(0.25, autocreate=False)
    uses = AttributeProperty(1, autocreate=False)
    
    def at_pre_use(self, user, target=None, *args, **kwargs):
        """使用前调用。如果返回 False,则中止使用。"""
        if target and user.location != target.location:
            user.msg("您离目标太远了!")
            return False
        
        if self.uses <= 0:
            user.msg(f"|w{self.key} 已用完。|n")
            return False

    def use(self, user, *args, **kwargs):
        """使用物品时调用""" 
        pass
    
    def at_post_use(self, user, *args, **kwargs):
        """使用后调用""" 
        # 减少一次使用,若用尽则删除物品。
        self.uses -= 1
        if self.uses <= 0: 
            user.msg(f"{self.key} 已用完。")
            self.delete()

at_pre_use 中,我们检查是否指定了目标(治疗某人或向敌人投掷火弹?),确保我们在同一位置。我们还确保我们还有 uses 剩余。在 at_post_use 中,我们确保勾选使用次数。

每个消耗品的具体作用会有所不同——我们稍后将需要实现该类的子类,重写 at_use 方法以实现不同效果。

4.5. 武器

所有武器需要能够描述在战斗中表现如何的属性。使用武器意味着攻击,所以我们可以让武器本身处理所有与执行攻击相关的逻辑。将攻击代码放在武器上还意味着,如果将来我们想让武器在攻击时做某些特殊的事情(例如,吸血剑在伤害敌人时会治愈攻击者),那么我们可以很容易地在相关的武器子类中添加这一点,而无需修改其他代码。

# mygame/evadventure/objects.py 

from .enums import WieldLocation, ObjType, Ability

# ... 

class EvAdventureWeapon(EvAdventureObject): 
    """所有武器的基类"""

    obj_type = ObjType.WEAPON 
    inventory_use_slot = AttributeProperty(WieldLocation.WEAPON_HAND, autocreate=False)
    quality = AttributeProperty(3, autocreate=False)
    
    attack_type = AttributeProperty(Ability.STR, autocreate=False)
    defense_type = AttributeProperty(Ability.ARMOR, autocreate=False)
    
    damage_roll = AttributeProperty("1d6", autocreate=False)

    def at_pre_use(self, user, target=None, *args, **kwargs):
        if target and user.location != target.location:
            # 我们假设武器只能在与目标同一位置使用
            user.msg("您离目标太远了!")
            return False

        if self.quality is not None and self.quality <= 0:
            user.msg(f"{self.get_display_name(user)} 已损坏,无法使用!")
            return False
        return super().at_pre_use(user, target=target, *args, **kwargs)

    def use(self, attacker, target, *args, advantage=False, disadvantage=False, **kwargs):
        """当武器被使用时,攻击对手"""

        location = attacker.location

        is_hit, quality, txt = rules.dice.opposed_saving_throw(
            attacker,
            target,
            attack_type=self.attack_type,
            defense_type=self.defense_type,
            advantage=advantage,
            disadvantage=disadvantage,
        )
        location.msg_contents(
            f"$You() $conj(attack) $you({target.key}) with {self.key}: {txt}",
            from_obj=attacker,
            mapping={target.key: target},
        )
        if is_hit:
            # 敌人被击中,计算伤害
            dmg = rules.dice.roll(self.damage_roll)

            if quality is Ability.CRITICAL_SUCCESS:
                # 对于关键成功,双倍伤害
                dmg += rules.dice.roll(self.damage_roll)
                message = (
                    f" $You() |ycritically|n $conj(hit) $you({target.key}) for |r{dmg}|n damage!"
                )
            else:
                message = f" $You() $conj(hit) $you({target.key}) for |r{dmg}|n damage!"

            location.msg_contents(message, from_obj=attacker, mapping={target.key: target})
            # 调用钩子
            target.at_damage(dmg, attacker=attacker)

        else:
            # 未击中
            message = f" $You() $conj(miss) $you({target.key})。"
            if quality is Ability.CRITICAL_FAILURE:
                message += ".. 这是一个 |rcritical miss!|n,损坏了武器。"
                if self.quality is not None:
                    self.quality -= 1
                location.msg_contents(message, from_obj=attacker, mapping={target.key: target})

    def at_post_use(self, user, *args, **kwargs):
        if self.quality is not None and self.quality <= 0:
            user.msg(f"|r{self.get_display_name(user)} 折断,无法再使用!")

在 EvAdventure 中,我们假设所有武器(包括弓等)都在与目标同一位置使用。武器还有一个 quality 属性,在用户掷出关键失败时会磨损。一旦质量降至 0,武器便报废,需要修理。

quality 是我们在 Knave 中需要跟踪的东西。当在攻击中出现关键失败时,武器的质量会下降。当它达到 0 时,它将断裂。我们假设 qualityNone 意味着质量不适用(也就是说,该物品是不可破坏的),因此在检查时我们必须考虑。

攻击/防御类型跟踪我们如何使用武器解决攻击,例如 roll + STR vs ARMOR + 10

use 方法中,我们使用了之前创建的 rules 模块来执行解决攻击所需的所有掷骰。

这段代码需要一些额外的解释:

location.msg_contents(
    f"$You() $conj(attack) $you({target.key}) with {self.key}: {txt}",
    from_obj=attacker,
    mapping={target.key: target},
)

location.msg_contents 会向 location 中的所有人发送消息。因为人们通常会注意到你向某人挥剑,这样做是有道理的。然而,这条消息应该根据谁看到而看起来_不同_。

我应该看到:

你用剑攻击格伦德尔:<掷骰结果> 

其他人应该看到

贝奥武夫攻击格伦德尔:<掷骰结果>  

而格伦德尔应该看到

贝奥武夫用剑攻击你:<掷骰结果>

我们向 msg_contents 提供以下字符串:

f"$You() $conj(attack) $you({target.key}) with {self.key}: {txt}"

{...} 是我们之前使用过的常规 f-string 格式化标记。$func(...) 片段是 Evennia FuncParser 函数调用。 FuncParser 调用被作为函数执行,结果替代它们在字符串中的位置。当这个字符串被 Evennia 解析时,发生以下情况:

首先,f-string 标记被替换,因此我们得到:

"$You() $cobj(attack) $you(Grendel) with sword: \n rolled 8 on d20 ..."

接下来,调用 FuncParser 函数:

  • $You() 根据字符串发送对象的不同返回 You 或者相应的名字。它使用 from_obj= kwarg 来知道这一点。由于 msg_contents=attacker,例如在本例中这将变为 YouBeowulf

  • $you(Grendel) 查找 msg_contentsmapping= kwarg,以确定谁应该被称呼。如果将被替换为显示名称或小写的 you。我们添加了 mapping={target.key: target} - 也就是 {"Grendel": <grendel_obj>}。所以这将变为 you 或者 Grendel ,具体取决于谁看到字符串。

  • $conj(attack) 根据观看者决定动词的形式。结果将是 You attack ... 或者 Beowulf attacks(注意额外的 s)。

一些 FuncParser 调用将所有这些视角压缩成一条字符串!

4.6. 魔法

Knave 中,任何人都可以使用魔法,只要他们同时双手握住一个符文石(我们对法术书的称呼)。每个休息期间只能使用一次符文石。因此,符文石是一个同时也是“消耗品”的“魔法武器”的例子。

# mygame/evadventure/objects.py 

# ... 
class EvAdventureConsumable(EvAdventureObject): 
    # ... 

class EvAdventureWeapon(EvAdventureObject): 
    # ... 

class EvAdventureRuneStone(EvAdventureWeapon, EvAdventureConsumable): 
    """所有魔法符文石的基础类"""
    
    obj_type = (ObjType.WEAPON, ObjType.MAGIC)
    inventory_use_slot = WieldLocation.TWO_HANDS  # 使用魔法时始终双手
    quality = AttributeProperty(3, autocreate=False)

    attack_type = AttributeProperty(Ability.INT, autocreate=False)
    defense_type = AttributeProperty(Ability.DEX, autocreate=False)
    
    damage_roll = AttributeProperty("1d8", autocreate=False)

    def at_post_use(self, user, *args, **kwargs):
        """使用/施放法术后调用""" 
        self.uses -= 1 
        # 我们不在这里删除符文石,但它必须在下次休息时重置。
        
    def refresh(self):
        """刷新符文石(通常在休息后)"""
        self.uses = 1

我们将符文石混合成武器和消耗品。请注意,我们不需要再次添加 .uses,这是从 EvAdventureConsumable 父类继承的。at_pre_useuse 方法也被继承了;我们仅重写 at_post_use,因为我们不希望符文石在用尽时被删除。

我们添加了一个方便的方法 refresh - 我们应该在角色休息时调用此方法,以重新激活符文石。

符文石_的确_能做什么,将在此基础类的子类的 at_use 方法中实现。由于 Knave 中的魔法通常是相当自定义的,因此这将导致大量的自定义代码。

4.7. 盔甲

盔甲、盾牌和头盔提高角色的 ARMOR 属性。在 Knave 中,存储的是盔甲的防御值(11-20)。相反,我们将存储“盔甲加成”(1-10)。如我们所知,防御总是 bonus + 10,因此结果是相同的——这意味着我们可以将 Ability.ARMOR 作为任何其他防御能力使用,而不必担心特殊情况。

# mygame/evadventure/objects.py 

# ... 

class EvAdventureArmor(EvAdventureObject): 
    obj_type = ObjType.ARMOR
    inventory_use_slot = WieldLocation.BODY 

    armor = AttributeProperty(1, autocreate=False)
    quality = AttributeProperty(3, autocreate=False)

class EvAdventureShield(EvAdventureArmor):
    obj_type = ObjType.SHIELD
    inventory_use_slot = WieldLocation.SHIELD_HAND 

class EvAdventureHelmet(EvAdventureArmor): 
    obj_type = ObjType.HELMET
    inventory_use_slot = WieldLocation.HEAD

4.8. 你的双手

当我们没有武器时,我们将用双手战斗。

我们将在即将到来的 装备教程 lesson 中使用此类来表示当你双手“空无一物”时的状态。这样,我们就不需要添加任何特殊情况。

# mygame/evadventure/objects.py

from evennia import search_object, create_object

_BARE_HANDS = None 

# ... 

class WeaponBareHands(EvAdventureWeapon):
    obj_type = ObjType.WEAPON
    inventory_use_slot = WieldLocation.WEAPON_HAND
    attack_type = Ability.STR
    defense_type = Ability.ARMOR
    damage_roll = "1d4"
    quality = None  # 假设拳头是不可摧毁的...

def get_bare_hands(): 
    """获取空手""" 
    global _BARE_HANDS
    if not _BARE_HANDS: 
        _BARE_HANDS = search_object("Bare hands", typeclass=WeaponBareHands).first()
    if not _BARE_HANDS:
        _BARE_HANDS = create_object(WeaponBareHands, key="Bare hands")
    return _BARE_HANDS

由于每个人的空手是相同的(在我们的游戏中),我们创建了一个共享的 Bare hands 武器对象。我们通过 search_object 查找该对象(.first() 意味着即使我们意外创建了多个手,也会抓取第一个,具体信息请见 Django 查询教程)。如果找不到,则创建它。

通过使用 global Python 关键字,我们在模块级属性 _BARE_HANDS 中缓存了获取/创建空手对象。因此,这充当了一个缓存,以便不必频繁地搜索数据库。

从现在开始,其他模块可以简单地导入并运行此函数来获取空手。

4.9. 测试和额外积分

记得在前面的 工具教程 中提到的 get_obj_stats 函数吗? 由于我们当时还不知道如何在游戏中存储对象的属性,所以我们不得不使用虚拟值。

好吧,我们刚刚找到了我们需要的一切!您可以返回并更新 get_obj_stats,以正确读取传入对象的数据。

当您更改此函数时,您还必须更新相关的单元测试——因此,您现有的测试将成为测试新对象的好办法!添加更多测试,显示将不同对象类型传递给 get_obj_stats 的输出。

试一试。如果需要帮助,完成的工具示例可在 evennia/contrib/tutorials/evadventure/utils.py 中找到。