11. 回合製戰鬥

在本課程中,我們將在戰鬥基礎 的基礎上實現一個輪流執行的戰鬥系統,您可以在選單中選擇操作,如下所示:

> attack Troll
______________________________________________________________________________

 You (Perfect)  vs  Troll (Perfect)
 Your queued action: [attack] (22s until next round,
 or until all combatants have chosen their next action).
______________________________________________________________________________

 1: attack an enemy
 2: Stunt - gain a later advantage against a target
 3: Stunt - give an enemy disadvantage against yourself or an ally
 4: Use an item on yourself or an ally
 5: Use an item on an enemy
 6: Wield/swap with an item from inventory
 7: flee!
 8: hold, doing nothing

> 4
_______________________________________________________________________________

Select the item
_______________________________________________________________________________

 1: Potion of Strength
 2. Potion of Dexterity
 3. Green Apple
 4. Throwing Daggers
 back
 abort

> 1
_______________________________________________________________________________

Choose an ally to target.
_______________________________________________________________________________

 1: Yourself
 back
 abort

> 1
_______________________________________________________________________________

 You (Perfect)  vs Troll (Perfect)
 Your queued action: [use] (6s until next round,
 or until all combatants have chosen their next action).
_______________________________________________________________________________

 1: attack an enemy
 2: Stunt - gain a later advantage against a target
 3: Stunt - give an enemy disadvantage against yourself or an ally
 4: Use an item on yourself or an ally
 5: Use an item on an enemy
 6: Wield/swap with an item from inventory
 7: flee!
 8: hold, doing nothing

Troll attacks You with Claws: Roll vs armor (12):
 rolled 4 on d20 + strength(+3) vs 12 -> Fail
 Troll missed you.

You use Potion of Strength.
 Renewed strength coarses through your body!
 Potion of Strength was used up.

請注意,本文件不顯示遊戲中的顏色。另外,如果您對替代方案感興趣,請參閱上一課,其中我們透過為每個動作輸入直接指令來實現類似「抽搐」的戰鬥系統。

對於「回合製」戰鬥,我們指的是以較慢的速度「滴答作響」的戰鬥,速度足夠慢,足以讓參與者在選單中選擇他們的選項(選單並不是絕對必要的,但它也是學習如何製作選單的好方法)。他們的行動會排隊,並在回合計時器用完時執行。為了避免不必要的等待,當大家做出選擇後,我們也會進入下一輪。

回合製系統的優點是它消除了玩家的速度。你的戰鬥能力並不取決於你輸入指令的速度。對於 RPG- 重型遊戲,您還可以讓玩家有時間在戰鬥回合中做出 RP 表情,以充實動作。

使用選單的優點是您可以直接執行所有可能的操作,這使得初學者友好且易於知道您可以做什麼。這也意味著更少的寫作,這對某些玩家來說可能是一個優勢。

11.1. 一般原則

以下是回合製戰鬥處理程式的一般原理:

  • CombatHandler 的回合製版本將儲存在_目前位置_。這意味著每個地點只會發生一場戰鬥。任何其他開始戰鬥的人都會加入同一個處理者並被分配到一邊戰鬥。

  • 處理程式將執行 30 秒的中央計時器(在本例中)。當它觸發時,所有排隊的操作都將被執行。如果每個人都提交了他們的操作,那麼這將在最後一個提交時立即發生。

  • 在戰鬥中你將無法走動——你被困在房間裡。逃離戰鬥是一個單獨的動作,需要幾個回合才能完成(我們需要建立這個)。

  • 開始戰鬥是透過attack <target>指令完成的。之後,您將進入戰鬥選單,並將使用該選單執行所有後續操作。

11.2. 回合製戰鬥處理程式

建立一個新模組evadventure/combat_turnbased.py

# in evadventure/combat_turnbased.py

from .combat_base import (
   CombatActionAttack,
   CombatActionHold,
   CombatActionStunt,
   CombatActionUseItem,
   CombatActionWield,
   EvAdventureCombatBaseHandler,
)

