Skip to content

feat: 新增链式战斗系统与相关角色实现#99

Draft
Wattls wants to merge 18 commits into
BnanZ0:mainfrom
Wattls:dev-sound-trig
Draft

feat: 新增链式战斗系统与相关角色实现#99
Wattls wants to merge 18 commits into
BnanZ0:mainfrom
Wattls:dev-sound-trig

Conversation

@Wattls
Copy link
Copy Markdown
Contributor

@Wattls Wattls commented May 22, 2026

  1. 新增ChainExecutor实现链式战斗流程管理
  2. 为多个角色添加链式战斗子类:HotoriChain、ZeroChain、JiuyuanChain、NanallyChain
  3. 新增CharFactory链式角色注册配置
  4. 为BaseChar添加链式执行支持与辅助方法
  5. 为DodgeCounterTrigger添加闪避抑制功能
  6. 扩展BaseCombatTask以支持链式战斗流程切换

Summary by CodeRabbit

  • 新功能
    • 新增角色链式连招系统,支持多角色按序执行、自动推进与锚点切换
    • 新增四位链式角色配置以支持多种连段组合(含 Hotori/Jiuyuan/Nanally/Zero)
    • 引入链式执行管理器以启动、推进、重置链路并跨角色传递按键
    • 新增团队技能窗口与记录以协同终极释放时机
    • 战斗流程调整:链执行期间优先切换链目标并在每回合重置执行器
    • 新增闪避抑制功能,可在链路期间临时禁用闪避触发

Review Change Stack

1.  新增ChainExecutor实现链式战斗流程管理
2.  为多个角色添加链式战斗子类:HotoriChain、ZeroChain、JiuyuanChain、NanallyChain
3.  新增CharFactory链式角色注册配置
4.  为BaseChar添加链式执行支持与辅助方法
5.  为DodgeCounterTrigger添加闪避抑制功能
6.  扩展BaseCombatTask以支持链式战斗流程切换
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b07837fe-0fe8-41a6-bbe4-428a379c58f9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

本PR新增链式执行器 ChainExecutor,扩展 BaseChar 协议并集成到 BaseCombatTask,增加闪避抑制,新增并注册 HotoriChain、JiuyuanChain、NanallyChain、ZeroChain 四个链式角色实现。

Changes

角色链式执行系统实现

Layer / File(s) Summary
ChainExecutor 执行引擎
src/combat/ChainExecutor.py
新增 ChainExecutor 类,管理链式步骤、目标计算、loop() 启动链、step_complete() 推进并记录节奏与状态。
BaseChar 链式执行协议
src/char/BaseChar.py
perform() 中优先执行 _chain_method(存在则调用对应方法;否则调用 task.chain_executor.step_complete() 并返回),新增 is_anchor()_get_char_key()_send_chain_key()on_chain_step_complete() 协议方法。
BaseCombatTask 系统整合
src/combat/BaseCombatTask.py
在构造中初始化 self.chain_executorcombat_once() 启动时重置执行器;修改 switch_next_char() 以优先使用链式目标或 _pending_anchor,新增 switch_to_char()suppress_dodge()/unsuppress_dodge() 接口。
闪避抑制功能
src/sound_trigger/DodgeCounterTrigger.py
新增 _dodge_suppressed 状态与 dodge_suppressed 属性;execute_dodge() 在抑制时提前返回,跳过闪避逻辑。
链式角色注册
src/char/CharFactory.py
导入并在 char_dict 中注册 HotoriChainJiuyuanChainNanallyChainZeroChain 的映射条目。
ZeroChain - 基础链段实现
src/char/ZeroChain.py
新增 ZeroChain,包含 do_perform()_do_perform_legacy()chain_q_e_wait()chain_nop()chain_e_only(),实现基础链段模式与超时处理。
Hotori 核心计时与钩子
src/char/Hotori.py
Hotori 新增 E/Q 施放时间戳及剩余恢复计算、time_to_cashout/startup、reset/waiting_for_team_skills 与 on_chain_step_complete 钩子。
HotoriChain - 锚点与团队协调
src/char/HotoriChain.py
新增 HotoriChain,定义 STARTUP_CHAIN/WARMUP_CHAINdo_perform()chain_e_start_chain()chain_q_na(),并实现团队技能窗口/记录的采集、过期与判定逻辑,完成步后设置 pending_anchor。
JiuyuanChain - 终极与重击流程
src/char/JiuyuanChain.py
新增 JiuyuanChain,包含 do_perform()chain_intro_only()chain_q_e_heavy(),实现终极/重击时序处理并与链式系统交互。
NanallyChain - 动态待机与切换时序
src/char/NanallyChain.py
新增 NanallyChain,包含 do_perform()chain_dynamic_standby(),实现基于冷却的 E→Q/终结节奏、切换到 HotoriChain 的按键回切与交接逻辑。

Sequence Diagram(s)

