Combat Planner 開發指南

提示:角色的具體程式碼實現可在 src/char 目錄中找到,也可檢視 GitHub 或 CNB 上的程式碼目錄。

Planner 是隊伍大腦。角色只宣告一個 CombatPlan:

  • actions:planner 可見的動作目錄,用於切人評分、route/request/reservation 匹配。
  • claims:FieldClaim 入場訴求,用於表達“我現在應該被切進來”。
  • entry:普通入場後的 Python generator 動作流。未提供時預設按 actions 順序執行。

公開匯入入口固定使用:

from src.combat.planner import ActionSlot, CombatContext, ExpectedEntry, FieldClaim, Planner, RoleProfile

src.combat.planner 只匯出正式開發 API。角色程式碼不要直接匯入 planner/core.py、planner/requests.py、planner/state.py 等內部模組。

快速入口

普通角色通常只需要覆蓋 describe_role() 和 combat_plan(context):

def describe_role(self):
    return RoleProfile(
        role=Planner.Role.SUB_DPS,
        field_preference=Planner.FieldPreference.SUB_DPS,
        max_field_time=1.5,
    )

def combat_plan(self, context: CombatContext):
    return self.plan(
        self.click_ultimate_action(),
        self.click_skill_action(),
    )

複雜動作順序用同一個 plan 裡的 action 變數寫 entry flow。一個 action 在一次 entry 中只能 執行一次;有限次的額外執行使用 repeat_for_entry():

def combat_plan(self, context: CombatContext):
    skill = self.click_skill_action(reason="skill available")
    ultimate = self.click_ultimate_action(reason="ultimate available")

    def entry():
        skill_result = yield skill
        if skill_result and self.ultimate_available():
            self.sleep(0.6)

        ultimate_result = yield ultimate
        if ultimate_result:
            yield skill.repeat_for_entry()

    return self.plan(skill, ultimate, entry=entry)

yield action 會把 action 交給 planner 執行。planner 完成 reservation/can_execute 檢查、執行、記錄 result、推進 request 後,把 ActionResult 送回 generator。 bool(ActionResult) 等於 result.success,所以可以直接寫:

a = yield action_a
b = yield action_b

if a and b:
    yield action_c

if a and not b:
    yield fallback_action

CombatPlan

角色通過 self.plan(*actions, claims=None, entry=None) 建立 CombatPlan:

def combat_plan(self, context):
    setup = self.planner_action(...)
    claims = []
    if self.should_claim_field():
        claims.append(FieldClaim.high(reason="burst window"))

    return self.plan(setup, claims=claims)

規則:

  • 建立 plan 時只宣告動作和入場訴求,不要傳送輸入。
  • 不要在建立 plan 時呼叫 context.request_route()、reserve_actions() 或 request_tags();這些一次性請求應在 action execute 中釋出,或在 entry flow 收到成功 result 後釋出。
  • entry flow 釋出的請求會在下一次 yield 或流程結束時收集;收到 result 後釋出請求並直接 return 也會生效。
  • actions 是評分和協作匹配目錄;entry 是普通入場執行流程。
  • claims 可以傳多個獨立入場理由;它們不會疊加分數,planner 只取當前匹配角色的最高優先順序 claim。
  • strict route、expected entry、active request 的硬排程優先於普通 entry flow。
  • 普通 entry flow 最多執行 MAX_ACTIONS_PER_ENTRY 個動作。
  • 同一個 action 在同一次入場中只會真實執行一次。

ActionIntent

ActionIntent 表達“角色進場後可以嘗試做什麼”。不要把一次普攻、等待、連點等 內部細節拆成很多 action;這些應寫在一個 action 的 execute 內。

欄位:

  • tags: set[ActionTag]:動作意義和評分依據。
  • execute: Callable[[CombatContext], ActionResult | bool | None]:真正執行動作。
  • name: str = "":高階精確匹配和日誌名。
  • slot: ActionSlot | None = None:動作槽位。協作路線和 reservation 優先用 slot 匹配。
  • reason: str = "":planner 日誌和切人理由。
  • can_execute: Callable[[CombatContext], bool] | None:planner 層硬限制。
  • priority_ready: Callable[[CombatContext], bool] | None:只用於切人評分。