from .combat_base import EvAdventureCombatBaseHandler

class EvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):

    action_classes = {
        "hold": CombatActionHold,
        "attack": CombatActionAttack,
        "stunt": CombatActionStunt,
        "use": CombatActionUseItem,
        "wield": CombatActionWield,
        "flee": None # we will add this soon!
    }

    # fallback action if not selecting anything
    fallback_action_dict = AttributeProperty({"key": "hold"}, autocreate=False)

	# track which turn we are on
    turn = AttributeProperty(0)
    # who is involved in combat, and their queued action
    # as {combatant: actiondict, ...}
    combatants = AttributeProperty(dict)

    # who has advantage against whom. This is a structure
    # like {"combatant": {enemy1: True, enemy2: True}}
    advantage_matrix = AttributeProperty(defaultdict(dict))
    # same for disadvantages
    disadvantage_matrix = AttributeProperty(defaultdict(dict))

    # how many turns you must be fleeing before escaping
    flee_timeout = AttributeProperty(1, autocreate=False)

	# track who is fleeing as {combatant: turn_they_started_fleeing}
    fleeing_combatants = AttributeProperty(dict)

    # list of who has been defeated so far
    defeated_combatants = AttributeProperty(list)

我們為 "flee" 操作留下一個佔位符,因為我們還沒有建立它。

由於回合製戰鬥處理程式在所有戰鬥人員之間共享,因此我們需要在處理程式上儲存對這些戰鬥人員的引用,在 combatants Attribute 中。 以同樣的方式,我們必須儲存一個誰對誰有優勢/劣勢的_矩陣_。我們還必須追蹤誰在逃跑,特別是他們逃跑了多長時間,因為在那之後他們就會離開戰鬥。

11.2.1. 取得戰鬥雙方

雙方的差異取決於我們是否在PvP房間中:在PvP房間中,其他人都是你的敵人。否則,戰鬥中只有 NPCs 是你的敵人(假設你與其他玩家組隊)。

# in evadventure/combat_turnbased.py

# ...

class EvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):

	# ...

    def get_sides(self, combatant):
           """
           Get a listing of the two 'sides' of this combat,
           from the perspective of the provided combatant.
           """
           if self.obj.allow_pvp:
               # in pvp, everyone else is an ememy
               allies = [combatant]
               enemies = [comb for comb in self.combatants if comb != combatant]
           else:
               # otherwise, enemies/allies depend on who combatant is
               pcs = [comb for comb in self.combatants if inherits_from(comb, EvAdventureCharacter)]
               npcs = [comb for comb in self.combatants if comb not in pcs]
               if combatant in pcs:
                   # combatant is a PC, so NPCs are all enemies
                   allies = pcs
                   enemies = npcs
               else:
                   # combatant is an NPC, so PCs are all enemies
                   allies = npcs
                   enemies = pcs
        return allies, enemies

請注意,由於 EvadventureCombatBaseHandler(我們的回合處理程式所基於的)是 Script,因此它提供了許多有用的功能。例如,self.obj 是 Script “所在”的實體。由於我們計劃將此處理程式放在目前位置,因此 self.obj 將是該房間。

我們在這裡所做的就是檢查它是否是 PvP 房間,並用它來確定誰是盟友還是敵人。請注意,combatant _不_包含在 allies 回傳值中 - 我們需要記住這一點。

11.2.2. 追蹤優勢/劣勢

# in evadventure/combat_turnbased.py

# ...

class EvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):

	# ...

    def give_advantage(self, combatant, target):
        self.advantage_matrix[combatant][target] = True

    def give_disadvantage(self, combatant, target, **kwargs):
        self.disadvantage_matrix[combatant][target] = True

    def has_advantage(self, combatant, target, **kwargs):
        return (
	        target in self.fleeing_combatants
	        or bool(self.advantage_matrix[combatant].pop(target, False))
        )
    def has_disadvantage(self, combatant, target):
        return bool(self.disadvantage_matrix[combatant].pop(target, False))