sequenceDiagram
  participant Task
  participant ChainExecutor
  participant Char
  participant Team
  Task->>ChainExecutor: loop(builder)
  ChainExecutor->>ChainExecutor: _start(steps)
  ChainExecutor->>Char: set _chain_method for target
  Char->>Char: perform() executes _chain_method
  Char->>Team: update_team_skill_records (when applicable)
  Char->>ChainExecutor: step_complete()
  ChainExecutor->>ChainExecutor: advance index / build next or stop
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🐰 链式舞步在战场编织,
四位英雄心意相济,
E键锁定,终极绽放,
团队之力,节奏相随,
小兔欢跃,代码成诗。

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题清晰概括了本PR的主要变更:新增链式战斗系统和相关角色实现,准确反映了代码变更的核心内容。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
src/char/HotoriChain.py (2)

90-165: ⚡ Quick win

提取重复的技能确认逻辑以降低认知复杂度

SonarCloud 标记此函数的认知复杂度为 28(允许上限 15)。主要原因是存在三处几乎相同的"成功退出"代码块(108-117、140-147、151-160),都执行相同的状态设置和角色切换序列。

建议提取公共逻辑到辅助方法:

♻️ 建议重构
+    def _finish_e_start_chain(self):
+        """E技能启动链完成后的公共处理"""
+        self._e_used = True
+        self._e_lockdown = True
+        self.start_team_skill_window()
+        self.sleep(0.1)
+        self.task.chain_executor.step_complete()
+        self._send_chain_key()
+        self.switch_next_char()
+
     def chain_e_start_chain(self):
         # ... 前部逻辑保持不变 ...
 
             if self.has_cd("skill"):
                 self.logger.info("chain_e_start_chain: E already in CD, proceeding")
-                self._e_used = True
-                self._e_lockdown = True
-                self.start_team_skill_window()
-                self.sleep(0.1)
-                self.task.chain_executor.step_complete()
-                self._send_chain_key()
-                self.switch_next_char()
-                return
+                self._finish_e_start_chain()
+                return

对其他两处重复代码块进行同样的替换。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/char/HotoriChain.py` around lines 90 - 165, The function
chain_e_start_chain contains three near-identical "successful exit" blocks;
extract that repeated sequence (setting self._e_used, self._e_lockdown, calling
start_team_skill_window, sleep, self.task.chain_executor.step_complete,
self._send_chain_key, and self.switch_next_char) into a helper method (e.g.,
_finalize_e_chain or _handle_successful_e_cast) and replace each duplicated
block with a single call to that helper from chain_e_start_chain to reduce
cognitive complexity and duplication.

7-21: ⚡ Quick win

将类级别的链式步骤定义改为不可变类型

STARTUP_CHAINWARMUP_CHAIN 作为类级别的可变列表,存在被意外修改的风险。建议改用元组(tuple)确保不可变性,这也符合其作为常量配置的语义。

♻️ 建议修改
 class HotoriChain(Hotori):
-    STARTUP_CHAIN = [
+    STARTUP_CHAIN = (
         ("HotoriChain", "chain_e_start_chain"),
         ("ZeroChain", "chain_q_e_wait"),
         ("JiuyuanChain", "chain_intro_only"),
         ("NanallyChain", "chain_e_q_6s_swap"),
         ("JiuyuanChain", "chain_q_e_heavy"),
         ("HotoriChain", "chain_q_na"),
-    ]
-    WARMUP_CHAIN = [
+    )
+    WARMUP_CHAIN = (
         ("ZeroChain", "chain_nop"),
         ("JiuyuanChain", "chain_intro_only"),
         ("ZeroChain", "chain_e_only"),
         ("NanallyChain", "chain_intro_e_q_10s_swap"),
         ("JiuyuanChain", "chain_q_e_heavy"),
-    ]
+    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/char/HotoriChain.py` around lines 7 - 21, Change the class-level mutable
lists STARTUP_CHAIN and WARMUP_CHAIN in HotoriChain.py to immutable tuples to
prevent accidental modification and reflect their constant configuration nature;
replace the list literals for STARTUP_CHAIN and WARMUP_CHAIN with tuple literals
(use parentheses instead of square brackets) wherever they are defined in the
HotoriChain class so any runtime code referencing STARTUP_CHAIN or WARMUP_CHAIN
receives an immutable sequence.
src/char/NanallyChain.py (2)

131-209: 🏗️ Heavy lift

认知复杂度高(51 vs 15),且与 chain_e_q_6s_swap 存在代码重复。

两个链式方法共享相同的模式:

  1. 角色切换等待逻辑(lines 173-181, 200-205 与 88-96, 120-125 相同)
  2. HotoriChain 查找(line 143 与 line 19)
  3. Hotori 点击循环(lines 186-191 与 101-111)