action.repeat_for_entry() 返回一個可在同一次 entry 中再次 yield 的動作副本。 它保留原 action 的執行、slot、標籤和 can_execute 限制,併為每次呼叫自動生成獨立 的 entry 去重結果。因此它適合 Q -> E -> 再尝试一次 E 這類有限 entry flow;副本通常 只在 entry flow 中 yield,不應加入 CombatPlan.actions。

每次 yield 都會計入單次 entry 的動作上限。不要在需要持續執行的長時間迴圈中 yield 它;此類迴圈應在呼叫已有動作 helper 前,先通過 context.is_action_allowed(self, action) 檢查完整 action 許可權。這樣迴圈保持由角色程式碼 控制,同時仍遵守 planner 的 can_execute 和 reservation 規則。

如果 action 設定了 slot,planner 會自動通過 context.is_slot_available(...) 檢查 reservation。開發者傳入的 can_execute 只需要表達額外機制限制。需要在 entry flow 外預查詢完整 action 時,使用 context.is_action_allowed(self, action);它同時檢查 can_execute 和 slot reservation。普通或有限 entry 動作仍直接 yield action。

execute 返回規則:

  • 返回 True:成功。
  • 返回 False / None / 沒寫 return:失敗。
  • 返回 ActionResult:使用 ActionResult.success。
  • 返回 1、"ok" 這類 truthy 值不會被當成成功。

普通角色不需要手寫 ActionResult。只有需要自定義 result name/tags/slot/reason 時才手寫。

ActionTag

ActionTag 表達動作意義和評分,不能表達某個角色專屬機制。

常用標籤:

  • ULTIMATE_ACTION:Q。
  • SKILL_ACTION:E。
  • ARC_ACTION:弧盤動作,評分為 0。
  • SUPPORT:輔助/治療/增益類動作。
  • TEAM_BUFF:為全隊提供增益的關鍵動作。僅在該增益應優先於主 DPS 終結技施放時使用。
  • HIGH_PRIORITY:顯著提高動作的切人評分。用於少數特別值得優先嚐試的動作;它不會改變角色上場後的動作執行順序。
  • FIELD_TIME:planner 內建站場動作,角色不應自己宣告。
  • LEGACY_COMBO:舊出招表動作。
  • DEFAULT_ACTION:低價值兜底入口。

切人評分不會累加同一角色所有 action;planner 只挑該角色當前最高分的 ready action 代表該角色參賽。tag 不控制普通入場流程;普通入場由 CombatPlan.entry 控制。

ActionSlot

ActionSlot 是協作匹配用的動作槽位,比 action name 更推薦。

常用槽位:

  • SKILL:E。
  • ULTIMATE:Q。
  • ARC:弧盤。
  • ENTRY_REACTION:入場/環合反應,不是按鍵 action。
  • FIELD_TIME:planner 內建站場。
  • LEGACY_COMBO:舊出招表。
  • CUSTOM:特殊動作。

協作和保留儘量寫:

FollowupStep.for_action(zero, ActionSlot.SKILL)
ActionReservation.for_action(nanally, ActionSlot.SKILL)
context.is_slot_available(self, ActionSlot.SKILL)

BaseChar Helper

開戰會話與首次登場

BaseCombatTask.begin_combat_session() 是戰鬥正式開始時的統一入口。它會建立公開的 task.combat_session, 呼叫首切決策並記錄實際首發角色; CombatPlanner 只負責決定首切 目標, 不執行輸入或管理會話。CombatSession.combat_start 是本場戰鬥進入時刻; use_ultimate 與 switch_enabled 是本場固定的戰鬥策略。

BaseChar.perform() 開始時會記錄本場第一個實際執行戰鬥邏輯的角色。角色邏輯可用:

if self.is_first_engage():
    # 本场首次实际登场的角色
    ...

if self.consume_first_engage():
    # 全场仅成功一次
    ...

is_first_engage() 在本場戰鬥內穩定; consume_first_engage() 全場僅返回一次 True。 兩者都不依賴首切耗時或時間視窗。task.combat_session 在首次讀取時會建立預設會話; 任務若需要禁止首切和後續切人, 應在呼叫 begin_combat_session() 前設定 task.combat_session.switch_enabled = False, 不要在執行期替換切人方法。

click_ultimate_action

self.click_ultimate_action(
    name=None,
    tags=None,
    add_tags=None,
    reason="ultimate action available",
    can_execute=None,
)
  • 自動設定 slot=ActionSlot.ULTIMATE。
  • 預設 tags={ActionTag.ULTIMATE_ACTION}。
  • tags 會完全指定基礎標籤;add_tags 可傳單個 tag 或 tag 集合,並會追加到它,或在未傳 tags 時追加到預設標籤。
  • 預設 name=f"{角色名}_ultimate"。
  • can_execute 預設包含 self.ultimate_available();傳入的額外條件會與之合併。
  • priority_ready 自動使用 self.ultimate_available()。
  • execute 呼叫 self.click_ultimate()。

click_skill_action

self.click_skill_action(
    name=None,
    tags=None,
    add_tags=None,
    reason="skill action available",
    down_time=0.01,
    can_execute=None,
)
  • 自動設定 slot=ActionSlot.SKILL。
  • 預設 tags={ActionTag.SKILL_ACTION}。
  • tags 會完全指定基礎標籤;add_tags 可傳單個 tag 或 tag 集合,並會追加到它,或在未傳 tags 時追加到預設標籤。
  • 預設 name=f"{角色名}_skill"。
  • can_execute 預設包含 self.skill_available();傳入的額外條件會與之合併。
  • priority_ready 自動使用 self.skill_available()。
  • execute 呼叫 self.click_skill(down_time=down_time)。

planner_action

self.planner_action(
    tags={ActionTag.SKILL_ACTION},
    execute=self.some_action,
    name=None,
    slot=None,
    reason="",
    can_execute=None,
    priority_ready=None,
)

用於建立自定義 action。長動作應在 execute 內完成。

FieldClaim

FieldClaim 表達“我應該被切進來”,不是動作。low、normal、high 和 critical 抬高普通入場評分;strict 在下一次切人決策時直接選定該角色。 角色切入後仍由 planner 從 actions、strict route/request 或 entry 中選擇動作。

def combat_plan(self, context):
    claims = []
    if self.has_burst_window():
        claims.append(
            FieldClaim.high(
                reason="burst window active",
                expected_entry=ExpectedEntry(slot=ActionSlot.ULTIMATE),
            )
        )
    return self.plan(self.click_ultimate_action(), claims=claims)

需要在限時視窗內回場時,可在 combat_plan() 中宣告 strict claim:

def combat_plan(self, context):
    ultimate = self.click_ultimate_action()
    claims = []
    if self.should_return_now():
        claims.append(
            FieldClaim.strict(
                reason="ultimate window ending",
            )
        )
    return self.plan(ultimate, claims=claims)

planner 每次切人決策都會重新讀取候選角色的 claim。已鎖定的 strict route 優先; 之後 strict claim 優先於環合反應、active request 和普通評分。多個角色同時宣告 strict claim 時,planner 用它們的普通評分及最近行動時間決定目標。strict claim 只在當前角色的動作結束後生效,不會中斷動作;切人時跳過 SwitchInGuard 和 wait_switch_cd() 等待。strict claim 只要求切入, 不設定 expected_entry。 切入后角色按自己的普通 entry 流程執行動作。