我們使用 advantage/disadvantage_matrix 屬性來追蹤誰對誰有優勢。

has dis/advantage 方法中,我們從矩陣中提取 pop 目標,這將導致值 TrueFalse(如果目標不在矩陣中,我們給出的預設值為 pop)。這意味著優勢一旦獲得,就只能使用一次。

我們也認為每個人在對抗逃跑的戰鬥人員時都具有優勢。

11.2.3. 新增和刪除戰鬥人員

由於戰鬥處理程式是共享的,我們必須能夠輕鬆新增和刪除戰鬥人員。 與基本處理程式相比,這是新的。

# in evadventure/combat_turnbased.py

# ...

class EvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):

    # ...

    def add_combatant(self, combatant):
        """
        Add a new combatant to the battle. Can be called multiple times safely.
        """
        if combatant not in self.combatants:
            self.combatants[combatant] = self.fallback_action_dict
            return True
        return False

    def remove_combatant(self, combatant):
        """
        Remove a combatant from the battle.
        """
        self.combatants.pop(combatant, None)
        # clean up menu if it exists
		# TODO!

我們只需新增帶有後備動作字典的戰鬥人員即可。我們從add_combatant返回bool,以便呼叫函式知道它們是否實際上是重新新增的(如果它們是新的,我們可能需要做一些額外的設定)。

現在我們只是 pop 戰鬥人員,但將來我們需要在戰鬥結束時對選單進行一些額外的清理(我們會做到這一點)。

11.2.4. 逃跑行動

由於你不能只是離開房間來逃離回合製戰鬥,我們需要新增一個新的 CombatAction 子類,就像我們在 基礎戰鬥課程 中建立的子類一樣。

# in evadventure/combat_turnbased.py

from .combat_base import CombatAction

# ...

class CombatActionFlee(CombatAction):
    """
    Start (or continue) fleeing/disengaging from combat.

    action_dict = {
           "key": "flee",
        }
    """

    def execute(self):
        combathandler = self.combathandler

        if self.combatant not in combathandler.fleeing_combatants:
            # we record the turn on which we started fleeing
            combathandler.fleeing_combatants[self.combatant] = self.combathandler.turn

        # show how many turns until successful flight
        current_turn = combathandler.turn
        started_fleeing = combathandler.fleeing_combatants[self.combatant]
        flee_timeout = combathandler.flee_timeout
        time_left = flee_timeout - (current_turn - started_fleeing) - 1

        if time_left > 0:
            self.msg(
                "$You() $conj(retreat), being exposed to attack while doing so (will escape in "
                f"{time_left} $pluralize(turn, {time_left}))."
            )


class EvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):

	action_classes = {
        "hold": CombatActionHold,
        "attack": CombatActionAttack,
        "stunt": CombatActionStunt,
        "use": CombatActionUseItem,
        "wield": CombatActionWield,
        "flee": CombatActionFlee # < ---- added!
    }

	# ...

我們建立動作來利用我們在戰鬥處理程式中設定的 fleeing_combatants 字典。該指令儲存了逃跑的戰鬥人員及其逃跑開始的turn。如果多次執行 flee 操作,我們將只顯示剩餘的回合數。

最後,我們確保將新的 CombatActionFlee 新增至戰鬥處理程式的 action_classes 登錄檔中。

11.2.5. 佇列動作

# in evadventure/combat_turnbased.py

# ...

class EvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):

    # ...

    def queue_action(self, combatant, action_dict):
        self.combatants[combatant] = action_dict

        # track who inserted actions this turn (non-persistent)
        did_action = set(self.ndb.did_action or set())
        did_action.add(combatant)
        if len(did_action) >= len(self.combatants):
            # everyone has inserted an action. Start next turn without waiting!
            self.force_repeat()

為了對一個動作進行排隊,我們只需將其 action_dict 與戰鬥者一起存放在 combatants Attribute 中。

