Files
gplan-2/production/qa/bugs/BUG-0001.md
T
2026-09-14 18:18:49 +08:00

86 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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