建议将这些重复模式提取到 NanallyChain 或基类中的共享方法,两个链式方法可以复用。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/char/NanallyChain.py` around lines 131 - 209, The method
chain_intro_e_q_10s_swap has high cognitive complexity and duplicates logic from
chain_e_q_6s_swap (character-switch waiting, HotoriChain lookup, and Hotori
click loop); extract shared patterns into small helper methods on NanallyChain
or its base: e.g., _wait_for_char_index(self, index, timeout) to encapsulate the
switch-wait loops used around self.task.is_char_at_index, _find_hotori(self) to
return the HotoriChain instance/key, and _play_hotori_clicks(self, hotori,
duration) to run the repeated hotori.click()/sleep() loop; then replace the
duplicated blocks in chain_intro_e_q_10s_swap and chain_e_q_6s_swap with calls
to these helpers and keep existing unique logic (skill/ultimate handling) intact
so behavior is unchanged.

15-129: 🏗️ Heavy lift

认知复杂度过高(70 vs 15),建议拆分为多个辅助方法。

此方法包含多个可复用的逻辑块:E技能施放(lines 24-56)、角色切换等待(lines 88-96, 120-125)、普攻循环(lines 62-78)。将这些抽取为独立方法可显著降低复杂度并提高可读性。

♻️ 建议的重构方向
def _switch_to_char(self, char, total_deadline):
    """尝试切换到指定角色,返回是否成功"""
    key = self._get_char_key(char.__class__.__name__)
    if key:
        self.task.send_key(key)
    switch_deadline = time.time() + 1.0
    while (not self.task.is_char_at_index(char.index)
           and time.time() < switch_deadline
           and time.time() < total_deadline):
        self.click()
        self.sleep(0.01)
    return self.task.is_char_at_index(char.index)

def _try_cast_e_with_timeout(self, deadline):
    """尝试在截止时间内施放E技能,返回是否成功"""
    while time.time() < deadline:
        self.task.sleep_check()
        if not self.task.is_char_at_index(self.index):
            self.click()
            self.sleep(0.01)
            continue
        clicked, _, _ = self.click_skill()
        if clicked:
            return True
        self.click()
        self.sleep(0.05)
    return False

def chain_e_q_6s_swap(self):
    # 使用上述辅助方法简化主流程
    ...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/char/NanallyChain.py` around lines 15 - 129, 方法 chain_e_q_6s_swap
的认知复杂度过高;请把可复用的逻辑块抽成小方法:将 E 技能施放循环(当前在 chain_e_q_6s_swap 中使用 e_deadline 的块)提取为
_try_cast_e_with_timeout(self, deadline) 并返回是否成功, 将角色切换等待逻辑(目前重复出现在 hotori 切换和
Nanally 切换处)提取为 _switch_to_char(self, char_or_name, total_deadline)
并返回是否切换成功,以及将普攻循环(NA 循环那段定时与日志逻辑)提取为 _run_na_loop(self, duration,
total_deadline, q_casted_ref)
并在主方法中用这些小函数替换原有代码以简化流程并降低复杂度;确保保留原有的副作用(self.task.sleep_check, self.click,
self.click_skill, self.click_ultimate, self.task._combat_settle.time 修改,
self.task.send_key, self.task.next_frame, self.logger 调用 和
self.task.chain_executor.step_complete()/ _send_chain_key()/switch_next_char())。
src/char/JiuyuanChain.py (1)

31-67: 💤 Low value

认知复杂度略超阈值(19 vs 15)。

SonarCloud 标记此函数认知复杂度为 19。可考虑将开场处理(lines 32-38)和终极技轮询(lines 39-47)抽取为辅助方法,以降低主函数的嵌套层级。

♻️ 可选重构示例
+    def _wait_intro_until_ultimate(self):
+        """等待开场动画直到终极技可用"""
+        if not self.has_intro:
+            return
+        start = time.time()
+        while time.time() - start < self.INTRO_MOTION_FREEZE_DURATION:
+            self.click()
+            if self.ultimate_available():
+                break
+            self.sleep(0.1)
+
     def chain_q_e_heavy(self):