我們使用 Python set() 來追蹤本回合誰已將操作排隊。如果本回合所有戰鬥人員都輸入了新的(或更新的)動作,我們將使用 .force_repeat() 方法,該方法適用於所有 Scripts。當呼叫此函式時,下一輪將立即觸發,而不是等到逾時。

11.2.6. 執行一個動作並勾選該回合

 1# in evadventure/combat_turnbased.py
 2
 3import random
 4
 5# ...
 6
 7class EvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
 8
 9    # ...
10
11    def execute_next_action(self, combatant):
12        # this gets the next dict and rotates the queue
13        action_dict = self.combatants.get(combatant, self.fallback_action_dict)
14
15        # use the action-dict to select and create an action from an action class
16        action_class = self.action_classes[action_dict["key"]]
17        action = action_class(self, combatant, action_dict)
18
19        action.execute()
20        action.post_execute()
21
22        if action_dict.get("repeat", False):
23            # queue the action again *without updating the
24            # *.ndb.did_action list* (otherwise
25            # we'd always auto-end the turn if everyone used
26            # repeating actions and there'd be
27            # no time to change it before the next round)
28            self.combatants[combatant] = action_dict
29        else:
30            # if not a repeat, set the fallback action
31            self.combatants[combatant] = self.fallback_action_dict
32
33
34   def at_repeat(self):
35        """
36        This method is called every time Script repeats
37        (every `interval` seconds). Performs a full turn of
38        combat, performing everyone's actions in random order.
39        """
40        self.turn += 1
41        # random turn order
42        combatants = list(self.combatants.keys())
43        random.shuffle(combatants)  # shuffles in place
44
45        # do everyone's next queued combat action
46        for combatant in combatants:
47            self.execute_next_action(combatant)
48
49        self.ndb.did_action = set()
50
51        # check if one side won the battle
52        self.check_stop_combat()

我們的操作執行由兩部分組成 - execute_next_action(在父類中定義供我們實現)和 at_repeat 方法,該方法是 Script 的一部分

對於execute_next_action

  • 第 13 行:我們從 combatants Attribute 得到 action_dict。如果沒有任何內容排隊,我們將返回 fallback_action_dict(預設為 hold)。

  • 第 16 行:我們使用 action_dictkey(類似「攻擊」、「使用」、「揮舞」等)從 action_classes 字典中取得符合 Action 的類別。

  • 第 17 行:這裡使用戰鬥人員和動作字典例項化動作類,使其準備好執行。然後在以下幾行執行此操作。

  • 第 22 行:我們在這裡引入一個新的可選 action-dict,即布林值 repeat 鍵。這允許我們重新排隊操作。如果不是,將使用後備操作。

Script 觸發後,每 interval 秒重複呼叫 at_repeat。這是我們用來追蹤每輪結束時間的方法。

  • 第 43 行:在此範例中,我們的操作之間沒有內部順序。所以我們只是隨機化它們的發射順序。

  • 第 49 行:這個 set 被分配給 queue_action 方法,以瞭解每個人何時提交新作業。我們必須確保在下一輪之前在這裡取消設定。

11.2.7. 檢查並停止戰鬥

 1# in evadventure/combat_turnbased.py
 2
 3import random
 4from evennia.utils.utils import list_to_string
 5
 6# ...
 7
 8class EvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):
 9
