Combat Planner 開發指南

角色負責描述自己的動作與出招流程, Planner 負責選擇切入目標和執行協作路線。 內建角色示例見 src/char。 角色只從 src.combat.planner 匯入公開 API, 不操作 Planner 內部狀態。

最簡單的角色

from src.char.BaseChar import BaseChar
from src.combat.planner import Planner, RoleProfile


class ExampleChar(BaseChar):
    def describe_role(self):
        return RoleProfile(role=Planner.Role.SUB_DPS, max_field_time=1.5)

    def combat_plan(self):
        plan = self.plan()
        self.register_ultimate(plan)
        self.register_skill(plan)
        return plan

效果: 普通入場按註冊順序嘗試 Q、E。沒有成功的已註冊動作且協作允許時, Planner 可用普攻站場兜底。 max_field_time=0 關閉該兜底; 它不限制角色自己編寫的爆發迴圈時長。 切人評分取角色當前最高分的可用動作, 不累加全部動作, 也不決定入場後的動作順序。

RoleProfile 的常用設定:

欄位 效果
role 隊伍定位: Planner.Role.MAIN_DPS、SUB_DPS 或 SUPPORT, 預設 SUB_DPS。
field_preference 普通切人時的站場傾向。預設根據 role 推導; 可顯式指定 Planner.FieldPreference.SETUP_ONLY 等偏好。
max_field_time 內建普攻兜底的站場預算, 預設 1.5 秒。
combat_start_priority 大於 0 時參與開戰首切, 數值高者優先; 不影響普通切人評分。

自己控制出招順序和引數

設定 plan.entry 後, 普通入場執行這個無參方法。直接呼叫原角色方法, 保留原生引數提示和返回值:

def combat_plan(self):
    plan = self.plan()
    self.register_skill(plan)
    self.register_ultimate(plan)
    plan.entry = self.perform_entry
    return plan


def perform_entry(self):
    if self.click_skill(down_time=0.3, post_sleep=0.2):
        self.click_ultimate(send_click=False)
    if self.skill_available():
        self.click_skill()

每次呼叫都是新的嘗試, 會重新檢查註冊的執行條件和 Reserve; Q/E helper 還提供冷卻檢查。 沒有入場去重或動作次數上限, 也不會回放路線此前執行過的結果。普通 E 使用原方法的預設按鍵時長 0.05 秒; 角色可覆蓋自己的預設引數。

普通函式按 Python 語義執行: 請求不會強制讓 entry 返回, 後續註冊動作仍受 Planner 限制。 即使後續動作被阻止並返回 False, 普通賦值、等待和輔助方法也不會因此自動停止。 希望提前結束時自行寫 return。

註冊動作

register_skill(plan)、register_ultimate(plan) 和 register_arc(plan) 分別登記 click_skill、click_ultimate、click_arc, 返回值都是 None。 Q/E helper 提供槽位、預設標籤和冷卻檢查; Support 的 Q/E 還會帶 SUPPORT 標籤。 register_arc() 預設不因弧盤主動切人。是否由 Planner 接管弧盤仍由角色顯式設定 self.planner_handles_arc = True, 註冊不會修改這個開關; 未接管時預設入口仍可能額外釋放一次弧盤。

自定義或複合動作使用普通方法:

def combat_plan(self):
    plan = self.plan()
    plan.register(
        self.perform_burst,
        slot=Planner.ActionSlot.CUSTOM,
        tags={Planner.ActionTag.DAMAGE},
        reason="burst available",
    )
    plan.entry = self.perform_entry
    return plan


def perform_entry(self):
    self.perform_burst(duration=0.8)


def perform_burst(self, duration: float = 0.5) -> bool:
    self.continues_normal_attack(duration)
    return True

註冊選項同樣可傳給 Q/E/弧盤 helper, 但 helper 的 slot 已由其動作種類確定:

選項 用途
name / reason 動作名稱、日誌和切人理由; 通常無需指定 name。
tags / add_tags tags 替換預設標籤, add_tags 追加標籤。標籤用於評分和協作篩選。
can_execute 無參硬條件, 返回 False 就不執行。通過 self.combat_context 查詢協作狀態。
priority_ready 無參評分條件, 返回 False 只取消該動作的切人評分資格, 不禁止執行。
slot plan.register() 的協作槽位, 例如 Planner.ActionSlot.SKILL。

plan.register() 不根據方法名推斷 slot、tags 或冷卻條件。常用標籤有 SKILL_ACTION、 ULTIMATE_ACTION、SUPPORT、TEAM_BUFF、HIGH_PRIORITY 和 DAMAGE。 SKILL_ACTION、ULTIMATE_ACTION 已有技能輸出評分, 不必額外疊加 DAMAGE。

同一計劃內每個方法只註冊一次, 同一 slot 可以註冊多個不同方法。 例如完整 E 連招和單次 E 都可屬於 SKILL; slot 路線按註冊順序選首個可執行動作。 若二者都註冊, 連招內部的單次 E 也會檢查許可並記賬; 若只註冊連招, 整個連招作為一個動作。 直接呼叫未註冊的輔助方法仍走普通 Python 路徑, 不會自動參與評分或記賬。 首次註冊前儲存的舊方法引用也不會自動變成受管呼叫; 使用角色當前方法呼叫即可參與 Planner。