-        if self.has_intro:
-            start = time.time()
-            while time.time() - start < self.INTRO_MOTION_FREEZE_DURATION:
-                self.click()
-                if self.ultimate_available():
-                    break
-                self.sleep(0.1)
+        self._wait_intro_until_ultimate()
         q_deadline = time.time() + 0.3
         # ... rest of the method
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/char/JiuyuanChain.py` around lines 31 - 67, The function chain_q_e_heavy
has high cognitive complexity; extract the intro motion-freeze block (the
has_intro loop that repeatedly clicks until ultimate_available or timeout) into
a helper method (e.g., _handle_intro_motion_freeze) and extract the short
ultimate-polling loop that attempts an ultimate click (the q_deadline loop that
sets task._combat_settle.time, sleeps, clicks and calls click_ultimate) into
another helper (e.g., _poll_and_click_ultimate); then simplify chain_q_e_heavy
to call these two helpers before continuing with the remaining click_skill loop
so nesting and branching are reduced while preserving behavior (ensure the
helpers return any signals needed by chain_q_e_heavy such as whether ultimate
was triggered).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/char/HotoriChain.py`:
- Around line 23-28: The attribute team_skill_window_start is used by methods
like team_skill_window_elapsed and waiting_for_team_skills but isn't declared in
__init__, risking an AttributeError; add an explicit initialization (e.g.
self.team_skill_window_start = None or 0) inside __init__ alongside
self.team_skill_records and the _e_* fields so the attribute always exists, and
leave start_team_skill_window() and clear_team_skill_records() to set/clear its
value as they already do.
- Around line 180-187: The infinite loop in HotoriChain.run that relies on
ultimate_available() and click_ultimate() needs a timeout safeguard: add a
max-wait timer (e.g., start = time.time(), timeout_seconds = X) inside the loop
that calls task.sleep_check(), and after each iteration check if time.time() -
start > timeout_seconds then log an error via self.logger.error (including
current state), break or raise a controlled exception so the loop can exit
safely; update references in the loop where ultimate_available(),
click_ultimate(send_click=True), self.click(), and self.sleep(0.1) are used to
include this timeout check and cleanup behavior.

In `@src/char/ZeroChain.py`:
- Around line 38-47: The infinite loop in the ZeroChain method relies solely on
click_skill() to return clicked=True and lacks a timeout; add a timeout guard
(e.g., record start = time.monotonic() and check elapsed against a configurable
timeout) around the while True loop that calls click_skill(), click(), sleep(),
etc., and when the timeout is exceeded log/warn and perform a safe exit path
(call task.chain_executor.step_complete() or an error handler, then call
_send_chain_key() and switch_next_char() or otherwise return) so the method
never blocks indefinitely; update the loop that uses click_skill(), click(),
sleep() to break on timeout using the chosen timeout variable.

In `@src/combat/ChainExecutor.py`:
- Around line 31-36: The chain startup does not guard against builder()
returning None or an empty list, causing an IndexError when accessing
self.steps[0]; update the startup logic in ChainExecutor where builder() is
called (the local variable steps and the methods _start and subsequent access to
self.steps and first_char/first_method) to explicitly validate the result of
builder() (treat None as []), check for emptiness, and handle that case by
logging/returning early (safe no-op) instead of proceeding to access steps[0];
apply the same guard to the similar block around the code referenced for lines
42-47.

---

Nitpick comments:
In `@src/char/HotoriChain.py`:
- Around line 90-165: The function chain_e_start_chain contains three
near-identical "successful exit" blocks; extract that repeated sequence (setting
self._e_used, self._e_lockdown, calling start_team_skill_window, sleep,
self.task.chain_executor.step_complete, self._send_chain_key, and
self.switch_next_char) into a helper method (e.g., _finalize_e_chain or
_handle_successful_e_cast) and replace each duplicated block with a single call
to that helper from chain_e_start_chain to reduce cognitive complexity and
duplication.
- Around line 7-21: Change the class-level mutable lists STARTUP_CHAIN and
WARMUP_CHAIN in HotoriChain.py to immutable tuples to prevent accidental
modification and reflect their constant configuration nature; replace the list
literals for STARTUP_CHAIN and WARMUP_CHAIN with tuple literals (use parentheses
instead of square brackets) wherever they are defined in the HotoriChain class
so any runtime code referencing STARTUP_CHAIN or WARMUP_CHAIN receives an
immutable sequence.

In `@src/char/JiuyuanChain.py`:
- Around line 31-67: The function chain_q_e_heavy has high cognitive complexity;
extract the intro motion-freeze block (the has_intro loop that repeatedly clicks
until ultimate_available or timeout) into a helper method (e.g.,
_handle_intro_motion_freeze) and extract the short ultimate-polling loop that
attempts an ultimate click (the q_deadline loop that sets
task._combat_settle.time, sleeps, clicks and calls click_ultimate) into another
helper (e.g., _poll_and_click_ultimate); then simplify chain_q_e_heavy to call
these two helpers before continuing with the remaining click_skill loop so
nesting and branching are reduced while preserving behavior (ensure the helpers
return any signals needed by chain_q_e_heavy such as whether ultimate was
triggered).