10    # ...
11
12     def stop_combat(self):
13        """
14        Stop the combat immediately.
15
16        """
17        for combatant in self.combatants:
18            self.remove_combatant(combatant)
19        self.stop()
20        self.delete()
21
22    def check_stop_combat(self):
23        """Check if it's time to stop combat"""
24
25        # check if anyone is defeated
26        for combatant in list(self.combatants.keys()):
27            if combatant.hp <= 0:
28                # PCs roll on the death table here, NPCs die.
29                # Even if PCs survive, they
30                # are still out of the fight.
31                combatant.at_defeat()
32                self.combatants.pop(combatant)
33                self.defeated_combatants.append(combatant)
34                self.msg("|r$You() $conj(fall) to the ground, defeated.|n", combatant=combatant)
35            else:
36                self.combatants[combatant] = self.fallback_action_dict
37
38        # check if anyone managed to flee
39        flee_timeout = self.flee_timeout
40        for combatant, started_fleeing in self.fleeing_combatants.items():
41            if self.turn - started_fleeing >= flee_timeout - 1:
42                # if they are still alive/fleeing and have been fleeing long enough, escape
43                self.msg("|y$You() successfully $conj(flee) from combat.|n", combatant=combatant)
44                self.remove_combatant(combatant)
45
46        # check if one side won the battle
47        if not self.combatants:
48            # noone left in combat - maybe they killed each other or all fled
49            surviving_combatant = None
50            allies, enemies = (), ()
51        else:
52            # grab a random survivor and check if they have any living enemies.
53            surviving_combatant = random.choice(list(self.combatants.keys()))
54            allies, enemies = self.get_sides(surviving_combatant)
55
56        if not enemies:
57            # if one way or another, there are no more enemies to fight
58            still_standing = list_to_string(f"$You({comb.key})" for comb in allies)
59            knocked_out = list_to_string(comb for comb in self.defeated_combatants if comb.hp > 0)
60            killed = list_to_string(comb for comb in self.defeated_combatants if comb.hp <= 0)
61
62            if still_standing:
63                txt = [f"The combat is over. {still_standing} are still standing."]
64            else:
65                txt = ["The combat is over. No-one stands as the victor."]
66            if knocked_out:
67                txt.append(f"{knocked_out} were taken down, but will live.")
68            if killed:
69                txt.append(f"{killed} were killed.")
70            self.msg(txt)
71            self.stop_combat()

check_stop_combat 在回合結束時被呼叫。我們想弄清楚誰死了以及“一方”是否獲勝。

  • 第28-38行:我們檢查所有戰鬥人員並確定他們是否在HP之外。如果是這樣,我們觸發相關的鉤子並將它們新增到 defeated_combatants Attribute 中。

  • 第 38 行:對於所有倖存的戰鬥人員,我們確保給他們fallback_action_dict

  • 第 41-46 行fleeing_combatant Attribute 是 {fleeing_combatant: turn_number} 形式的字典,追蹤他們第一次開始逃跑的時間。我們將其與當前回合數和 flee_timeout 進行比較,看看他們是否現在逃跑並應該被允許從戰鬥中移除。

  • 第 49-56 行:這裡我們正在確定衝突的一方是否擊敗了另一方。

  • 第 60 行list_to_string Evennia 實用程式將條目清單(例如 ["a", "b", "c")轉換為漂亮的字串 "a, b and c"。我們用它來向戰鬥人員呈現一些美好的結局訊息。

11.2.8. 開始戰鬥

由於我們使用 Script 的計時器元件來計時我們的戰鬥,因此我們還需要一個輔助方法來「啟動」它。

from evennia.utils.utils import list_to_string

# in evadventure/combat_turnbased.py

# ...

class EvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):

    # ...

    def start_combat(self, **kwargs):
        """
        This actually starts the combat. It's safe to run this multiple times
        since it will only start combat if it isn't already running.

        """
        if not self.is_active:
            self.start(**kwargs)

start(**kwargs) 方法是 Script 上的方法,並且將使其開始每 interval 秒呼叫 at_repeat。我們將在 kwargs 內傳遞 interval(例如,我們稍後將傳遞 combathandler.start_combat(interval=30))。

11.3. 使用EvMenu作為戰鬥選單

EvMenu 用於在 Evennia 中建立遊戲內選單。我們在角色生成課程中已經使用了一個簡單的EvMenu。這次我們需要更先進一點。 雖然EvMenu 檔案 更詳細地描述了其功能,但我們將在此快速概述其工作原理。

EvMenu 由 nodes 組成,它們是此表單上的常規函式(這裡有些簡化,有更多選項):

