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 算失敗的原語義。新角色按本文的方法註冊與直接呼叫編寫。