In `@src/char/NanallyChain.py`:
- Around line 131-209: The method chain_intro_e_q_10s_swap has high cognitive
complexity and duplicates logic from chain_e_q_6s_swap (character-switch
waiting, HotoriChain lookup, and Hotori click loop); extract shared patterns
into small helper methods on NanallyChain or its base: e.g.,
_wait_for_char_index(self, index, timeout) to encapsulate the switch-wait loops
used around self.task.is_char_at_index, _find_hotori(self) to return the
HotoriChain instance/key, and _play_hotori_clicks(self, hotori, duration) to run
the repeated hotori.click()/sleep() loop; then replace the duplicated blocks in
chain_intro_e_q_10s_swap and chain_e_q_6s_swap with calls to these helpers and
keep existing unique logic (skill/ultimate handling) intact so behavior is
unchanged.
- Around line 15-129: 方法 chain_e_q_6s_swap 的认知复杂度过高;请把可复用的逻辑块抽成小方法:将 E
技能施放循环(当前在 chain_e_q_6s_swap 中使用 e_deadline 的块)提取为
_try_cast_e_with_timeout(self, deadline) 并返回是否成功, 将角色切换等待逻辑(目前重复出现在 hotori 切换和
Nanally 切换处)提取为 _switch_to_char(self, char_or_name, total_deadline)
并返回是否切换成功,以及将普攻循环(NA 循环那段定时与日志逻辑)提取为 _run_na_loop(self, duration,
total_deadline, q_casted_ref)
并在主方法中用这些小函数替换原有代码以简化流程并降低复杂度;确保保留原有的副作用(self.task.sleep_check, self.click,
self.click_skill, self.click_ultimate, self.task._combat_settle.time 修改,
self.task.send_key, self.task.next_frame, self.logger 调用 和
self.task.chain_executor.step_complete()/ _send_chain_key()/switch_next_char())。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 809202c1-7a81-4c38-9199-d1738769e06a

📥 Commits

Reviewing files that changed from the base of the PR and between dd5d057 and 2c6aae4.

📒 Files selected for processing (9)
  • src/char/BaseChar.py
  • src/char/CharFactory.py
  • src/char/HotoriChain.py
  • src/char/JiuyuanChain.py
  • src/char/NanallyChain.py
  • src/char/ZeroChain.py
  • src/combat/BaseCombatTask.py
  • src/combat/ChainExecutor.py
  • src/sound_trigger/DodgeCounterTrigger.py

Comment thread src/char/HotoriChain.py
Comment thread src/char/HotoriChain.py Outdated
Comment thread src/char/ZeroChain.py Outdated
Comment thread src/combat/ChainExecutor.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@BnanZ0
Copy link
Copy Markdown
Owner

BnanZ0 commented May 23, 2026

Findings
[P1] 全局闪避抑制没有异常恢复:在 JiuyuanChain.py (line 52) 和 NanallyChain.py (line 16) 中调用 suppress_dodge() 后,后续 sleep_check()、技能点击、切人等任一步抛异常都会跳过 unsuppress_dodge(),导致 DodgeCounterTrigger.py (line 46) 后续一直不闪避。这里应改成 try/finally,更好是做成上下文管理器。

[P1] 通用链式执行器直接硬编码角色:ChainExecutor.step_complete() 在 ChainExecutor.py (line 61) 查找 "HotoriChain" 并调用其记录逻辑。这把特定角色机制塞进了通用战斗调度层,明显污染架构。后续每加一种链式队伍,都可能继续改 executor,维护性会快速变差。

[P2] 链式调度状态侵入 BaseChar / BaseCombatTask:BaseChar.perform() 通过动态字符串 _chain_method 执行方法,见 BaseChar.py (line 128);切人逻辑又读取 _pending_anchor,见 BaseCombatTask.py (line 491)。这让所有角色都承担链式系统的隐藏约定,且缺少类型约束和生命周期边界。建议让 ChainExecutor 持有显式 Step/回调对象,角色只暴露清晰的链式动作接口。

[P2] 角色实现直接改任务内部状态:多处链式角色写 self.task._combat_settle.time = None,例如 HotoriChain.py (line 180)、JiuyuanChain.py (line 40)、NanallyChain.py (line 47)。这是对 CombatCheck 内部 settle 机制的绕过,建议封装成 BaseCombatTask.allow_ultimate_during_settle() 之类的语义方法。

[P3] git diff --check 发现 ZeroChain.py (line 27) 和 ZeroChain.py (line 37) 有 trailing whitespace。

总体判断
与当前项目架构的融合度:中低。PR 复用了现有角色继承和 CharFactory 注册机制,这是好的;但链式系统没有形成独立边界,反而把“浔创生链式”这种具体队伍逻辑扩散到了 BaseChar、BaseCombatTask、ChainExecutor 和声音触发层。
是否存在污染架构的代码:存在,主要是 ChainExecutor 硬编码 HotoriChain,以及核心类新增 _chain_method / _pending_anchor 这类隐式协议。
是否导致可维护性变差:会。当前实现能跑特定队伍,但下一套链式队伍大概率要继续复制 *Chain.py、修改通用 executor、追加私有状态访问。建议先抽出通用链式编排接口,再把 Hotori 这套作为一个配置/策略实现落进去。

@BnanZ0
Copy link
Copy Markdown
Owner

BnanZ0 commented May 23, 2026

简单来说 实现不够优雅 我之后有时间再看一下有没有机会重构进去