def node_somenodename(caller, raw_string, **kwargs):

    text = "some text to show in the node"
    options = [
        {
           "key": "Option 1", # skip this to get a number
           "desc": "Describing what happens when choosing this option."
           "goto": "name of the node to go to"  # OR (callable, {kwargs}}) returning said name
        },
        # other options here
    ]
    return text, options

因此,基本上每個節點都採用 caller(使用選單的節點)、raw_string(空字串或使用者在前一個節點上輸入的內容)和 **kwargs 引數,可用於在節點之間傳遞資料。它返回 textoptions

text是使用者進入這部分選單時會看到的內容,例如「選擇你想要攻擊的人!」。 options 是描述每個選項的字典清單。它們將顯示為節點文字下方的多項選擇清單(請參閱本課程頁面頂部的範例)。

當我們稍後建立 EvMenu 時,我們將建立一個_節點索引_ - 唯一名稱和這些「節點函式」之間的對應。所以像這樣:

# example of a EvMenu node index
    {
      "start": node_combat_main,
      "node1": node_func1,
      "node2": node_func2,
      "some name": node_somenodename,
      "end": node_abort_menu,
    }

每個 option 字典都有一個鍵 "goto" ,用於確定玩家選擇該選項時應跳到哪個節點。在選單內部,每個節點都需要用這些名稱來引用(如 "start""node1" 等)。

每個選項的 "goto" 值可以直接指定名稱(如 "node1")_或_它可以作為元組 (callable, {keywords}) 給出。這個 callable 被稱為_並且預計會傳回下一個要使用的節點名稱(如 "node1")。

callable(通常稱為「goto callable」)看起來與節點函式非常相似:

def _goto_when_choosing_option1(caller, raw_string, **kwargs):
    # do whatever is needed to determine the next node
    return nodename  # also nodename, dict works

這裡,caller 仍然是使用選單的字串,raw_string 是您輸入的用於選擇此選項的實際字串。 **kwargs 是您新增至 (callable, {keywords}) 元組中的關鍵字。

goto-callable 必須傳回下一個節點的名稱。或者,您可以同時返回 nodename, {kwargs}。如果您這樣做,下一個節點將獲得這些 kwargs 作為傳入 **kwargs。透過這種方式,您可以將資訊從一個節點傳遞到下一個節點。一個特殊的功能是,如果 nodename 回傳為 None,則 current 節點將再次_rerun_。

這是一個(有點做作的)範例,說明瞭 goto-callable 和 node-function 如何結合在一起:

# goto-callable
def _my_goto_callable(caller, raw_string, **kwargs):
    info_number = kwargs["info_number"]
    if info_number > 0:
        return "node1"
    else:
        return "node2", {"info_number": info_number}  # will be **kwargs when "node2" runs next


# node function
def node_somenodename(caller, raw_string, **kwargs):
    text = "Some node text"
    options = [
        {
            "desc": "Option one",
            "goto": (_my_goto_callable, {"info_number", 1})
        },
        {
            "desc": "Option two",
            "goto": (_my_goto_callable, {"info_number", -1})
        },
    ]

11.5. 攻擊指令

我們只需要一個指令來執行回合製戰鬥系統。這是attack 指令。一旦你使用它一次,你就會進入選單。

# in evadventure/combat_turnbased.py

from evennia import Command, CmdSet, EvMenu

# ...

