Skip to content

feat(combat): add customizable fixed team axes - #210

Open
GodRaymond233 wants to merge 2 commits into
BnanZ0:mainfrom
GodRaymond233:feature/team-axis
Open

feat(combat): add customizable fixed team axes#210
GodRaymond233 wants to merge 2 commits into
BnanZ0:mainfrom
GodRaymond233:feature/team-axis

Conversation

@GodRaymond233

@GodRaymond233 GodRaymond233 commented Jun 23, 2026

Copy link
Copy Markdown

Summary

新增固定队伍轴的自定义能力。

主要内容:

  • 新增固定队伍轴框架。
  • 已内建 娜娜莉+零+九原+浔 标准创生队固定轴作为内置预设。
  • 在“固定轴”页面支持新增自定义队伍轴。
  • 自定义队伍轴包含:轴名、轴简介、轴内容。
  • 自定义轴支持 4 个角色位置,使用 p1_ / p2_ / p3_ / p4_ 前缀复用现有出招表指令。

Why

原本固定轴只能使用预设队伍,用户无法在界面中自行配置完整队伍轴。

这个改动让用户可以基于 4 个位置分别编写角色出招逻辑,从而自定义自己的固定队伍流程,而不是只依赖内置创生队轴。

Tests

  • 已手动测试固定轴配置流程,流程正常。
  • python -m unittest tests.TestTeamAxis tests.TestCustomCharCore tests.TestCustomChar
  • uvx ruff check . --select F,I

Manual test evidence

录屏:

Summary by CodeRabbit

发布说明

  • 新功能
    • 新增“固定队伍轴”系统,实现固定四人队伍的队伍级自动战斗流程(开场与循环)。
    • 支持创建/启用/停用/删除自定义队伍轴,并提供命令语法与示例指引。
    • 新增内置队伍轴(九原-零-娜娜莉-浔),并新增对应配置界面入口。
  • 改进
    • 团队轴匹配与执行框架完善,自动战斗在匹配后接管战斗流程。
  • Bug Fixes
    • 修复战斗上下文为空时的异常风险;重击时长参数可配置并向下传递。
  • 文档
    • 新增“专属队伍轴”模块说明文档。
  • 测试
    • 扩充固定/团队轴的单元测试覆盖。

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

本 PR 新增了完整的"团队轴(Team Axis)"框架:定义了 BaseTeamAxis 抽象基类与 TeamAxisError,实现了基于文本命令的 CustomTeamAxis 编译器,内建了 NanallyZeroJiuyuanHotoriAxis 四人轴,提供了 TeamAxisRegistry 注册/发现机制,扩展 CustomCharManager DB 模式至版本 6 以持久化团队轴配置,在 AutoCombatTask 中接入固定轴逻辑,新增 TeamAxisTab UI 配置界面,并补充了对应测试与文档。

Changes

团队轴(Team Axis)系统全量引入

Layer / File(s) Summary
TeamAxisError 与 BaseTeamAxis 基类
src/team_axis/BaseTeamAxis.py
新增 TeamAxisError 运行时异常;BaseTeamAxis 提供配置类属性、构造函数(builtin_key→char 缓存)、matches/matches_signature 匹配、perform_next 开场/循环控制流、角色读取与强制切换、skill/ultimate/normal_attack/heavy_attack/sleep/call_current 动作封装。
CustomTeamAxis 编译器与数据定义
src/team_axis/CustomTeamAxis.py
新增 _split_example_commands_prefixed_example_slot_command 工厂;TeamAxisComboCompiler 复用 CustomChar 组合语法并支持 if_ 条件;冻结数据类 CustomTeamAxisDefinition 封装元信息与签名;CustomTeamAxis 实现文本编译、槽位命令分发(p1_~p4_ 前缀)与角色级命令映射。
NanallyZeroJiuyuanHotoriAxis 内建轴
src/team_axis/axes/NanallyZeroJiuyuanHotoriAxis.py, src/team_axis/axes/__init__.py
四人队(娜娜莉/零/九原/浔)完整循环轴,含开场/循环阶段编排、终极可用性分支、头像进度与冷却等待识别、ROI 图像进度评分,以及 _require_action 断言工具。
TeamAxisRegistry 注册表与包入口
src/team_axis/TeamAxisRegistry.py, src/team_axis/__init__.py
通过反射扫描 src.team_axis.axes 发现内建轴类,从 CustomCharManager 追加自定义轴,按 priority 降序缓存;提供 get_axis_classes/get_axis_class/create_matching_team_axis/clear_registry_cache__init__.py 通过 __all__ 统一导出。
CustomCharManager 团队轴持久化扩展
src/char/custom/CustomCharManager.py
DB 模式从 4 升至 6;新增 CUSTOM_TEAM_AXIS_PREFIX;扩展默认结构、归一化函数族、load_db/validate_db/migrate_db_schema;新增 generate_custom_team_axis_id/get_custom_team_axes/get_custom_team_axis/set_custom_team_axis/delete_custom_team_axis/get_fixed_team_axis/set_fixed_team_axis 等完整 CRUD。
AutoCombatTask 接入与 Jiuyuan 修复
src/tasks/trigger/AutoCombatTask.py, src/char/Jiuyuan.py
AutoCombatTask 新增 create_fixed_team_axis(),战斗循环按轴是否存在分支(team_axis.perform_next() 或原逻辑),捕获 TeamAxisError 并在战斗结束时调用 on_combat_end()Jiuyuan.fire_bullets 增加 duration 参数并加 context 非空保护。
TeamAxisTab 配置界面
src/ui/TeamAxisTab.py, src/ui/CharHubTab.py
新增 TeamAxisSlotCard(槽位展示)与 TeamAxisTab(固定队伍槽位展示、内建/自定义轴下拉、新建/保存/删除/启用/停用、可搜索命令文档);CharHubTab 注册 TeamAxisTab 为子界面。
测试、文档与 .gitignore
tests/TestTeamAxis.py, tests/TestCustomCharCore.py, tests/TestCustomChar.py, src/team_axis/README.md, .gitignore
TestTeamAxis 使用伪对象覆盖匹配/执行/注册表/自定义轴/NanallyZeroJiuyuanHotoriAxis 完整时序;TestCustomCharCore 覆盖持久化与删除;TestCustomChar 覆盖 UI 匹配与自定义轴保存;README 说明接管边界与用法;.gitignore 新增 tests/.tmp/