Wattls and others added 4 commits May 25, 2026 17:42
- [重构] ChainExecutor: 清除所有角色硬编码(如 HotoriChain 特判),回归纯粹的链式调度器职责。
- [重构] HotoriChain: 引入 `time_to_next_burst` 时间 API,接管 15s/20s 轴的时间裁判权,精准扣除大招时停。同时通过 `on_chain_step_complete` 实现控制反转,自主管理 E 技能锁定。
- [重构] NanallyChain: 移除死循环与硬编码倒计时,重构为基于状态机的 1.2s/0.8s 无阻塞极限合轴。新增 `is_char_at_index` 真实在场判定,解决hotori错误释放E技能的问题。
- [重构] JiuyuanChain: 剥离跨角色越权操作,移除对 Hotori 私有变量的直接修改。
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/combat/ChainExecutor.py (1)

31-36: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

为链启动补充空步骤保护,避免直接崩溃。

Line 35 与 Line 46 在 builder() 返回 None 或空列表时会触发异常(越界/解包),链式流程会被直接打断。建议在 loop()_start() 双重兜底并安全返回。

建议修复
 def loop(self, builder):
     import time
     self._builder = builder
-    steps = builder()
+    steps = builder() or []
+    if not steps:
+        self.active = False
+        self.steps = []
+        self.current_index = 0
+        self.task.log_info("chain builder returned no steps, skip start")
+        return
     self._chain_start_time = time.time()
     self._last_step_time = time.time()
     self._start(steps)
     first_char, first_method = self.steps[0]

 def _start(self, steps):
+    if not steps:
+        self.active = False
+        self.steps = []
+        self.current_index = 0
+        return
     self.steps = steps
     self.current_index = 0
     self.active = True
     first_char, first_method = steps[0]

Also applies to: 42-47

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/combat/ChainExecutor.py` around lines 31 - 36, The code currently assumes
builder() returns a non-empty list and directly indexes/unpacks steps (e.g.,
steps = builder(), self._start(steps), first_char, first_method = self.steps[0])
which raises on None/empty; update _start() and loop() to defensively handle a
falsy or empty steps: if steps is None or len(steps) == 0, log/return safely and
do not proceed to unpack or index self.steps; also set self.steps to an empty
list before returning so other code (like task.get_current_char calls) sees a
consistent state. Ensure checks are applied around the builder() call and at the
top of _start() to short-circuit execution when no steps are present.
🧹 Nitpick comments (5)
src/char/Hotori.py (1)

156-158: 💤 Low value

空代码块需要填充或移除

on_chain_step_complete 方法中存在空的 pass 语句。如果这是有意的空操作(例如作为子类覆盖的占位符),应添加注释说明意图;否则应移除整个条件分支或实现实际逻辑。

♻️ 建议:添加注释或移除空块
     def on_chain_step_complete(self):
         if self.team_skill_window_start > 0:
-            pass
+            # 基类中无需处理,由子类 HotoriChain 覆盖实现
+            pass

或者如果不需要此方法,可以完全移除:

-    def on_chain_step_complete(self):
-        if self.team_skill_window_start > 0:
-            pass
-
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/char/Hotori.py` around lines 156 - 158, The conditional in
on_chain_step_complete contains an empty pass for the case when
team_skill_window_start > 0; either implement the intended behavior or remove
the dead branch — if this is intentionally a placeholder for subclasses, replace
the bare pass with a short clarifying comment like "# intentionally no-op;
override in subclasses" or document the intent; otherwise delete the if-block or
add the actual logic that should run when team_skill_window_start > 0 (refer to
on_chain_step_complete and the team_skill_window_start attribute to locate the
code).
src/char/HotoriChain.py (1)

216-216: ⚖️ Poor tradeoff

直接修改任务内部状态应封装为语义化方法

直接设置 self.task._combat_settle.time = None 绕过了 CombatCheck/settle 机制。这种对内部字段的直接修改增加了隐式耦合,使代码更难维护。

建议在 BaseCombatTask 中封装为语义化方法,如 allow_ultimate_during_settle()reset_combat_settle(),以明确表达意图并集中管理该逻辑。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/char/HotoriChain.py` at line 216, The code directly mutates an internal
field (self.task._combat_settle.time = None) which breaks encapsulation and the
CombatCheck/settle flow; instead add a semantic method on BaseCombatTask (e.g.,
reset_combat_settle() or allow_ultimate_during_settle()) that performs the
necessary state change and any related checks, then replace the direct
assignment in HotoriChain with a call to that new method (use
BaseCombatTask.reset_combat_settle() or the chosen method name to locate and
update the call site and to centralize the logic).
src/char/NanallyChain.py (3)

36-37: ⚖️ Poor tradeoff

直接修改任务内部状态应封装为语义化方法

HotoriChain 中相同,直接设置 self.task._combat_settle.time = None 绕过了 settle 机制。建议封装到 BaseCombatTask 的公开方法中。

Also applies to: 67-68

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/char/NanallyChain.py` around lines 36 - 37, Directly manipulating
self.task._combat_settle.time bypasses the settle mechanism; instead add a
semantic public method on BaseCombatTask (e.g., clear_combat_settle() or
mark_settle_cleared()) that encapsulates setting the internal settle state, then
replace direct assignments (both occurrences where self.task._combat_settle.time
= None in NanallyChain, including the one before click_ultimate and the similar
one at lines ~67-68) with calls to that new BaseCombatTask method; mirror the
pattern used in HotoriChain to keep behavior consistent and preserve
encapsulation.