class CmdTurnAttack(Command):
    """
    Start or join combat.

    Usage:
      attack [<target>]

    """

    key = "attack"
    aliases = ["hit", "turnbased combat"]

    turn_timeout = 30  # seconds
    flee_time = 3  # rounds

    def parse(self):
        super().parse()
        self.args = self.args.strip()

    def func(self):
        if not self.args:
            self.msg("What are you attacking?")
            return

        target = self.caller.search(self.args)
        if not target:
            return

        if not hasattr(target, "hp"):
            self.msg("You can't attack that.")
            return

        elif target.hp <= 0:
            self.msg(f"{target.get_display_name(self.caller)} is already down.")
            return

        if target.is_pc and not target.location.allow_pvp:
            self.msg("PvP combat is not allowed here!")
            return

        combathandler = _get_combathandler(
            self.caller, self.turn_timeout, self.flee_time)

        # add combatants to combathandler. this can be done safely over and over
        combathandler.add_combatant(self.caller)
        combathandler.queue_action(self.caller, {"key": "attack", "target": target})
        combathandler.add_combatant(target)
        target.msg("|rYou are attacked by {self.caller.get_display_name(self.caller)}!|n")
        combathandler.start_combat()

        # build and start the menu
        EvMenu(
            self.caller,
            {
                "node_choose_enemy_target": node_choose_enemy_target,
                "node_choose_allied_target": node_choose_allied_target,
                "node_choose_enemy_recipient": node_choose_enemy_recipient,
                "node_choose_allied_recipient": node_choose_allied_recipient,
                "node_choose_ability": node_choose_ability,
                "node_choose_use_item": node_choose_use_item,
                "node_choose_wield_item": node_choose_wield_item,
                "node_combat": node_combat,
            },
            startnode="node_combat",
            combathandler=combathandler,
            auto_look=False,
            # cmdset_mergetype="Union",
            persistent=True,
        )


class TurnCombatCmdSet(CmdSet):
    """
    CmdSet for the turn-based combat.
    """

    def at_cmdset_creation(self):
        self.add(CmdTurnAttack())

attack target指令將確定目標是否有生命值(只有有生命值的物體才能被攻擊)以及房間是否允許戰鬥。如果目標是 PC,它會檢查是否允許 PvP。

然後,它繼續啟動一個新的指令處理程式或重複使用一個新的指令處理程式,同時向其中新增攻擊者和目標。如果目標已經處於戰鬥狀態,則不會執行任何操作(與 .start_combat() 呼叫相同)。

當我們建立 EvMenu 時,我們將其傳遞給我們之前討論過的“選單索引”,現在每個槽中都有實際的節點功能。 我們使選單持久化,以便它在重新載入後仍然存在。

要使該指令可用,請將 TurnCombatCmdSet 新增至角色的預設 cmdset 中。

11.6. 確保選單停止

戰鬥可能會因多種原因而結束。發生這種情況時,我們必須確保清理選單,以便恢復正常操作。我們將其新增到戰鬥處理程式的 remove_combatant 方法中(我們之前在那裡留下了 TODO):


# in evadventure/combat_turnbased.py

# ...

class EvadventureTurnbasedCombatHandler(EvAdventureCombatBaseHandler):

    # ...
    def remove_combatant(self, combatant):
        """
        Remove a combatant from the battle.
        """
        self.combatants.pop(combatant, None)
        # clean up menu if it exists
        if combatant.ndb._evmenu:                   # <--- new
            combatant.ndb._evmenu.close_menu()      #     ''

當 evmenu 處於活動狀態時,使用者可以使用 .ndb._evmenu(請參閱 EvMenu 檔案)。當我們退出戰鬥時,我們使用它來獲取evmenu並呼叫它的close_menu()方法來關閉選單。

我們的回合製戰鬥系統已經完成!

11.7. 測試

Turnbased 戰鬥處理程式的單元測試非常簡單,您可以按照前面課程的程式來測試處理程式上的每個方法是否返回您所期望的模擬輸入。

對選單進行單元測試更加複雜。您可以在 evennia.utils.tests.test_evmenu 中找到執行此操作的範例。

11.8. 實戰小測試

對程式碼進行單元測試不足以看出戰鬥是否有效。我們還需要進行一些「功能」測試,看看它在實踐中是如何運作的。

這是我們進行最小測試所需的:

  • 一個可以進行戰鬥的房間。

  • NPC 進行攻擊(它不會做任何反擊,因為我們還沒有增加任何 AI)

  • 我們可以wield的武器。

  • 一個物品(例如藥水)我們可以use

Twitch實戰課中,我們使用了批次指令script來創造遊戲中的測試環境。這將按順序執行遊戲中的 Evennia 指令。出於演示目的,我們將使用 batch-code script,它以可重複的方式執行原始 Python 程式碼。批次程式碼 script 比批次指令 script 靈活得多。