使用建議:

  • 只是 Q/E 可用,不需要 FieldClaim;action 本身會參與評分。
  • 需要“之後搶回場”時用普通 FieldClaim;必須在下一次切人決策中回場時用 FieldClaim.strict()。
  • FieldClaim.critical() 仍是普通評分檔位,不會強制切人。
  • 普通 claim 搶回場後需要優先做某動作時, 加 expected_entry。
  • 多個 FieldClaim 適合表達多個獨立機制入口;planner 不累加 claim 分,只選擇最高等級的匹配 claim。

combat_policies

combat_policies(context) 用於隨隊伍生命週期長期生效的策略。planner reset 當前隊伍 時會呼叫。適合釋出常駐 reservation,不適合釋出“本次 Q/E 成功後才出現”的臨時視窗。

def combat_policies(self, context: CombatContext):
    context.reserve_actions(
        [ActionReservation.for_action(zero, ActionSlot.SKILL)],
        reason="reserve Zero skill",
        until=Planner.NEVER_EXPIRES,
    )

協作請求

協作請求必須在 action 執行成功後釋出,或者在 combat_policies() 裡釋出長期策略。

def combat_plan(self, context):
    setup = self.click_skill_action()

    def entry():
        setup_result = yield setup
        if setup_result:
            context.request_route(
                [FollowupStep.for_action(zero, ActionSlot.SKILL, reason="Zero E")],
                reason="setup route",
            )

    return self.plan(setup, entry=entry)

常用 API:

  • context.request_route(...):固定順序協作路線。

FollowupStep.for_switch(target, wait_for_turn=True) 預設切入後等待目標正常執行完本輪:

context.request_route([
    FollowupStep.for_switch(a),
    FollowupStep.for_action(b, ActionSlot.ULTIMATE),
])

這裡 A 按自己的正常 entry flow 執行完本輪後, 才推進到 B 的終結技。 A 已在場時也會執行本輪, 不直接跳過。它不指定首動, 不要求入場反應, 也不繞過動作許可或 reservation。本輪結束沿用正常流程的結束條件和動作數上限; 無動作或全部失敗時仍可嘗試正常站場回退, 不要求某個技能成功才完成步驟。 異常中斷不會算作完成; route 過期或被替換後不會再推進舊步驟。

若只要求切入, 使用 FollowupStep.for_switch(a, wait_for_turn=False)。 該模式切入成功或目標已在場時立即完成步驟。單步 route 結束後恢復正常流程; 多步 route 會立即推進, 不等待 A 正常流程結束, 也不保證 A 執行任何動作。

兩種模式均繼承 strict route 的排程優先順序和生命週期。新 route 會替換已有 route, 因此它不是單純的高優先順序 request_switch(); 普通獨立切人訴求仍使用後者。

  • context.request_switch(...):請求下一次普通排程切給某角色。
  • context.request_role(...):請求下一次普通排程切給某個隊伍定位的角色;多個 匹配角色時按普通切人評分選擇。它不指定動作,也不打斷當前 entry flow。
  • context.reserve_actions(...):保留隊友動作。
  • context.request_tags(...):請求一定數量的 tag 動作。

request_role(Planner.Role.SUPPORT) 請求的是角色的靜態隊伍定位; request_tags({Planner.ActionTag.SUPPORT}) 請求的是任意支援類動作。前者適合 “讓任一輔助角色進場”,後者適合“讓任一隊友完成一次治療/增益動作”。

context.request_role(Planner.Role.SUPPORT, reason="need a support role")

行為摘要

  • 切人評分與普通 entry 執行分離。
  • 評分使用 actions 中最高分 ready action,再疊加 FieldClaim、request 和站場偏好分。
  • 當前角色普通入場執行由 entry 控制;未寫 entry 時按 actions 順序執行。
  • priority_ready=False 只降低切人吸引力,不是硬阻止。
  • can_execute=False 是硬阻止;被阻止的 entry action 會得到失敗 result,不會真實執行。
  • strict route、expected entry、active request 優先於普通 entry flow。
  • ActionResult.tags 不控制 entry flow。
在 GitHub 查看來源 ↗ · 頁面產生時間: 2026年9月24日