23-27: ⚡ Quick win

合并嵌套的 if 语句

连续的 if self.skill_available()if self.click_skill() 可以合并为单个条件表达式,减少嵌套层级。

♻️ 建议修改
                 if self.skill_available():
-                    if self.click_skill():
-                        _last_e_time = time.time()
-                        self.logger.info("Nanally E released for copy")
-                        break
+                    if self.click_skill():
+                        _last_e_time = time.time()
+                        self.logger.info("Nanally E released for copy")
+                        break

或使用 and 合并:

-                if self.skill_available():
-                    if self.click_skill():
-                        _last_e_time = time.time()
-                        self.logger.info("Nanally E released for copy")
-                        break
+                if self.skill_available() and self.click_skill():
+                    _last_e_time = time.time()
+                    self.logger.info("Nanally E released for copy")
+                    break
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/char/NanallyChain.py` around lines 23 - 27, Merge the nested conditionals
in the NanallyChain method (the block using self.skill_available() and
self.click_skill()) into a single if statement using an and expression (e.g., if
self.skill_available() and self.click_skill():) so that click_skill() is only
called when skill_available() is true, and preserve the existing actions
(_last_e_time assignment, self.logger.info call, and break) inside that single
block.

62-68: ⚡ Quick win

合并嵌套的 if 语句

if self.task.is_char_at_index(self.index) 内的 if self.skill_available()if self.click_skill() 可以合并。

♻️ 建议修改
                 if self.task.is_char_at_index(self.index):
-                    if self.skill_available():
-                        if self.click_skill():
-                            _last_e_time = time.time()
+                    if self.skill_available() and self.click_skill():
+                        _last_e_time = time.time()
                     if self.ultimate_available() and time.time() - _last_e_time >= 0.4:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/char/NanallyChain.py` around lines 62 - 68, Inside the block guarded by