Sequence Diagram(s)

sequenceDiagram
  participant AutoCombatTask
  participant CustomCharManager
  participant TeamAxisRegistry
  participant BaseTeamAxis

  rect rgba(70, 130, 180, 0.5)
    note over AutoCombatTask,CustomCharManager: 战斗开始,首次创建固定轴
    AutoCombatTask->>CustomCharManager: get_fixed_team()
    AutoCombatTask->>CustomCharManager: get_fixed_team_axis()
    AutoCombatTask->>TeamAxisRegistry: create_matching_team_axis(task, axis_id)
    TeamAxisRegistry->>BaseTeamAxis: matches(task.chars)
    TeamAxisRegistry-->>AutoCombatTask: team_axis 实例 or None
  end

  rect rgba(60, 179, 113, 0.5)
    note over AutoCombatTask,BaseTeamAxis: 战斗循环
    loop 每轮循环
      alt team_axis 存在
        AutoCombatTask->>BaseTeamAxis: perform_next()
        BaseTeamAxis->>BaseTeamAxis: run_opening() / run_cycle()
      else 原逻辑
        AutoCombatTask->>AutoCombatTask: get_current_char().perform()
      end
    end
    AutoCombatTask->>BaseTeamAxis: on_combat_end()
  end
Loading
sequenceDiagram
  participant TeamAxisTab
  participant CustomCharManager
  participant CustomTeamAxis
  participant TeamAxisRegistry

  TeamAxisTab->>CustomCharManager: get_fixed_team()
  TeamAxisTab->>TeamAxisRegistry: get_axis_classes()
  TeamAxisTab->>TeamAxisTab: fixed_team_signature()
  TeamAxisTab->>CustomTeamAxis: validate_axis_syntax(content)
  CustomTeamAxis-->>TeamAxisTab: (valid, error)
  TeamAxisTab->>CustomCharManager: set_custom_team_axis(axis_id, name, ...)
  TeamAxisTab->>CustomCharManager: set_fixed_team_axis(enabled, axis_id)
  TeamAxisTab->>TeamAxisTab: refresh_state()
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 耳朵竖起听战鼓,
四人成阵轴旋转,
p1 切换 p2 跟上,
if_ 一声定乾坤,
团队轴兔跳得欢!🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.66% 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的核心变更——引入可定制的固定团队轴框架,涵盖了新增BaseTeamAxis、CustomTeamAxis、内建的NanallyZeroJiuyuanHotoriAxis、UI界面支持等主要功能。
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
tests/TestTeamAxis.py (1)

299-302: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

时间补丁建议改为固定返回值,降低实现耦合导致的测试脆弱性。

Line 299-302 与 Line 354-357 目前用 side_effect=(100.0, 100.0),如果实现内部新增一次 monotonic() 调用会直接触发 StopIteration。这里更适合 return_value=100.0

建议修改
     `@patch`(
         "src.team_axis.axes.NanallyZeroJiuyuanHotoriAxis.time.monotonic",
-        side_effect=(100.0, 100.0),
+        return_value=100.0,
     )