動作應返回 True、False、None 或 ActionResult。False 表示失敗, True 和 None 表示成功, ActionResult 按自身 success 判斷; 未寫 return 也算正常完成。註冊方法直接呼叫時保留其原返回值, 被阻止時返回 False。需要用返回值寫布林分支時, 請顯式返回 True/False。 內部動作失敗不會自動傳給組合函式; 需要時判斷內部結果並 return False。 引數只寫在方法呼叫處; 預設目錄和方法路線都不額外傳參, 因此所需引數應有預設值。 需要不同預設引數或許可的兩種技能形態時, 可分別寫成兩個有意義的方法註冊, 參考 Akane。

釋出隊友路線

以下程式碼放在 entry 或動作方法中, a、b 是已找到的隊友:

self.combat_context.request_route(
    [
        Planner.Route.optional(a.click_ultimate),
        Planner.Route.action(a, slot=Planner.ActionSlot.SKILL),
        Planner.Route.reaction(a, b, optional=True),
        b,
    ],
    reason="team setup",
    timeout=8,
)

這條路線依次嘗試 A 的可選 Q、指定 E、A → B 的可選反應, 再讓 B 執行一輪普通 entry。 傳角色物件表示完整一輪出招, 不表示僅切入。

步驟 效果
a.click_skill 切到 A 後按預設引數呼叫方法; 不要求註冊, 已註冊時沿用登記資訊。
Planner.Route.optional(a.click_skill) 指定方法不可用或返回 False 時跳過。
Planner.Route.action(a, slot=..., tags=..., optional=False) 從 A 的動作中按 slot/tag 選擇。至少提供一種篩選; 同時提供時兩者都要滿足, tags 內命中任意一個即可。
Planner.Route.reaction(a, b, optional=False) 從 A 觸發對 B 的反應, 等待實際確認。A 不在場時先切入 A。
a 執行 A 的完整普通 entry。

action、reaction 預設都是必需步驟。必需動作不可用時短暫普攻等待, 等待普攻不完成該步驟; 返回 False 時停在該步, 下次排程重試。可選步驟不可用或失敗時跳過整步; 不會因為首個候選執行失敗, 就在本次嘗試其他候選。route 不讀取 priority_ready。 引數不匹配、條件函式或方法拋異常等程式錯誤不會被 optional 吞掉。

方法步驟可以直接使用未註冊的組合函式, 函式內照常寫條件和迴圈, 並返回執行結果。 它僅作為路線步驟記賬, 不加入普通動作目錄或評分, 不自動安裝方法包裝。 未註冊方法沒有 slot、tags 和額外執行條件; 需要這些能力時再註冊。 其內部呼叫已註冊動作時, 仍檢查那些動作自身的執行條件和 Reserve。 方法與已註冊動作使用同一結果規則: 正常結束且未返回 False 時完成步驟, 返回 ActionResult 時按其 success 判斷。

可選反應在來源 A 實際在場後檢查資源和元素配對。不足時跳過; 確認切入 B 卻未確認反應時, 可選則跳過, 必需則重試。 死亡目標可由可選步驟跳過, 必需步驟則結束為失敗。 路線完成後繼續最後在場角色的普通 entry, 並遵守其他未完成協作請求和 Reserve。

新路線在當前 entry 返回後啟用, 不打斷髮布者的後續程式碼, 也不會被當前 entry 的動作提前完成:

self.combat_context.request_route(teammate.click_skill)
self.skill_done = True
self.click_ultimate()  # 满足自身条件和 Reserve 时正常尝试。
# 希望马上结束当前 entry 时, 在这里 return。

已生效的舊路線要等當前 entry 結束後才被新路線替換。多次釋出時後釋出的路線替換前面的路線; 等待啟用不會延長其期限。timeout 與 until=无参条件 二選一, 不指定則沒有該期限限制。 return_to_source=True 讓路線完成後優先切回釋出者, 不打斷其他角色出招。 釋出者登場即結束返場訴求, 不要求釋放技能。

保留動作和視窗

只想讓隊友保留 E 時, 釋出 Reserve。它不負責切人, 也不推進路線:

context = self.combat_context
context.reserve_actions(
    [Planner.Reserve.action(teammate, slot=Planner.ActionSlot.SKILL)],
    until=self.is_record_expire,
    reason="hold teammate E",
)

保留用於隊友, 不限制釋出者自己。它按角色物件和 slot 匹配, 限制該隊友該槽位的所有動作; 當前路線明確要求的動作可以消費保留。 until 必填, 使用無參條件或 Planner.NEVER_EXPIRES。永久保留持續到 reset, 不接受 on_finish。 多個槽位寫多個 Planner.Reserve.action(...)。

需要“路線完成後仍保留技能, 到視窗結束才釋放”時使用視窗:

timer = self.clock.timer(timeout=6)
window = self.combat_context.open_window(until=timer.expired, reason="team record window")
window.reserve_actions(
    [Planner.Reserve.action(teammate, slot=Planner.ActionSlot.ULTIMATE)]
)
route = window.request_route(
    [Planner.Route.action(teammate, slot=Planner.ActionSlot.SKILL, optional=True)]
)
self.record_window = window

子路線完成或失敗不關閉視窗; 視窗到期結束尚未完成的子路線和保留。 視窗可儲存在角色上, 在後續 entry 中繼續使用, 不必重新傳入 context。 到期後追加非空請求會報錯。需要多階段協作時, 在子路線的完成回撥中通過仍有效的視窗釋出下一條路線。

入場訴求和其他協作

在 combat_plan 中用 plan.add(FieldClaim.high(reason="burst window")) 提高角色回場意願。 low、normal、high、critical 是普通評分檔位; FieldClaim.strict(reason="return now") 要求下一次排程優先切入該角色, 但已生效的嚴格路線優先。claim 不打斷正在執行的 entry, 多個 claim 也不疊加評分。 普通 claim 可加 expected_entry=ExpectedEntry(slot=Planner.ActionSlot.ULTIMATE) 優先嚐試 Q; strict claim 切入後仍走普通 entry。

請求 用途
context.request_switch(teammate) 下一次普通排程優先切給指定角色, 不指定動作。
context.request_role(Planner.Role.SUPPORT) 選擇輔助定位的角色; 多人匹配時按普通評分選擇。
context.request_tags({Planner.ActionTag.SUPPORT}, count=2) 請求兩個不同隊友各完成一次支援類動作; 每個角色最多計一次, 預設排除釋出者。

request_role 匹配角色定位, request_tags 匹配動作標籤。tag 請求可用 avoid_source=False 包含釋出者, 可用 return_to_source 請求完成後返場。 這些請求與 route 都支援 until 和 on_finish; 它們不採用新 route 的延遲啟用規則。

隨隊伍長期生效的策略寫在無參 combat_policies() 中, Planner reset 時會呼叫它。 臨時請求放在 entry、動作或生命週期回撥中。combat_plan、評分條件、describe_role、 switch_in_guard 和到期條件只做查詢, 不釋出請求或傳送輸入。

目標角色可用無 context 引數的 guard 延遲普通切入:

def switch_in_guard(self, from_char, has_intro):
    return SwitchInGuard.delay_until_ready(
        condition=self.skill_available,
        timeout=1.5,
        reason="wait for entry state",
    )

從 src.combat.planner 匯入 FieldClaim、ExpectedEntry、SwitchInGuard 等所需型別。 guard 條件滿足或超時都會繼續切人; strict route 和 strict claim 跳過該等待。

查詢狀態與處理完成

self.combat_context 在當前隊伍執行週期內穩定可讀。reset 後應重新使用角色上的 context, 舊 context 和視窗不能再發布請求。

  • has_active_request(): 查詢協作請求, 包括已提交但等待啟用的 route; 不包含視窗本身、純保留或純切人請求。
  • has_strict_route(): 只報告已經啟用的嚴格路線。
  • strict_route_wants_action(char, slot=..., tags=...): 當前路線是否正在要求該動作。
  • is_slot_available(char, slot=...): 查詢 slot 保留限制, 不包含冷卻和 can_execute 檢查。

這些查詢讀取已提交狀態, 不觸發執行或清理, 也不是 entry 必須立即返回的指令。 請求通常返回 handle; 空路線或空保留返回 None。可訂閱無參回撥:

if route is not None:
    route.on_fulfilled(self.on_route_complete)
    route.on_failed(self.on_route_failed)
    route.on_expired(self.on_route_expired)

上述回撥是角色自己定義的方法。on_finish 響應首次完成、失敗或到期, 不重複呼叫。 可用 is_pending、is_fulfilled、is_failed、is_expired、is_closed 查詢狀態; when.fulfilled、failed、expired、closed、any 可直接作為其他請求的 until 條件。 closed 表示請求不再影響排程, 與成功、失敗、到期分開; 請求回場時可能已經 fulfilled 但尚未 closed。 需要持續視窗語義時使用 open_window, 不依靠已完成路線的到期回撥維持保留。 回撥可以繼續釋出請求; 單個回撥異常不會丟棄同批其餘通知, 處理後異常會向任務上拋。

計時與外接程式碼相容

長動作使用 self.clock.timer(timeout=...) 和 expired() 控制時長, 需要時呼叫 check_combat。 self.clock.now() 返回統一單調時間, 不使用 time.time/monotonic/perf_counter。 首次登場邏輯可用 self.is_first_engage() 或全場只消費一次的 self.consume_first_engage()。 詳見執行期時鐘約定。

舊 ActionIntent、FollowupStep、ActionReservation、click_*_action、planner_action 及帶 context 的舊 hook 仍由相容層支援, 已標記 deprecated, 僅供尚未遷移的外接程式碼使用。 舊動作宣告仍保留 None 算失敗的原語義。新角色按本文的方法註冊與直接呼叫編寫。

在 GitHub 查看來源 ↗ · 頁面產生時間: 2026年11月9日