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" # armor
HEAD = "head" # helmets
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):
"""
Base for all evadventure objects.
"""
inventory_use_slot = WieldLocation.BACKPACK
size = AttributeProperty(1, autocreate=False)
value = AttributeProperty(0, autocreate=False)
# this can be either a single type or a list of types (for objects able to be
# act as multiple). This is used to tag this object during creation.
obj_type = ObjType.GEAR
# default evennia hooks
def at_object_creation(self):
"""Called when this object is first created. We convert the .obj_type
property to a database tag."""
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):
"""The top of the description"""
return ""
def get_display_desc(self, looker, **kwargs):
"""The main display - show object stats"""
return get_obj_stats(self, owner=looker)
# custom evadventure methods
def has_obj_type(self, objtype):
"""Check if object is of a certain type"""
return objtype.value in make_iter(self.obj_type)
def at_pre_use(self, *args, **kwargs):
"""Called before use. If returning False, can't be used"""
return True
def use(self, *args, **kwargs):
"""Use this object, whatever that means"""
pass
def post_use(self, *args, **kwargs):
"""Always called after use."""
pass
def get_help(self):
"""Get any help text for this item"""
return "No help for this item"
4.2.1. 是否使用屬性¶
理論上,size 和 value 不會改變,_也可以_設定為常規 Python
類別上的屬性:
class EvAdventureObject(DefaultObject):
inventory_use_slot = WieldLocation.BACKPACK
size = 1
value = 0
這樣做的問題是,如果我們想建立一個size 3和value 20的新物件,我們必須為其建立一個新類別。我們無法即時更改它,因為更改只會儲存在記憶體中,並且會在下次伺服器重新載入時遺失。
因為我們使用AttributeProperties,所以我們可以在建立物件時(或以後)將size和value設定為我們喜歡的任何內容,並且屬性將無限期地記住我們對該物件的更改。
為了提高效率,我們使用autocreate=False。通常,當您建立定義了 AttributeProperties 的新物件時,會立即同時建立符合的 Attribute。因此,通常情況下,該物件將與兩個屬性 size 和 value 一起建立。對於 autocreate=False,不會建立 Attribute 除非更改預設值。也就是說,只要你的物件有size=1,就根本不會建立資料庫Attribute。建立大量物件時,這可以節省時間和資源。
缺點是,由於沒有建立 Attribute,因此您無法使用 obj.db.size 或 obj.attributes.get("size") 引用它_除非您更改其預設值_。您也無法在資料庫中查詢帶有 size=1 的所有物件,因為大多數物件還沒有資料庫內資料
size Attribute 進行搜尋。
在我們的例子中,我們只會將這些屬性稱為 obj.size 等,並且不需要查詢
所有具有特定尺寸的物體。所以我們應該是安全的。
4.3. 其他物件型別¶
到目前為止,其他一些物件型別非常簡單。
# mygame/evadventure/objects.py
from evennia import AttributeProperty, DefaultObject
from .enums import ObjType
class EvAdventureObject(DefaultObject):
# ...
class EvAdventureQuestObject(EvAdventureObject):
"""Quest objects should usually not be possible to sell or trade."""
obj_type = ObjType.QUEST
class EvAdventureTreasure(EvAdventureObject):
"""Treasure is usually just for selling for coin"""
obj_type = ObjType.TREASURE
value = AttributeProperty(100, autocreate=False)
4.4. 耗材¶
「消耗品」是具有一定次數「使用」的物品。一旦完全消耗,就無法再使用。一個例子是健康藥水。
# mygame/evadventure/objects.py
# ...
class EvAdventureConsumable(EvAdventureObject):
"""An item that can be used up"""
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):
"""Called before using. If returning False, abort use."""
if target and user.location != target.location:
user.msg("You are not close enough to the target!")
return False
if self.uses <= 0:
user.msg(f"|w{self.key} is used up.|n")
return False
def use(self, user, *args, **kwargs):
"""Called when using the item"""
pass
def at_post_use(self, user, *args, **kwargs):
"""Called after using the item"""
# detract a usage, deleting the item if used up.
self.uses -= 1
if self.uses <= 0:
user.msg(f"{self.key} was used up.")
self.delete()
在at_pre_use中,我們檢查是否指定了目標(治療其他人或向敵人投擲火焰彈?),確保我們位於同一位置。我們也確保還剩下usages。在at_post_use中,我們確保勾選用法。
每個消耗品的具體作用會有所不同 - 我們稍後需要實現此類的子級,以不同的效果覆蓋 at_use。
4.5. 武器¶
所有武器都需要描述它們在戰鬥中的效率的屬性。 「使用」武器意味著用它進行攻擊,因此我們可以讓武器本身處理有關執行攻擊的所有邏輯。在武器上新增攻擊程式碼也意味著,如果我們將來想要一種武器在攻擊時做一些特殊的事情(例如,在傷害敵人時治癒攻擊者的吸血鬼劍),我們可以輕鬆地將其新增到相關武器子類中,而無需修改其他程式碼。
# mygame/evadventure/objects.py
from .enums import WieldLocation, ObjType, Ability
# ...
class EvAdventureWeapon(EvAdventureObject):
"""Base class for all weapons"""
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:
# we assume weapons can only be used in the same location
user.msg("You are not close enough to the target!")
return False
if self.quality is not None and self.quality <= 0:
user.msg(f"{self.get_display_name(user)} is broken and can't be used!")
return False
return super().at_pre_use(user, target=target, *args, **kwargs)
def use(self, attacker, target, *args, advantage=False, disadvantage=False, **kwargs):
"""When a weapon is used, it attacks an opponent"""
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:
# enemy hit, calculate damage
dmg = rules.dice.roll(self.damage_roll)
if quality is Ability.CRITICAL_SUCCESS:
# doble damage roll for 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})
# call hook
target.at_damage(dmg, attacker=attacker)
else:
# a miss
message = f" $You() $conj(miss) $You({target.key})."
if quality is Ability.CRITICAL_FAILURE:
message += ".. it's a |rcritical miss!|n, damaging the weapon."
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)} breaks and can no longer be used!")
在EvAdventure中,我們假設所有武器(包括弓箭等)都在與目標相同的位置使用。武器還有quality attribute,如果使用者發生嚴重故障,武器就會磨損。一旦品質降到0,武器就壞了,需要修理。
quality 是我們需要在 Knave 中追蹤的內容。當攻擊嚴重失敗時,武器的品質就會下降。當它達到0時,它就會破裂。我們假設 None 的 quality 意味著品質不適用(即該物品牢不可破),因此我們在檢查時必須考慮到這一點。
攻擊/防禦型別追蹤我們如何解決使用武器的攻擊,例如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 中的每個人傳送訊息。由於人們通常會注意到您是否向某人揮舞劍,因此告訴人們這一點是有意義的。然而,這則訊息看起來應該_不同_取決於誰看到它。
我應該看到:
You attack Grendel with sword: <dice roll results>
其他人應該看到
Beowulf attacks Grendel with sword: <dice roll results>
格倫德爾應該看到
Beowulf attacks you with sword: <dice roll results>
我們向msg_contents提供以下字串:
f"$You() $conj(attack) $You({target.key}) with {self.key}: {txt}"
{...} 是普通的 f 字串格式標記,就像我們之前使用的一樣。 $func(...) 位元是 Evennnia FuncParser 函式呼叫。 FuncParser 呼叫作為函式執行,結果會取代它們在字串中的位置。當該字串被 Evennia 解析時,會發生以下情況:
首先替換 f 字串標記,這樣我們就得到了:
"$You() $cobj(attack) $you(Grendel) with sword: \n rolled 8 on d20 ..."
接下來執行 funcparser 函式:
$You()成為名稱或You,取決於字串是否傳送到該物件。它使用from_obj=kwarg 到msg_contents方法來瞭解這一點。由於msg_contents=attacker,在此範例中變為You或Beowulf。$you(Grendel)尋找mapping=kwarg 到msg_contents以確定應在此處處理的人員。如果將其替換為顯示名稱或小寫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):
"""Base for all magical rune stones"""
obj_type = (ObjType.WEAPON, ObjType.MAGIC)
inventory_use_slot = WieldLocation.TWO_HANDS # always two hands for magic
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):
"""Called after usage/spell was cast"""
self.uses -= 1
# we don't delete the rune stone here, but
# it must be reset on next rest.
def refresh(self):
"""Refresh the rune stone (normally after rest)"""
self.uses = 1
我們使符文石成為武器和消耗品的混合體。請注意,我們不必再增加 .uses,它是從 EvAdventureConsumable 父級繼承的。 at_pre_use和use方法也是繼承的;我們只涵蓋at_post_use,因為我們不希望符石在用完後被刪除。
我們新增了一些方便的方法 refresh - 我們應該在角色休息時呼叫它,以使符石再次啟動。
符文石的確切用途將在該基類的子類的 at_use 方法中實現。由於 Knave 中的魔法往往是非常自訂的,因此它會導致大量自訂程式碼是有道理的。
4.7. 盔甲¶
盔甲、盾牌和頭盔會增加角色的ARMOR屬性。在_Knave_中,儲存的是盔甲的防禦值(值11-20)。我們將儲存「護甲加值」(1-10)。我們知道,防禦總是bonus + 10,所以結果是一樣的——這意味著我們可以像任何其他防禦能力一樣使用Ability.ARMOR,而不必擔心特殊情況。
``
# mygame/evadventure/objects.py
# ...
class EvAdventureAmor(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. 你赤手空拳¶
當我們沒有武器的時候,我們就用赤手空拳去戰鬥。
我們將在接下來的裝備教學中用它來表示你手中「什麼都沒有」的情況。這樣我們就不需要為此新增任何特殊情況。
# 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 # let's assume fists are indestructible ...
def get_bare_hands():
"""Get the 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
由於每個人的空手都是相同的(在我們的遊戲中),因此我們建立每個人共享的 one Bare hands 武器物件。我們透過使用 search_object 搜尋物件來做到這一點(.first() 意味著我們抓住第一個物件,即使我們不小心建立了多隻手,請參閱 Django 查詢教學 以瞭解更多資訊)。如果我們找不到,我們就會創造它。
透過使用 global Python 關鍵字,我們將徒手物件 get/create 快取在模組級屬性 _BARE_HANDS 中。因此,這充當快取,不必不必要地搜尋資料庫。
從此以後,其他模組只需匯入並執行函式即可輕鬆上手。
4.9. 測試和額外學分¶
還記得之前實用教學中的get_obj_stats函式嗎? 我們必須使用虛擬值,因為我們還不知道如何在遊戲中的物件上儲存屬性。
好吧,我們剛剛找到了我們需要的一切!您可以返回並更新 get_obj_stats 以正確地從它接收的物件中讀取資料。
當您更改此功能時,您還必須更新相關的單元測試 - 因此您現有的測試也成為測試新物件的好方法!新增更多測試,顯示將不同物件型別提供給 get_obj_stats 的輸出。
自己嘗試一下。如果您需要協助,可以在 evennia/contrib/tutorials/evadventure/utils.py 中找到已完成的實用程式範例。