...
     `@patch`(
         "src.team_axis.axes.NanallyZeroJiuyuanHotoriAxis.time.monotonic",
-        side_effect=(100.0, 100.0),
+        return_value=100.0,
     )

Also applies to: 354-357

🤖 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 `@tests/TestTeamAxis.py` around lines 299 - 302, The patch decorator for
`src.team_axis.axes.NanallyZeroJiuyuanHotoriAxis.time.monotonic` uses
`side_effect=(100.0, 100.0)` which restricts the mock to return a value exactly
twice, causing StopIteration if the implementation makes additional calls to
monotonic(). Replace `side_effect=(100.0, 100.0)` with `return_value=100.0` in
both patch decorators (lines 299-302 and 354-357) to provide a fixed return
value that works regardless of how many times the implementation calls
monotonic(), making the test less brittle and decoupled from internal
implementation details.
🤖 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/team_axis/CustomTeamAxis.py`:
- Around line 13-25: The `_prefixed_example` function incorrectly splits command
examples using simple `split(",")` without accounting for commas inside
parentheses, causing multi-parameter commands like `walk(w, 0.2)` to be split
incorrectly and generate malformed prefixed examples. Replace the simple
`split(",")` call with intelligent parsing that tracks parenthesis depth and
only treats commas at depth zero as delimiters. Iterate through the string
character by character, increment depth on opening parentheses and decrement on
closing ones, and only mark a comma as a split point when depth equals zero.

In `@src/team_axis/TeamAxisRegistry.py`:
- Around line 45-48: The custom axes compilation in the list comprehension lacks
error handling, causing any single malformed axis configuration to crash the
entire registration process and interrupt the battle loop. Wrap the
CustomTeamAxis.build_definition(axis) call within a try-except block that
catches exceptions for individual axis builds, logs the error details for that
specific axis, and continues processing remaining axes by only including
successfully built definitions in the final custom_axes list. This ensures that
one bad configuration does not block the entire axis registration flow.

In `@src/ui/TeamAxisTab.py`:
- Around line 282-295: The `_match_state` method currently only validates the
team signature but does not check if the custom axis has actual content,
allowing axes with empty content to be directly enabled and written to the
effective configuration. Add a validation check in the `_match_state` method
after obtaining the axis via `get_axis_class(self.selected_axis_id())` to ensure
the axis content is not empty before returning the success status. This will
prevent incomplete or unconfigured axes from being enabled and avoid execution
failures during battle engagement.

In `@tests/TestCustomChar.py`:
- Around line 255-260: The assertion in the
test_char_hub_contains_fixed_axis_tab method directly checks for a hardcoded
localized string "固定轴" against hub.team_axis_tab.name, which couples the test to
a specific locale and will fail in different language environments. Replace this
localized string assertion with a language-independent check, such as verifying
the tab instance exists or using the objectName() property instead, to ensure
the test remains robust across different internationalization environments
without depending on specific locale settings.

In `@tests/TestTeamAxis.py`:
- Around line 275-279: The test case at lines 275-279 uses a patch context for
CustomCharManager that affects the TeamAxisRegistry's caching behavior, which
can cause cross-test contamination. Add calls to clear_registry_cache() before
entering the patch context (either in setUp or immediately before the with patch
block) and after the test completes (either in tearDown or immediately after the
assertions) to ensure the registry cache is properly isolated and cleaned up
between test runs.

---

Nitpick comments:
In `@tests/TestTeamAxis.py`:
- Around line 299-302: The patch decorator for
`src.team_axis.axes.NanallyZeroJiuyuanHotoriAxis.time.monotonic` uses
`side_effect=(100.0, 100.0)` which restricts the mock to return a value exactly
twice, causing StopIteration if the implementation makes additional calls to
monotonic(). Replace `side_effect=(100.0, 100.0)` with `return_value=100.0` in
both patch decorators (lines 299-302 and 354-357) to provide a fixed return
value that works regardless of how many times the implementation calls
monotonic(), making the test less brittle and decoupled from internal
implementation details.
🪄 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: a2c9a4c7-001b-4d7c-9a3e-ef4538e0f622

📥 Commits

Reviewing files that changed from the base of the PR and between 782db64 and f28afb2.

📒 Files selected for processing (16)
  • .gitignore
  • src/char/Jiuyuan.py
  • src/char/custom/CustomCharManager.py
  • src/tasks/trigger/AutoCombatTask.py
  • src/team_axis/BaseTeamAxis.py
  • src/team_axis/CustomTeamAxis.py
  • src/team_axis/README.md
  • src/team_axis/TeamAxisRegistry.py
  • src/team_axis/__init__.py
  • src/team_axis/axes/NanallyZeroJiuyuanHotoriAxis.py
  • src/team_axis/axes/__init__.py
  • src/ui/CharHubTab.py
  • src/ui/TeamAxisTab.py
  • tests/TestCustomChar.py
  • tests/TestCustomCharCore.py
  • tests/TestTeamAxis.py

Comment thread src/team_axis/CustomTeamAxis.py
Comment thread src/team_axis/TeamAxisRegistry.py Outdated
Comment thread src/ui/TeamAxisTab.py
Comment thread tests/TestCustomChar.py
Comment thread tests/TestTeamAxis.py
@sonarqubecloud

Copy link
Copy Markdown

@BnanZ0

BnanZ0 commented Jun 23, 2026

Copy link
Copy Markdown
Owner

没有必要 当前的combat planner已经可以支撑固定轴的编写,另外引入一个系统没有意义,且当前的创生队已经会根据老板娘的技能来打创生反应了

@GodRaymond233

Copy link
Copy Markdown
Author

Thanks for the review. I addressed the reported issues in 950b265:

  • Fixed command example splitting so examples with nested commas, such as walk(w, 0.2), are preserved correctly.
  • Made custom team axis loading tolerant of malformed custom axis configs.
  • Added validation to prevent empty or invalid custom axes from being enabled.
  • Updated tests to avoid locale-sensitive text assertions.
  • Added registry cache cleanup in tests to avoid cross-test pollution.

Validation:

  • .venv\Scripts\python.exe -m unittest tests.TestTeamAxis tests.TestCustomCharCore tests.TestCustomChar
  • uvx ruff check src/team_axis/CustomTeamAxis.py src/team_axis/TeamAxisRegistry.py src/ui/TeamAxisTab.py tests/TestTeamAxis.py tests/TestCustomChar.py --select F,I

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/team_axis/CustomTeamAxis.py (1)

13-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

SonarCloud 质量门:_split_example_commands 认知复杂度超标(21 > 15)

逻辑本身正确,但 SonarCloud 将此函数标记为 CI 失败。可将引号/转义处理抽取为独立辅助函数以降低复杂度,使该函数只负责括号深度与逗号切分。

♻️ 建议的重构思路
+def _consume_quoted(char: str, quote: str, escape: bool) -> tuple[str, bool]:
+    if escape:
+        return quote, False
+    if char == "\\":
+        return quote, True
+    if char == quote:
+        return "", False
+    return quote, False
+
+
 def _split_example_commands(example: str) -> list[str]:
     parts = []
     current = []
     depth = 0
     quote = ""
     escape = False

     for char in example:
         if quote:
             current.append(char)
-            if escape:
-                escape = False
-            elif char == "\\":
-                escape = True
-            elif char == quote:
-                quote = ""
+            quote, escape = _consume_quoted(char, quote, escape)
             continue
🤖 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/team_axis/CustomTeamAxis.py` around lines 13 - 52, The
`_split_example_commands` function exceeds SonarCloud's cognitive complexity
threshold (21 > 15) due to nested conditional logic. Extract the quote and
escape handling logic (the `if quote:` block and the `if char in ("'", '"'):`
block) into a separate helper function that processes character handling when
inside or entering quoted strings. This helper function should return the
updated quote and escape states along with whether the character should be
appended to the current buffer. Refactor the main function to call this helper
and keep only the bracket depth tracking and comma-based splitting logic at the
top level, which will significantly reduce the cognitive complexity of
`_split_example_commands`.

Source: Linters/SAST tools

🤖 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.

Nitpick comments:
In `@src/team_axis/CustomTeamAxis.py`:
- Around line 13-52: The `_split_example_commands` function exceeds SonarCloud's
cognitive complexity threshold (21 > 15) due to nested conditional logic.
Extract the quote and escape handling logic (the `if quote:` block and the `if
char in ("'", '"'):` block) into a separate helper function that processes
character handling when inside or entering quoted strings. This helper function
should return the updated quote and escape states along with whether the
character should be appended to the current buffer. Refactor the main function
to call this helper and keep only the bracket depth tracking and comma-based
splitting logic at the top level, which will significantly reduce the cognitive
complexity of `_split_example_commands`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 92df4a3d-b2e7-485c-b092-24538ca1a6e0

📥 Commits

Reviewing files that changed from the base of the PR and between f28afb2 and 950b265.

📒 Files selected for processing (5)
  • src/team_axis/CustomTeamAxis.py
  • src/team_axis/TeamAxisRegistry.py
  • src/ui/TeamAxisTab.py
  • tests/TestCustomChar.py
  • tests/TestTeamAxis.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/ui/TeamAxisTab.py
  • tests/TestTeamAxis.py

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.

2 participants