建立一個新的子資料夾 evadventure/batchscripts/(如果它尚不存在)

建立一個新的Python模組evadventure/batchscripts/combat_demo.py

批次程式碼檔案是有效的 Python 模組。唯一的區別是它有一個 # HEADER 區塊和一個或多個 # CODE 部分。當處理器執行時,在單獨執行該程式碼區塊之前,# HEADER 部分將新增到每個 # CODE 部分的頂部。由於您可以在遊戲中執行該檔案(包括在不重新載入伺服器的情況下重新整理它),因此可以按需執行更長的 Python 程式碼。

# Evadventure (Turnbased) combat demo - using a batch-code file.
#
# Sets up a combat area for testing turnbased combat.
#
# First add mygame/server/conf/settings.py:
#
#    BASE_BATCHPROCESS_PATHS += ["evadventure.batchscripts"]
#
# Run from in-game as `batchcode turnbased_combat_demo`
#

# HEADER

from evennia import DefaultExit, create_object, search_object
from evennia.contrib.tutorials.evadventure.characters import EvAdventureCharacter
from evennia.contrib.tutorials.evadventure.combat_turnbased import TurnCombatCmdSet
from evennia.contrib.tutorials.evadventure.npcs import EvAdventureNPC
from evennia.contrib.tutorials.evadventure.rooms import EvAdventureRoom

# CODE

# Make the player an EvAdventureCharacter
player = caller  # caller is injected by the batchcode runner, it's the one running this script # E: undefined name 'caller'
player.swap_typeclass(EvAdventureCharacter)

# add the Turnbased cmdset
player.cmdset.add(TurnCombatCmdSet, persistent=True)

# create a weapon and an item to use
create_object(
    "contrib.tutorials.evadventure.objects.EvAdventureWeapon",
    key="Sword",
    location=player,
    attributes=[("desc", "A sword.")],
)

create_object(
    "contrib.tutorials.evadventure.objects.EvAdventureConsumable",
    key="Potion",
    location=player,
    attributes=[("desc", "A potion.")],
)

# start from limbo
limbo = search_object("#2")[0]

arena = create_object(EvAdventureRoom, key="Arena", attributes=[("desc", "A large arena.")])

# Create the exits
arena_exit = create_object(DefaultExit, key="Arena", location=limbo, destination=arena)
back_exit = create_object(DefaultExit, key="Back", location=arena, destination=limbo)

# create the NPC dummy
create_object(
    EvAdventureNPC,
    key="Dummy",
    location=arena,
    attributes=[("desc", "A training dummy."), ("hp", 1000), ("hp_max", 1000)],
)

如果在 IDE 中編輯此內容,您可能會在 player = caller 行上收到錯誤。這是因為 caller 未在此檔案中的任何位置定義。相反,caller(執行script的那個)由batchcode執行器注入。

但除了 # HEADER# CODE 特殊之外,這只是一系列正常的 Evennia api 呼叫。

使用開發者/超級使用者帳戶登入遊戲並執行

> batchcode evadventure.batchscripts.turnbased_combat_demo

這應該會將您置於與虛擬物件一起的競技場中(如果沒有,請檢查輸出中是否有錯誤!如果需要重新開始,請使用 objectsdelete 指令列出並刪除物件。)

現在您可以嘗試attack dummy,並且應該能夠猛擊假人(降低其生命值以測試摧毀它)。如果您需要修復某些內容,請使用 q 退出選單並存取 reload 指令(對於最終戰鬥,您可以在建立 EvMenu 時透過傳遞 auto_quit=False 來停用此功能)。

11.9. 結論

至此,我們已經介紹了一些關於如何實現基於抽搐和回合的戰鬥系統的想法。在這個過程中,您已經接觸到了許多概念,例如類別、scripts 和處理程式、指令、EvMenus 等等。

在我們的戰鬥系統真正可用之前,我們需要敵人真正反擊。我們接下來會討論這個問題。