86 lines
8.3 KiB
Markdown
86 lines
8.3 KiB
Markdown
# Bug Report
|
||
|
||
## Summary
|
||
**Title**: 结算战斗段首行下发时 StatusSlotRow 按索引误取 tooltip 节点崩溃,结算协程中断 → 结算阶段永久软锁
|
||
**ID**: BUG-0001
|
||
**Severity**: S1-Critical(核心循环阻断——结算阶段永久假死,只能杀进程)
|
||
**Priority**: P1-Immediate
|
||
**Status**: Closed
|
||
**Reported**: 2026-09-12
|
||
**Reporter**: 用户(设计师本人,垂直切片试玩)
|
||
|
||
## Classification
|
||
- **Category**: Crash / 软锁(soft lock,无崩溃弹窗的假死)
|
||
- **System**: C 区状态效果槽(StatusSlotRow)呈现层 × B1 结算编排(信号链中断面)
|
||
- **Frequency**: 条件必现(前置 = 状态槽悬停过一次 + 其后任一结算行刷新时英雄侧效果 ≥1;悬停是新增益出现后的自然操作,实际对局高频可触)
|
||
- **Regression**: Yes——`status_slot_row.gd` 与 C 区数据框为 2026-09-12 五区改造批新增;2026-09-11 build 无此视图
|
||
|
||
## Environment
|
||
- **Build**: 垂直切片 · battle.md 五区改造批(2026-09-12);卡死会话种子 42,19:22:19 开局
|
||
- **Platform**: PC · Win10 · Godot 4.7.2 (OpenGL Compatibility · RTX 2060 SUPER)
|
||
- **Scene/Level**: 完整关卡模式(第一关)· 轮末结算(道具窗口跳过后、战斗段首行)
|
||
- **Game State**: 第 1 场战斗结算中;道具窗口以「跳过」关闭;英雄侧存在本结算新授予的状态效果(槽可见)
|
||
|
||
## Reproduction Steps
|
||
**Preconditions**: 对局中英雄侧至少有 1 个状态效果(如安全格授予的增伤),状态槽在 C 区可见。
|
||
|
||
1. 鼠标悬停任一状态效果槽(tooltip 弹出——此步把 `_tip` 挂入 StatusSlotRow 子节点表)
|
||
2. 正常结束本轮(停手或 AP 耗尽),进入轮末结算
|
||
3. 结算段任一 step_row 下发(如道具窗口跳过 → 战斗段「主角攻击」行)触发 `_refresh_presentations`
|
||
4. 游戏无声冻结:棋盘保持去饱和、阶段文案停在「结算中」,所有输入通道无效
|
||
|
||
**Expected Result**: 结算七段完整回放,逐行兑现后进入下一轮(或终局 overlay)。
|
||
**Actual Result**: `status_slot_row.gd:118` 脚本错误沿信号链回卷,`_finish_settlement` 在战斗段(⑤)中途被中止——`_settling` 恒为 `true`、A3 恒为 `PHASE_WAITING`,结算永不恢复。
|
||
|
||
## Technical Context
|
||
- **Likely affected files**:
|
||
- `prototypes/dungeon-sweeper-vertical-slice/presentation/views/status_slot_row.gd:57-63`(`set_effects` 用 `get_child(i)` 按位取槽——`_tip` 入树后索引错位)
|
||
- `prototypes/dungeon-sweeper-vertical-slice/presentation/views/status_slot_row.gd:80-88`(`_sync_slots` 显式保留 `_tip`,与上行索引假设自相矛盾)
|
||
- `prototypes/dungeon-sweeper-vertical-slice/presentation/views/status_slot_row.gd:114-118`(`_fill_slot` 对 cast 为 null 的 `col` 调 `get_child`)
|
||
- 中断面:`core/battle_orchestrator.gd:233-263`(`_finish_settlement`——错误回卷致 `_settling` 悬挂)
|
||
- **Related systems**: `main.gd _on_step_row → _refresh_presentations`(每结算行同步刷新);`level_runner.gd:128` 转发 lambda;`hud_presenter.gd:29`
|
||
- **Possible root cause**(已由崩溃回溯证实):
|
||
tooltip 节点 `_tip` 在 `_init` 不入树,仅 `_on_slot_hover` 首次悬停时 `add_child(_tip)`,与动态重建的槽共用同一子节点表。此后 `set_effects` 内 `get_child(i)` 取到的第 0 个"槽"实为 `_tip`(PanelContainer,cast 通过),`_fill_slot` 中 `_tip.get_child(0)` 是 Label → `as VBoxContainer` = null → 118 行 `col.get_child(0)` 报
|
||
`Cannot call method 'get_child' on a null value`。
|
||
Godot 4 对信号回调内的运行时错误整条 GDScript 调用栈回卷:按钮 lambda → `submit_window_selection` → `_finish_settlement` → `_combat_segment` 全部中止,B1 无"结算恢复"路径,软锁。
|
||
|
||
## Evidence
|
||
- **Logs**(`user://logs/`,DungeonSweeperVerticalSlice):
|
||
- `godot.log`(19:22 会话)完整回溯:`_fill_slot (status_slot_row.gd:118)` ← `set_effects` ← `set_status_effects` ← `HudPresenter.refresh` ← `main._on_step_row` ← `level_runner lambda` ← `_emit_row` ← `_combat_segment (battle_orchestrator.gd:474)` ← `_finish_settlement (242)` ← `submit_window_selection (230)` ← `_on_window_skipped` ← skip 按钮 lambda
|
||
- `playtest_42_20260912T192219.jsonl`:`ux_window_choice{skip:true}` 之后再无任何事件(无 settlement 快照、无 ux_round_exit、无下一轮)——结算协程死亡实锤
|
||
- **Visual**: 棋盘去饱和锁定、阶段指示「第 N 轮 · 结算中」、明细带展开但停在该结算首行;与 2026-09-11 崩溃 #1 的"输入锁已上 + 呈现层崩溃 = 无声冻结"同族
|
||
|
||
## Related Issues
|
||
- `production/qa/playtests/playtest-2026-09-11-designer.md` 崩溃 #1/#2——同一失败家族(结算期呈现层错误 + 核心中断/输入锁死),当时的防复发三条硬性要求(载荷 schema 成文 / 场景级 headless 测试 / 状态可见性)第三条仍未覆盖本路径
|
||
- `presentation/views/board_view.gd:476` `_make_cursor_texture` 启动期 `canvas_item is null` 渲染错误(P3,独立缺陷,与本软锁无关)
|
||
- `status_slot_row.gd:57-63` 潜在次生缺陷:`set_effects` 循环上界是 `effects.size()` 而 `_sync_slots` 只建 `min(size, 4)` 个槽——英雄侧效果 >4 时(off_guard max_stack 16)同函数必崩
|
||
- 场景探针(ui_probe/ui_click_probe)未覆盖「悬停 → 结算刷新」路径——冒烟再次漏掉呈现层时序态
|
||
|
||
## Notes
|
||
- 修复方向(最小面):槽与 tooltip 分离承载——`_tip` 改挂 StatusSlotRow 之外的独立层,或 `set_effects`/`_sync_slots` 改用槽引用数组(不按子节点位序索引)。两处任选其一,后者改动面更小但必须同步修掉 >4 溢出循环上界
|
||
- 按代码规范「缺陷修复必须沉淀回归断言」:需在场景级探针补"悬停槽位后触发一次含效果行的结算,断言无脚本错误且 phase 走到下一轮"
|
||
- 本次卡死用户未点「诊断转储」(现存三份 diag 均来自探针 `{"test":true}` 上下文,core 字段为空)——诊断按钮对真实软锁会话未被使用,不影响本次定位(godot.log 回溯已足够)
|
||
|
||
## Fix Record(2026-09-12,Status 仍 Open——正式流转待 /bug-report verify)
|
||
- **修复**:`status_slot_row.gd` 槽生命周期改由 `_slots: Array[PanelContainer]` 引用数组唯一管理,`set_effects`/`_sync_slots`/`set_cleared_feedback` 全部弃用子节点位序索引;顺带修掉 effects > MAX_SLOTS 时循环越界次生缺陷(超出截断至 4 槽,符合"最多 4 槽可见"规格)
|
||
- **回归断言**:`smoke/ui_probe.gd` 新增 BUG-0001 回归段(建槽 → 悬停入 tooltip → 重刷断言槽位/填充 → 超上限截断 → 空态清槽)
|
||
- **阴性对照**:回滚修复版跑探针,精确复现野外崩溃(`status_slot_row.gd:118` 同行同错);恢复修复版后探针通过——断言有效
|
||
- **验证**:smoke_all 161 检查 0 失败 · ui_probe PASS · ui_click_probe(seed 7)关卡通关/问题=0/选用失效=0/无卡死 · ui_tune_probe PASS ×2
|
||
- **待办**:人类完整试玩复跑(playtest 报告 Top1 优先级)后走 verify/close
|
||
|
||
## Verification Record(2026-09-12)
|
||
**判定**:VERIFIED FIXED
|
||
- 复现路径核查:`status_slot_row.gd` 中 `set_effects`/`_sync_slots`/`set_cleared_feedback` 已无子节点位序索引(grep 复核,仅 `_slots` 引用数组访问)——原崩溃代码路径不复存在
|
||
- 阴性对照:修复前版本在 ui_probe 回归段精确复现野外崩溃(`status_slot_row.gd:118` 同行同错),证明断言与定位有效
|
||
- 相关测试:smoke_all 161 项检查 0 失败(关闭时复跑)· ui_probe PASS(含 BUG-0001 回归段)· ui_click_probe(seed 7)关卡通关/问题=0/无卡死 · ui_tune_probe PASS
|
||
- 人工确认:用户试玩确认"没有问题"
|
||
|
||
## Closure Record
|
||
**Closed**: 2026-09-12
|
||
**Resolution**: Fixed — StatusSlotRow 槽生命周期改由 `_slots` 引用数组唯一管理,弃用子节点位序索引(tooltip 入树不再错位);顺带修复 effects > MAX_SLOTS 循环越界(截断至 4 槽)
|
||
**Fix commit / PR**: 本地工作区直接修复(项目非 git 仓)
|
||
**Verified by**: qa-tester
|
||
**Closed by**: 用户
|
||
**Regression test**: `prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd`(BUG-0001 回归段)
|
||
**Status**: Closed
|