self.task.is_char_at_index(self.index), merge the nested skill checks by
replacing the separate if self.skill_available(): if self.click_skill():
sequence with a single conditional that calls click_skill only when both
self.skill_available() and self.click_skill() are true; update _last_e_time only
when the combined condition succeeds. Keep the ultimate logic intact (the check
using self.ultimate_available() and time.time() - _last_e_time >= 0.4, setting
self.task._combat_settle.time = None and calling self.click_ultimate()) so
behavior/timing is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/char/BaseChar.py`:
- Around line 602-603: 在 BaseChar 类的 on_chain_step_complete 方法中补充一条明确说明该空实现为默认
no-op 拓展点的文档字符串或内联注释(例如在函数体内添加 """No-op hook; override in subclasses to handle
chain step completion.""" 或 # intentional no-op hook for
subclasses),以表明这是有意为之并避免 Sonar 静态检查误报;定位符号:方法名 on_chain_step_complete,类名
BaseChar。

In `@src/char/NanallyChain.py`:
- Around line 15-107: chain_dynamic_standby has excessive cognitive complexity
(≈75); refactor by extracting smaller helpers and delegating responsibilities:
implement _try_initial_e_release(self) to run the initial "E" release block
(return last_e_time or 0), implement _try_q_after_e(self, last_e_time) to
attempt the Q after E, implement _standby_loop_iteration(self, hotori,
last_e_time) to encapsulate the inner per-iteration logic that handles clicks,
skill/ultimate checks and sending Nanally key (return updated last_e_time and a
flag to indicate handing off), and implement _switch_to_hotori_and_back(self,
hotori) to perform sending Hotori key, waiting for switch, executing Hotori
burst, and switching back; then simplify chain_dynamic_standby to call these
helpers in sequence, pass/return _last_e_time and hotori as needed, and preserve
all existing timing, logging and task.sleep_check() behavior to keep semantics
identical.

---

Duplicate comments:
In `@src/combat/ChainExecutor.py`:
- Around line 31-36: The code currently assumes builder() returns a non-empty
list and directly indexes/unpacks steps (e.g., steps = builder(),
self._start(steps), first_char, first_method = self.steps[0]) which raises on
None/empty; update _start() and loop() to defensively handle a falsy or empty
steps: if steps is None or len(steps) == 0, log/return safely and do not proceed
to unpack or index self.steps; also set self.steps to an empty list before
returning so other code (like task.get_current_char calls) sees a consistent
state. Ensure checks are applied around the builder() call and at the top of
_start() to short-circuit execution when no steps are present.

---

Nitpick comments:
In `@src/char/Hotori.py`:
- Around line 156-158: The conditional in on_chain_step_complete contains an
empty pass for the case when team_skill_window_start > 0; either implement the
intended behavior or remove the dead branch — if this is intentionally a
placeholder for subclasses, replace the bare pass with a short clarifying
comment like "# intentionally no-op; override in subclasses" or document the
intent; otherwise delete the if-block or add the actual logic that should run
when team_skill_window_start > 0 (refer to on_chain_step_complete and the
team_skill_window_start attribute to locate the code).

In `@src/char/HotoriChain.py`:
- Line 216: The code directly mutates an internal field
(self.task._combat_settle.time = None) which breaks encapsulation and the
CombatCheck/settle flow; instead add a semantic method on BaseCombatTask (e.g.,
reset_combat_settle() or allow_ultimate_during_settle()) that performs the
necessary state change and any related checks, then replace the direct
assignment in HotoriChain with a call to that new method (use
BaseCombatTask.reset_combat_settle() or the chosen method name to locate and
update the call site and to centralize the logic).

In `@src/char/NanallyChain.py`:
- Around line 36-37: Directly manipulating self.task._combat_settle.time
bypasses the settle mechanism; instead add a semantic public method on
BaseCombatTask (e.g., clear_combat_settle() or mark_settle_cleared()) that
encapsulates setting the internal settle state, then replace direct assignments
(both occurrences where self.task._combat_settle.time = None in NanallyChain,
including the one before click_ultimate and the similar one at lines ~67-68)
with calls to that new BaseCombatTask method; mirror the pattern used in
HotoriChain to keep behavior consistent and preserve encapsulation.
- Around line 23-27: Merge the nested conditionals in the NanallyChain method
(the block using self.skill_available() and self.click_skill()) into a single if
statement using an and expression (e.g., if self.skill_available() and
self.click_skill():) so that click_skill() is only called when skill_available()
is true, and preserve the existing actions (_last_e_time assignment,
self.logger.info call, and break) inside that single block.
- Around line 62-68: Inside the block guarded by
self.task.is_char_at_index(self.index), merge the nested skill checks by
replacing the separate if self.skill_available(): if self.click_skill():
sequence with a single conditional that calls click_skill only when both
self.skill_available() and self.click_skill() are true; update _last_e_time only
when the combined condition succeeds. Keep the ultimate logic intact (the check
using self.ultimate_available() and time.time() - _last_e_time >= 0.4, setting
self.task._combat_settle.time = None and calling self.click_ultimate()) so
behavior/timing is preserved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 003351b3-bc27-4cd6-a30a-89d7c10384d9

📥 Commits

Reviewing files that changed from the base of the PR and between e40fb57 and 9fd3f37.

📒 Files selected for processing (6)
  • src/char/BaseChar.py
  • src/char/Hotori.py
  • src/char/HotoriChain.py
  • src/char/JiuyuanChain.py
  • src/char/NanallyChain.py
  • src/combat/ChainExecutor.py
💤 Files with no reviewable changes (1)
  • src/char/JiuyuanChain.py

Comment thread src/char/BaseChar.py Outdated
Comment thread src/char/NanallyChain.py Outdated
@BnanZ0 BnanZ0 marked this pull request as draft May 27, 2026 08:49
@Wattls Wattls marked this pull request as ready for review May 27, 2026 08:50
@Wattls Wattls marked this pull request as draft May 27, 2026 09:14
Wattls added 3 commits May 28, 2026 01:26
1. 新增ChainLoader实现角色策略化加载与替换
2. 新增全局连招策略配置UI与后端逻辑
3. 为多个角色链添加角色就位等待逻辑
4. 优化内置连招注册表过滤规则
5. 取消九原连招的闪避压制问题
6. 优化娜娜莉连招的技能释放时机判断
1.  统一链内常量配置,替换硬编码
2.  新增CD确认机制,避免技能释放误判
3.  优化切人等待逻辑,增加超时保护
4.  重构NanallyChain动态驻场逻辑,修复切人循环问题
@Wattls
Copy link
Copy Markdown
Contributor Author

Wattls commented May 27, 2026

简单来说 实现不够优雅 我之后有时间再看一下有没有机会重构进去

你看看还有什么问题

@Wattls Wattls marked this pull request as ready for review May 27, 2026 20:47
@Wattls Wattls marked this pull request as draft May 28, 2026 15:07
@SkyJiashu
Copy link
Copy Markdown

好像存在角色检测问题,同样顺序配队,提示缺少必要角色,另外部分情况无自动索敌。
ScreenShot_2026-05-28_213125_980
ScreenShot_2026-05-28_213139_390

- 新增 get_all_character_names() 合并内置+自定义角色名

- 新增重新关联角色功能
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants