From d2b36e59cb623d9f8fe264bd544ebacd2f297a15 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 30 Oct 2026 07:13:23 +0800 Subject: [PATCH 1/3] =?UTF-8?q?iter44-=E5=8D=A11=EF=BC=9A=E5=8D=A1?= =?UTF-8?q?=E2=91=A0(=E7=BB=84=E5=90=88=E6=A0=B9=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E9=9D=A2=E6=89=B9):=20ui=5Fregistry+=E4=B8=89=E5=9F=9F?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=E5=BB=BA=E4=BB=B6=C2=B7main.gd=2049=E2=86=92?= =?UTF-8?q?15=C2=B7=E5=85=A8=E6=B6=88=E8=B4=B9=E9=9D=A2=E6=94=B9=E5=9D=80m?= =?UTF-8?q?ain.ui.*=C2=B7=E5=9B=9B=E9=97=B8=E4=B8=BB=E8=AF=81=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA=E9=9B=B6=E5=8F=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/art/art-assets-checklist.md | 2 +- design/art/art-prompts.md | 6 +- design/art/remote-gen-prompts.md | 2 +- .../engineering/architecture/architecture.md | 4 +- design/engineering/process-conventions.md | 2 +- design/engineering/test-conventions.md | 2 +- game/presentation/main.gd | 340 ++++------------- game/presentation/main_adventure_flow.gd | 90 ++--- game/presentation/main_debug_flow.gd | 30 +- game/presentation/main_overlay_flow.gd | 68 ++-- game/presentation/main_ui_build.gd | 237 ++++++------ game/presentation/ui_battle.gd | 10 + game/presentation/ui_battle.gd.uid | 1 + game/presentation/ui_modals.gd | 32 ++ game/presentation/ui_modals.gd.uid | 1 + game/presentation/ui_registry.gd | 101 ++++++ game/presentation/ui_registry.gd.uid | 1 + game/presentation/ui_screens.gd | 16 + game/presentation/ui_screens.gd.uid | 1 + game/probe/fx_shot_probe.gd | 6 +- game/probe/layout_probe.gd | 11 +- game/probe/probe_ui_util.gd | 2 +- game/probe/screen_flow_probe.gd | 42 +-- game/probe/shot_probe.gd | 58 +-- game/probe/t17_defer_probe.gd | 18 +- game/probe/ui_click_probe.gd | 342 +++++++++--------- game/probe/ui_input_probe.gd | 30 +- game/probe/ui_probe.gd | 102 +++--- game/probe/ui_struct_probe.gd | 136 +++---- game/probe/ui_tune_probe.gd | 80 ++-- game/tests/.gates/baseline.json | 10 +- game/tests/README.md | 4 +- game/tests/smoke_boot.gd | 8 +- game/tests/system_test.gd | 82 ++--- .../tests/unit/contracts/source_guard_test.gd | 39 +- .../unit/presentation/pacing_queue_test.gd | 14 +- .../presentation/theme_projection_test.gd | 4 +- reports/需求卡片-测速循环批-2026-10-29.md | 94 ++--- tools/render_gate.sh | 16 - tools/run_all_gates.sh | 21 +- 待办清单.md | 78 ++-- 41 files changed, 1073 insertions(+), 1070 deletions(-) create mode 100644 game/presentation/ui_battle.gd create mode 100644 game/presentation/ui_battle.gd.uid create mode 100644 game/presentation/ui_modals.gd create mode 100644 game/presentation/ui_modals.gd.uid create mode 100644 game/presentation/ui_registry.gd create mode 100644 game/presentation/ui_registry.gd.uid create mode 100644 game/presentation/ui_screens.gd create mode 100644 game/presentation/ui_screens.gd.uid diff --git a/design/art/art-assets-checklist.md b/design/art/art-assets-checklist.md index 2aac8c7f..b6a3e98f 100644 --- a/design/art/art-assets-checklist.md +++ b/design/art/art-assets-checklist.md @@ -131,7 +131,7 @@ ### 3.5 营地场景图 - **要什么**:营地场景图 9 张——`scene_camp_bg`(背景)+ `scene_camp_gate / forge / chest / notebook / dummy / board / campfire`(七个可点物件)+ `scene_camp_tent`(承载件)。钥石顶栏图标沿用现役 `icon_keystone`。 -- **来源/规格**:**✦ 插画级**(作者自跑远程生图管线);烛光黄 `#F6C667` + 木色 `#8A5A34`(art-bible §6.2);2× 源档出图、最近邻降采样到 1280×800;后处理=抠底/降采样/色板量化/描边统一。**出图登记照 [remote-gen-prompts](remote-gen-prompts.md) 头注轻量口径执行:用途+生成时间必注,其余参数有则记。** +- **来源/规格**:**✦ 插画级**(作者自跑远程生图管线);烛光黄 `#F6C667` + 木色 `#8A5A34`(art-bible §6.2);2× 源档出图、最近邻降采样到 1280×800;后处理=抠底/降采样/色板量化/描边统一。**每张出图须登记模型名+分辨率+seed+对话/日期**。 - **排期**:营地交互重做**美术自立批**(不占迭代位;UI 贴图重构批已搁置)。终稿落 `game/assets/art/scene/`,消费常量 `UiKit.TEX_CAMP_*`。 - **现状**:全占位。 diff --git a/design/art/art-prompts.md b/design/art/art-prompts.md index ca2d2c3e..76348109 100644 --- a/design/art/art-prompts.md +++ b/design/art/art-prompts.md @@ -1,4 +1,4 @@ -# 生图提示词卡(角色级资产)v2.11 +# 生图提示词卡(角色级资产)v2.10 > **性质**:美术清单生图通道(✦ 用户自跑,现走线上 API)的执行侧文件——每资产一段**自包含完整提示词**:正向 = 通用底座+主体+动作已拼妥、负向 = 通用底座+增补已并妥,整段复制直接贴进工作流即可,**不需要读其他文件、不需要拼装**。人物规格转写自 [character-design.md](character-design.md)(人物设计 v1.0,自 art-bible §5 迁出,圣经 §5 为存根),风格/色彩条款转写自 [art-bible](art-bible.md)(§4.1 主色板 / §4.2 语义色规则);冲突时人物规格以 character-design 为准、色彩以 art-bible 为准。 > **边界**:本文件只管**角色级**资产的生图通道。图标/格面/样张走脚本管线(⚙ 代理出),规格在 art-bible §4.6 与 [art-assets-checklist](art-assets-checklist.md),不需要提示词。**场景级资产(营地场景图的背景层与八个物件层)另立 [camp-scene-prompts](camp-scene-prompts.md),本文件不覆盖**;两卡的角色/场景分工以「谁的资产谁出卡」为界,同一段提示词不两处维护。战斗舞台的场景级两件与护甲类型图标三枚的**出图工单**登记在本文件 §5(camp-scene-prompts 只管营地场景图,战斗屏这两类无处可落);§5 只把各权威节的定值收进逐件工单,不立新值。意图动画三档是动画资产,等一屏样张锁方案后另立规格卡,本文件不覆盖。 @@ -289,7 +289,7 @@ Seamless tileable pixel art texture of dry dirt ground, top-down, for a dungeon > **范围注**:本文件原定位角色级(见文件头边界句),§4bis 已有「地形类同库登记」的先例,本节循同一先例补登记战斗屏两类——战斗舞台的场景级两件(① ②)与护甲类型图标三枚(③),两组在营地卡与清单里都无处落工单;角色级立绘重产(④)是本文件的本职。四组的规格定值清单 §4 已有登记,本节补的是「逐件照着就能出件」那一层。 > **定值纪律(§8 头注 定值单点纪律)**:本节**指路不立值**——色值 = 圣经 §4.1(含场景环境子板)、尺寸与缩放 = §8.2、明度带 = §2、甲档 = character-design §5.3、命名 = §8.3 + asset-path-map。本节新增零定值;凡圣经未定的值,一律进本节 §5.5 缺口,不在本节填空。 > **验收标注纪律**:逐条按三档标 `←机检: <断言>` / `←目检: <判据>` / `←方向性`,照圣经头注 2026-10-06 用户拍板的写法。 -> **流程**:出图前后一律走 §0 五步;出图登记义务照 remote-gen-prompts 头注轻量口径(用途+生成时间必注,其余参数有则记),随候选图存 `artwork/candidates/<资产名>/`;登记不作正稿遴选前置。 +> **流程**:出图前后一律走 §0 五步;出图登记义务(模型名 + 分辨率 + seed + 对话/日期)照 remote-gen-prompts 头注,随候选图存 `artwork/candidates/<资产名>/`,缺登记的候选不入正稿遴选。 > **产出状态**:组二地台件与组三护甲三枚已按各节 ⑤「程序化产出即终稿」产出入包(`scene_stage_plaza.png` 48×12、`ui_armor_light/ward/heavy.png` 24×24,路径见各组 ① 字段;登记=战斗一屏样张批,合并提交 3b66d1a6);组一背景插画与组四立绘重产仍待出图。 ### 5.1 组一 · B 区舞台背景插画(1 件) @@ -445,7 +445,7 @@ Do not add any of the following: realistic, photorealistic or 3D rendering; pain 1. 出件顺序 = 先主角、再三 Boss(Alpha 范围 = 主角 + 三 Boss 重产,其余敌种当前可读则保留到 Beta)。 2. **源密度判据 = 三判据②**:显示 96–188px 下脸、铲、旗、物种可认——照这条反推源档即可,逐键定档已完成(§8.2),本批不必再猜档位。 3. 手绘起稿顺序 = 剪影(§5.1 archetype)→ 体块主色 → 暗部与受光 → 描边 → 落影底盘。 -4. 逐件 2–3 版候选;登记照 remote-gen-prompts 头注轻量口径(用途+生成时间必注,手绘作者有则记),随候选图存 `artwork/candidates/<资产名>/`。 +4. 逐件 2–3 版候选;登记义务项 = 手绘者 / 日期 / 源档尺寸(手绘件无模型名与 seed,按此三项替换 §0 的登记项),随候选图存 `artwork/candidates/<资产名>/`。 5. 候选评审 = 并排横评,**隐去文件名与来源后按三判据逐条判读**(§0 五步第 1 步「便于横评」的手绘侧落法),不靠记忆与印象拍板。 6. 出件 = 透明 PNG(Alpha 直出,禁先出白底图再抠);入包前过 §8.3 色板检查器(报新增色 / 近色告警 / 超 48 上限拦截)。 diff --git a/design/art/remote-gen-prompts.md b/design/art/remote-gen-prompts.md index 394440d0..29ed0880 100644 --- a/design/art/remote-gen-prompts.md +++ b/design/art/remote-gen-prompts.md @@ -2,7 +2,7 @@ > **渠道定位**:生图渠道 = **远程对话式生图为主,本地 ComfyUI 为备选**;本文件是**对话操作单**(怎么开对话/贴什么参考图/怎么续写),提示词正文词库归 [art-prompts](art-prompts.md)——分工:词库改词库、操作单改流程,同一段正文不双处维护。 > **冲突优先级**:画法与色彩以 [art-bible](art-bible.md) 为准,人物规格以 [character-design](character-design.md) 为准;本单为执行侧操作单,不另立规格。 -> **出图登记义务(出处备查)**:每张产出随图注明 **用途**(备选/参考/其他什么用)与 **生成时间**(YYYY-MM-DD HH:MM 格式),随候选原图同存 `artwork/candidates/<资产名>/`;模型名、seed、分辨率等参数有则随手记、无则不填,分辨率这类文件属性里自带的值不必抄。登记不作正稿遴选前置——缺注明不落选,遴选时问清用途即可。 +> **出图登记义务(复现性)**:每张产出登记 **模型名 + 分辨率 + seed(可取则取)+ 对话/日期**,随候选原图同存 `artwork/candidates/<资产名>/`;缺登记的候选不入正稿遴选。 > 开新对话出图:同一角色 2–3 版候选各开新对话(同对话续写=修图);同角色换姿势在同对话续写。分辨率开大档;候选原图存 `artwork/candidates/<资产名>/`。 ## 1. 主角战斗立绘 diff --git a/design/engineering/architecture/architecture.md b/design/engineering/architecture/architecture.md index fd60bb59..a169a01a 100644 --- a/design/engineering/architecture/architecture.md +++ b/design/engineering/architecture/architecture.md @@ -238,7 +238,7 @@ | **run_system_test**(系统测试运行器) | `tools/run_system_test.py` | 系统测试总入口:headless 跑真实玩法路径断言面(宿主=`res://tests/system_test.gd`)+ 三闸判定(口径同 run_smoke,摘要行兼容两种宿主格式) | 进程退出码(0=PASS / 1=FAIL) | 三闸齐备(同 run_smoke:退出码/摘要失败数/脚本错误扫描;wall-clock 超时在位) | | **run_all**(单元与集成测试) | `game/tests/run_all.gd` | 64 套统一入口(61 单元+3 集成,2026-10-28 实数):套件 manifest 逐条「套件 → 期望退出码」,致命类 expect=1 独立子进程实跑;`negative` 字段标负面套件(仅豁免错误扫描一闸);选跑态 `--only=<词[,词…]>`(直跑形态对套件路径子串匹配只跑命中套件,零命中=用法错 quit(2),仅供开发期最窄确认——不产闸回执、不作验收凭据,批末全量闸不经选跑分支) | 逐套件结果 + 汇总退出码(任一套件失败 quit(1)) | 套件级退出码协议在位;三硬闸齐备(入口内扫子套件输出 + 编排层扫 `unit.log`;`Kit.finish` 零断言返回专用码 2)。子进程输出须经系统命令解释器重定向落盘再读——`OS.execute` 的输出管道在本机恒空 | | **probe_gate**(场景探针) | `tools/probe_gate.sh` | headless 场景探针段编排:默认段 ui_input_probe / ui_struct_probe / layout_probe / ui_contrast_probe / **screen_flow_probe**(屏壳切换),`--click` 加跑 ui_click_probe,`--full` 加跑 ui_probe / ui_tune_probe / t17_defer_probe / fx_probe;孤儿 Godot 预清 + 批末残留清点(必须 0) | 段级 FAIL 输出 + 汇总退出码 | 每段 timeout 守卫在位(挂死 124/137 = FAIL,不静默);判定统一走 `verdict` 函数——退出码为唯一权威判据,PASS 串降级为附加判据,逐段跑 `tools/error_scan.py` | -| **render_gate**(视觉 golden) | `tools/render_gate.sh` | 窗口化渲染闸:①shot_probe 七屏截图 → shot_checker A-D 语义断言;②fx_shot_probe / board_shot_probe 取证(超时/零字节/文件缺失任一 = FAIL);③棋盘 golden 逐像素容差比对(差 ≤16 / changed ≤2%,provisional)+ 菜单 golden;④批末孤儿残留清零;⑤三段探针日志逐段脚本错误扫描 | 段级 FAIL 输出 + 汇总退出码 | 查退出码在位(每段 timeout 守卫 + 退出码/取证文件双检);SCRIPT ERROR 扫描在位(逐段 `tools/error_scan.py`,标记与 run_smoke 同源);断言数下限不适用(判定主体 = golden 容差与取证文件存在性,非断言计数) | +| **render_gate**(视觉 golden) | `tools/render_gate.sh` | 窗口化渲染闸:①shot_probe 七屏截图 → shot_checker A-D 语义断言;②fx_shot_probe / board_shot_probe 取证(超时/零字节/文件缺失任一 = FAIL);③棋盘 golden 逐像素容差比对(差 ≤16 / changed ≤2%,provisional)+ 菜单 golden;④批末孤儿残留清零 | 段级 FAIL 输出 + 汇总退出码 | 查退出码在位(每段 timeout 守卫 + 退出码/取证文件双检);**SCRIPT ERROR 扫描缺失**;断言数下限不适用(判定主体 = golden 容差与取证文件存在性,非断言计数) | | **static_scan**(静态扫描域) | `game/tests/static_scan.sh` | 静态扫描域 S1–S10 可执行载体(grep 规则 + `tools/audit_type_hints.py`〔S9〕+ `tools/audit_file_headers.py`〔S10〕) | fail 聚合退出码(任一硬失败规则命中即 exit 1) | 查退出码在位;SCRIPT ERROR 扫描与断言数下限不适用(逐规则命中即红,无运行时错误面与断言计数);S3 现为 warning 级、不参与 fail 判定(见下表) | ### 改动域划分(2026-10-17 用户裁「代码批只跑代码闸、文档批只跑文档闸」) @@ -267,7 +267,7 @@ | 硬闸 | run_smoke | run_system_test | run_all | probe_gate | render_gate | static_scan | |---|---|---|---|---|---|---| | **退出码优先** | 已落实(闸①) | 已落实(闸①,口径同 run_smoke) | 已落实(套件 manifest expect 协议) | 已落实(`verdict` 函数逐段判退出码,PASS 串不作判据) | 已落实(段级退出码 + 取证双检) | 已落实(fail 聚合退出) | -| **SCRIPT ERROR 扫描** | 已落实(闸③,外部扫描——GDScript 无脚本错误陷阱 API,闸设在外部包装) | 已落实(闸③,标记清单与 run_smoke 同源) | 已落实(入口内扫子套件输出;子进程的 stdout/stderr 须先落盘——`OS.execute` 管道在本机恒空) | 已落实(逐探针段 `tools/error_scan.py`,标记与 run_smoke 同源) | 已落实(三段探针日志逐段 `tools/error_scan.py`,标记与 run_smoke 同源) | 不适用(静态域无运行时错误面) | +| **SCRIPT ERROR 扫描** | 已落实(闸③,外部扫描——GDScript 无脚本错误陷阱 API,闸设在外部包装) | 已落实(闸③,标记清单与 run_smoke 同源) | 已落实(入口内扫子套件输出;子进程的 stdout/stderr 须先落盘——`OS.execute` 管道在本机恒空) | 已落实(逐探针段 `tools/error_scan.py`,标记与 run_smoke 同源) | **缺失**(本批未收——判定主体是 golden 容差与取证存在性;登记待办清单) | 不适用(静态域无运行时错误面) | | **断言数下限** | 无独立下限(闸②摘要行必须在案,防「跑一半仍 PASS」;脚本错误吞断言路径由闸③兜住) | 同 run_smoke(过渡期宿主摘要行必须在案) | 已落实(`Kit.finish` 零断言返回专用退出码 2——与「断言失败=1」区分,防致命类套件的 expect 协议放过) | 已落实(退出码 0 但无该探针签名标记亦判红——防静默退出) | 不适用(golden 容差 + 取证存在性判定) | 不适用(逐规则命中判定;另有扫描面为空即判红的零命中自检) | 背景事实(run_smoke 头注在案):GDScript 运行时脚本错误不改进程退出码、不计入冒烟失败数、且静默中断所在函数剩余断言——2026-09-19 状态记录 83 实证「309 项 0 失败」实为断言未跑全。这是 SCRIPT ERROR 扫描必须存在于判定层的原因。 diff --git a/design/engineering/process-conventions.md b/design/engineering/process-conventions.md index 04273dd3..727fc342 100644 --- a/design/engineering/process-conventions.md +++ b/design/engineering/process-conventions.md @@ -48,7 +48,7 @@ 7. **验收改动先行声明**——验收变化(基线换代/探针增改/旧断言改判)预先写进需求卡片,禁事后补 **完成** -8. **批末按域验收才合并(worktree 批;直改车道无批末动作,验收在 commit 点由 git 钩子执行,见 §1 第 3 条)**——在任务 worktree 内跑 `tools/run_all_gates.sh`,**按本批改动域选档**:按文件性质判——代码面=`game/` 下非 `.md` 的行为载体(代码、测试脚本、探针、场景、数据表、素材)→ `--code`;文档件=`game/` 内 `.md`(README、生成清单)与 `game/` 外一切路径(`design/`、根规则文件、`tools/` 工具脚本、`.zcode/` 配置等)→ `--docs`;两类同批都有 → 全量档不带参数。代码域按**关联档选测**(2026-10-29 用户裁):02 单元与集成只跑「需求卡片申报 ∪ diff 推导」并集的套件——申报=在册需求卡片对照表「验收测试」列提到的测试文件名,推导=闸脚本内套件族映射表按改动文件归族;改动命中公共件/数据表/autoload/数值·存档·RNG·结算触面或映射不到 → 横切回落全部套件;00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降;03 场景探针与 05 渲染闸维持影响面口径(本批 game/ 改动全在测量面 game/tests、game/sim 时记 SKIP)。所选域全绿即可合回 master,全量回归由用户自发发起(合入后无自动兜底)。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。该脚本是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。跑完写回执 `game/tests/.gates/receipt.json`(含 `domain`、`scope`(full|related,02 跑子集=related)与 `dirty_game` 字段),合回时由 worktree-guard 按 diff 文件性质推域强制校验(六况拦截:缺回执/quick 模式/回执未覆盖分支尖=回执 commit 之后代码面又有改动、或跑闸时代码面带着未提交改动(`dirty_game`)/域不覆盖本次改动/含新增红/master HEAD 非回执 commit 祖先=合并将产出未测内容;guard 只验域覆盖与全绿,不复核选套件清单)。回执与分支尖的对齐判据是**代码面**不是 commit 号:回执之后只追加文档件(`game/` 内 `.md`、`reports/`、`design/` 等)提交,回执照样有效,不构成复跑理由。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占合回全量——其质量义务=落批时人工确认闸可跑通且判据口径未变,闸真崩溃时合回闸 fail-closed(跑不出回执即拦),不以合回全量为兜底 +8. **批末按域验收才合并(worktree 批;直改车道无批末动作,验收在 commit 点由 git 钩子执行,见 §1 第 3 条)**——在任务 worktree 内跑 `tools/run_all_gates.sh`,**按本批改动域选档**:按文件性质判——代码面=`game/` 下非 `.md` 的行为载体(代码、测试脚本、探针、场景、数据表、素材)→ `--code`;文档件=`game/` 内 `.md`(README、生成清单)与 `game/` 外一切路径(`design/`、根规则文件、`tools/` 工具脚本、`.zcode/` 配置等)→ `--docs`;两类同批都有 → 全量档不带参数。代码域按**关联档选测**(2026-10-29 用户裁):02 单元与集成只跑「需求卡片申报 ∪ diff 推导」并集的套件——申报=在册需求卡片对照表「验收测试」列提到的测试文件名,推导=闸脚本内套件族映射表按改动文件归族;改动命中公共件/数据表/autoload/数值·存档·RNG·结算触面或映射不到 → 横切回落全部套件;00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降;03/04 探针与 05 渲染闸维持影响面口径(本批 game/ 改动全在测量面 game/tests、game/sim 时记 SKIP)。所选域全绿即可合回 master,全量回归由用户自发发起(合入后无自动兜底)。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。该脚本是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。跑完写回执 `game/tests/.gates/receipt.json`(含 `domain`、`scope`(full|related,02 跑子集=related)与 `dirty_game` 字段),合回时由 worktree-guard 按 diff 文件性质推域强制校验(六况拦截:缺回执/quick 模式/回执未覆盖分支尖=回执 commit 之后代码面又有改动、或跑闸时代码面带着未提交改动(`dirty_game`)/域不覆盖本次改动/含新增红/master HEAD 非回执 commit 祖先=合并将产出未测内容;guard 只验域覆盖与全绿,不复核选套件清单)。回执与分支尖的对齐判据是**代码面**不是 commit 号:回执之后只追加文档件(`game/` 内 `.md`、`reports/`、`design/` 等)提交,回执照样有效,不构成复跑理由。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占合回全量——其质量义务=落批时人工确认闸可跑通且判据口径未变,闸真崩溃时合回闸 fail-closed(跑不出回执即拦),不以合回全量为兜底 9. **合并后主区零复跑**(2026-10-24 用户裁:合回重跑验的是与回执同一份字节,零信息)——合回落地后 code 批只做两件秒级事:`--editor --quit` 重刷类名缓存(在册坑:.godot 缓存不入 git,与合并后代码失配会假红假绿)+冒烟启动确认可开机(合计约 1 分钟);docs 批零动作。全量验证**每批跑一次**=worktree 批末按域闸,顺序是「先并入 master、再跑闸」(§1 第 2 条);只有两种情形允许把这次跑作废重来——① 闸红且修法动了 `game/`,② 闸已开跑后 master 又被推进(合并将产出未测内容,由合回闸祖先判定拦下,§3.9 第六况)。「跑都跑了再跑一遍保险」「闸后顺手改点 `game/` 再补一跑」都不构成复跑理由;master 上永不跑全量 10. **批次完成后的登记义务**——合回 master 后按 doc-conventions §12.1 推进待办清单条目状态(做完整条删除),涉及迭代编排的在 roadmap 迭代总表与顶部「当前位」节换代——执行入口=项目技能 batch-closure(批后收尾与中间文档退役),跨会话教训写记忆库。批次过程与结论由 git 提交信息承载,禁止再写会话状态文件(见 doc-conventions 第 10 节)。`design/` 与根规则文件(`AGENTS.md`/`roadmap.md`/`待办清单.md`/`体验验证清单.md`)实现批禁写,见 §12 11. **文档同步闸(实现批禁写文档)**——实现批**禁写 `design/` 整个目录与根规则文件**,**另禁写 `game/tests/README.md`(能力清单,归写卡段维护,test-conventions §8.9)**,要往外交代的东西一律落 `reports/`:实现报告 `reports/iter-实现报告-<日期>.md`(各卡状态+实机目检总清单+遗留读数义务+实现决定记录——超白名单/超卡片明列范围事项逐条记「决了什么/为什么/代价/撤销方式」,全部按卡执行时只写一行「零拍板」)、GDD 反哺点清单 `reports/iter-反哺点-<日期>.md`(每条写清哪个文件哪一节、改成什么、依据是什么)。设计权威文档(GDD/art-bible)**任何批都不直改**:用户在会话中逐点裁决后,由工程/文档设计改动批(直改类)写入 `design/`(执行入口=doc-design 技能),并把反哺条目从 `reports/` 清掉(吸收即死;退役执行=batch-closure 技能)。机械面文档(roadmap 行/术语表/README 工具注)同样归工程/文档设计改动批在合并前更新。反哺点未落地期间相关 GDD 节视为待反哺,读前先查 `reports/` 与待办清单;需求卡片必列「关联文档同步面」按两层标注 diff --git a/design/engineering/test-conventions.md b/design/engineering/test-conventions.md index f82fde02..c89d61a8 100644 --- a/design/engineering/test-conventions.md +++ b/design/engineering/test-conventions.md @@ -67,7 +67,7 @@ - 「跑一遍没崩=通过」无效:测试绿只证明**当前代码没犯这个错**,不证明**犯了这个错会被抓住**;断言存在不等于断言执行。 - **脚本错误会静默中断所在函数的剩余断言,而套件仍报全绿**(机制事实):GDScript 无脚本错误陷阱 API,错误不改进程退出码、不计失败数。三处实证:①2026-09-19「309 项 0 失败」实为断言未跑全(状态记录 83);②敌人表行集纪律 10 条断言因定型数组赋值错误从未执行(迭代 29 修复后两条正向见红=夹具本缺锚行);③`Callable` 形参类型写错致解析函数返回 null、后续断言整段蒸发。 - 由此产生两条义务:**写闸件**按 §6 配三硬闸;**写验收函数**必须自证断言全数执行——真实入口断言在当轮执行并通过(§8.4)为主证,零命中自检(扫描面为空即判红)为辅证。 -- **覆盖率不是目标**:不引入行覆盖率度量(Godot 无内建,第三方属新基建);现行补洞=能力清单覆盖域表+人工判定,判定可靠性靠上列自证手段,不靠覆盖率数字。 +- **覆盖率不是目标**:行覆盖率度量缺失(Godot 无内建,第三方属新基建,是否引入待办清单在册待裁);现行补洞=零指名单测清单+人工判定,判定可靠性靠上列自证手段,不靠覆盖率数字。 ## 8. 改动级验收闭合(2026-10-21 立) diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 11aa6eb6..9540e8c6 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -1,7 +1,8 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION ## 组合根:唯一的模式流转与 core↔view 接线处(垂直切片验证:线性第一关承载元进度循环)。 ## db.hero 运行期只读,加成经 runner.set_hero_override / debug_session 生效值入战斗。 -## 禁链式穿透 settlement.*(相位读取只走 presentation_snapshot);模态开合只走 _open_modal/_close_modal。 +## 禁链式穿透 settlement.*(相位读取只走 presentation_snapshot);模态开合只走 ui.modals.flow(注册面编排口)。 +## 界面句柄与层清单在 ui 注册面(ui_registry.gd)——屏壳/战区/模态三域句柄与 LAYER_CONTRACT 单点宿主。 ## 局态全在 run_scope.gd 节点,禁在屏/视图侧持有局态引用;autoload 节点路径常量单点在 AUTOLOAD_PATH_*。 extends Control @@ -15,10 +16,8 @@ const AUTOLOAD_PATH_META := "/root/MetaProgression" const UiAssembler := preload("res://presentation/ui_assembler.gd") const Narrator := preload("res://presentation/battle_narrator.gd") -const ModalStack := preload("res://presentation/modal_stack.gd") const TuneSimController := preload("res://presentation/tune_sim_controller.gd") const DebugSessionScript := preload("res://presentation/debug_session.gd") -const RowPacer := preload("res://presentation/row_pacer.gd") # 结算行排播泵 const TreasureInteractController := preload("res://presentation/treasure_interact_controller.gd") # 秘宝两步交互 const CurioInteractController := preload("res://presentation/curio_interact_controller.gd") # 三选一与替换子交互 const CampShopPresenter := preload("res://presentation/camp_shop_presenter.gd") # 营地/商店投影 @@ -31,33 +30,6 @@ const MenuScreenScene := preload("res://presentation/scenes/menu_screen.tscn") const CampScreenScene := preload("res://presentation/scenes/camp_screen.tscn") const AdventureScreenScene := preload("res://presentation/scenes/adventure_screen.tscn") -# ==================== 视觉层合同(唯一事实源——树序=登记序=z 序;改序只改此表) ==================== -# kind: shell=屏壳容器(成员随壳不占独立树位;level_screen 例外另占登记位)/modal=模态层(入登记册, -# tags 随登记)/state=全屏态(占树位不入登记册)/node=非视觉节点(占树位不入登记册)。 -# host=宿住屏壳(树序随宿主、登记序按本合同);shell=模态成员的屏壳(成员与屏壳同槽:屏壳占该树位、 -# 成员占该登记位)。落位=_apply_layer_contract(全仓唯一层序落位点,add_child 之后统一排序+登记)。 -const LAYER_CONTRACT: Array[Dictionary] = [ - {"key": "adventure_screen", "kind": "shell"}, - {"key": "menu_screen", "kind": "shell"}, - {"key": "row_pacer", "kind": "node"}, - {"key": "legend_overlay", "kind": "modal", "tags": []}, - {"key": "node_overlay", "kind": "modal", "tags": []}, - {"key": "level_screen", "kind": "modal", "tags": [ModalStack.TAG_SCREEN_STATE], "host": "adventure_screen"}, - {"key": "curio_overlay", "kind": "modal", "tags": []}, - {"key": "end_overlay", "kind": "modal", "tags": []}, - {"key": "shop_overlay", "kind": "modal", "tags": []}, - {"key": "camp_overlay", "kind": "modal", "tags": [], "shell": "camp_screen"}, - {"key": "settings_overlay", "kind": "modal", "tags": []}, - {"key": "status_panel", "kind": "modal", "tags": []}, - {"key": "career_overlay", "kind": "modal", "tags": []}, - {"key": "save_select_overlay", "kind": "modal", "tags": []}, # 选档界面(营地交互重做卡③——登记生涯同档) - {"key": "inter_overlay", "kind": "state", "tags": []}, - {"key": "confirm_dialog", "kind": "modal", "tags": []}, - {"key": "debug_overlay", "kind": "modal", "tags": [ModalStack.TAG_NON_LOCKING]}, - {"key": "debug_badge", "kind": "float"}, # 调试入口全局浮层——末位=最顶绘制序; - # float 类不注册 ModalStack(open() 遮挡记账只隐藏登记层)→ 任何 UI 打开都不遮它 -] - var db var run_scope = null # 会话状态承载(run_scope.gd;_ready 即建闲时态,永非 null) # SaveManager/MetaProgression 升格 autoload——成员名保留为「autoload 读视图」 @@ -71,99 +43,24 @@ var meta: MetaProgressionCore: return get_node(AUTOLOAD_PATH_META) var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件) var _debug_entry := false # 调试会话门控(缺省 false=发布态:角标恒隐、浮窗无 UI 开启口;沿革查 git log) -var _seed_edit: LineEdit -var confirm_dialog: ConfirmDialog # 通用确认弹窗(消费方编排——现行消费方=选档删档确认;新冒险互斥确认已随卡②主菜单撤出,归卡④大门) +var ui: UiRegistry # 界面注册面(ui_registry.gd——界面句柄/层序合同/模态编排句柄的单点宿主) var _confirm_consumer := "" # 确认弹窗消费方状态量(契约 3 编排——open 前设、分派后清) var _confirm_consumer_slot := 0 # 消费方附随槽号(delete_slot 消费方专用) -var curio_overlay: CurioPickOverlay # 提升三选一覆盖层(level-clear.md v2 选择面承载;奖励节点共用) -var _start_overlay: Control -var _end_overlay: BattleEndOverlay -var _inter_overlay: Control -var _inter_label: Label -var level_screen: LevelMapScreen # 关卡画面句柄(指向 adventure_screen.level_screen) -var _battle_root: Control # 战斗屏宿主句柄(指向 adventure_screen.battle_root) # 调试模式编排(单场/连续两层——状态机与战斗生命周期归 debug_session.gd,本类只接线与出 UI) var _debug = null -# 视图(views/)——battle.md 三区(节点链进度 -# 由关卡画面本体选路覆盖层承载) -var stage: BattleStageView # B 区(含 log_view 明细带)——句柄指向 adventure_screen -var log_view: BattleLogView -var info_panel: HeroInfoPanelView # C 区(攻击/AC 数据框+两预留区) -var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx) -var board_view: BoardView # 探针兼容别名(= board_zone.board_view) -var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar) -var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下) - -# 三屏壳实例(屏成员经本类句柄暴露;探针挂载假设 main.X 一级路径) -var _menu_screen = null # MenuScreen(开始菜单屏壳) -var _camp_screen = null # CampScreen(营地屏壳) -var _adventure_screen = null # AdventureScreen(冒险屏壳——内含关卡地图/战斗两容器) - # 拆件后的组合根持有面(职责见各类头注;装配见 _build_ui) -var _row_pacer = null # 结算行排播泵(row_pacer.gd——本类只入队/排空/读队列) var _treasure_ui = null # 秘宝两步交互(treasure_interact_controller.gd——本类只接线与转发意图) var _bonus = null # 三选一与替换子交互(curio_interact_controller.gd——本类只接线与回调注入) var _camp_pres = null # 营地/商店投影(camp_shop_presenter.gd——本类只接线、探针缝保缝转发) -# 排播队列只读委托面(探针/测试读 main._row_queue——与 RowPacer 队列共享同一 Array 引用) -var _row_queue: Array[Variant]: - get: - return _row_pacer.pending_rows() - -# 模态 overlay(z 序 = 登记序;pattern 6 层级纪律) -var legend_overlay: LegendOverlay -var node_overlay: NodeOverlay -var shop_overlay: ShopOverlay # 商店覆盖层(商店房节点兑付呈现) -var camp_overlay: CampOverlay # 营地覆盖层句柄(指向 camp_screen.camp_overlay) var settings_store # 系统级设置独立存储(音量/全屏/减动效) -var settings_overlay: SettingsOverlay # 设置覆盖层(最小设置面) -var status_panel: StatusPanel # 局内状态面板·只读(钥石余额+营地三柜台进度查看;登记设置层同档) -var career_overlay: CareerOverlay # 生涯页·只读全屏页(跨局汇总六类九项;登记营地同档〔设置层旁、确认弹窗之前〕) -var save_select_overlay: SaveSelectOverlay # 选档界面·3 存档位(新档/进档/删档——营地交互重做卡③;登记生涯同档) -var debug_overlay: DebugOverlay # 调试浮窗(种子/数值调整/批量模拟/两层调试入口收一) -var debug_badge: Button # 调试浮层入口角标(UI 最顶层浮层——LAYER_CONTRACT 末位 float 类,任何 UI 不遮挡) - -var _modal_flow = null # 模态编排纯逻辑类(modal_flow.gd——依赖仅 ModalStack+查询 Callable) -var _modals = null # 模态层级状态机(=_modal_flow.stack——探针 _list 登记序断言缝保留) var _tune_sim = null # 调试浮窗批量模拟控制器(tune_sim_controller.gd) -# 结算框挂起态属性化(探针/冒烟直读写接缝语义零变化——读=armed,写=arm/disarm;计时器随会话住 -# run_scope.settlement_stop_timer) -var _settlement_stop_pending: bool: - get: - return run_scope.settlement_stop_timer.is_armed() if run_scope != null else false - set(v): - if run_scope == null: - return - if v: - run_scope.settlement_stop_timer.arm() - else: - run_scope.settlement_stop_timer.disarm() - -# 锁期点击重放(v3.1):延迟一帧重入唯一动作通道——此刻队列空锁已解,按普通动作受理 -#(局面在演播期无变化:逻辑先行、演播只是回放——点击语义保真;_row_queue=RowPacer 委托面, -# 排播泵本体在 row_pacer.gd)。 -var _deferred_modal: Control: # 演播后开窗挂起只读视图(t17_defer_probe 挂起态断言缝;本体归 _modal_flow) - get: - return _modal_flow._deferred if _modal_flow != null else null - - -# 演播后排窗(伤害过程演完才切结算窗口):排播队列在播=挂起,演播完边沿 -# (_on_pump_ended 派发)再开——旧路径 _open_modal 前置 flush 会把击杀轮的伤害/死亡演出 -# 同帧倒掉(「直接跳战斗胜利」缺陷本体)。队列空=立即开(行为同旧路径)。 -func _open_modal_after_playback(m: Control) -> void: - _modal_flow.open_after_playback(m) # 演播后排窗挂起编排归 ModalFlow - - -# 演播完边沿派发(row_pacer.pump_ended 注入口——先开挂起 modal,后锁期点击重放): -func _on_pump_ended() -> void: - _modal_flow.pump_ended() # 派发编排归 ModalFlow(重放经注入 Callable 回本类) - func _replay_locked_click() -> void: - if run_scope.locked_click.is_empty() or not _row_queue.is_empty(): + if run_scope.locked_click.is_empty() or not ui.screens.row_pacer.pending_rows().is_empty(): return var click: Dictionary[Variant, Variant] = run_scope.locked_click run_scope.locked_click = {} @@ -173,7 +70,7 @@ func _replay_locked_click() -> void: # 会话生命周期:_ready 建闲时态(永非 null——全组合根零空判责), # 开局/续玩/调试两层各建新会话,旧会话整节点 queue_free()(帧末回收;新节点先挂树无真空窗)。 func _begin_session(adventure_session: bool) -> void: - _row_pacer.flush() # 会话更替即清队(待播行不住局态——机制保证,不靠时序约定) + ui.screens.row_pacer.flush() # 会话更替即清队(待播行不住局态——机制保证,不靠时序约定) if run_scope != null: run_scope.queue_free() run_scope = RunScope.new() @@ -194,10 +91,11 @@ func _ready() -> void: settings_store = SettingsStore.new() settings_store.load_settings() settings_store.apply() + ui = UiRegistry.new() # 注册面先于装配建(_build_ui 八步向容器登记——装配时序零变化) _build_ui() _begin_session(false) # 闲时会话(gp_ledger 等探针注入面自 _ready 起恒在——ui_probe 合成段时序) - _start_overlay.visible = true - debug_badge.visible = _debug_entry # 浮层角标可见性须门控落定后赋值(构建/连接在 _build_ui);_debug_entry 缺省 false=发布态恒隐 + ui.screens.start_overlay.visible = true + ui.screens.debug_badge.visible = _debug_entry # 浮层角标可见性须门控落定后赋值(构建/连接在 _build_ui);_debug_entry 缺省 false=发布态恒隐 # boot 菜单优先——启动不自动开浮窗(开始屏不得被调试面板盖住); # 调试入口=UI 最顶层全局浮层角标(LAYER_CONTRACT 末位 float 类,任何 UI 不遮挡) @@ -206,94 +104,13 @@ func _ready() -> void: func _build_ui() -> void: MainUiBuild.build(self) - debug_overlay.diagnose_requested.connect(_on_diagnose_pressed) # 诊断转储入口 + ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed) # 诊断转储入口 func _fatal_screen(msg: String) -> void: MainUiBuild.fatal_screen(self, msg) -# ==================== 模态 overlay 层级管理(pattern 6 / 缺陷⑤——状态机归 modal_stack.gd) ==================== - -func _register_modal(m: Control, tags: Array = []) -> void: - # 撑满宿主(Control 默认锚 = 0×0——全屏变暗层与居中布局整体失效;route 选路层预览截图实证后 - # 统一收口:六个模态同病,探针经信号直发绕过真实点击从未暴露。anchors_and_offsets 而非 anchors - # ——后者会把控件重置回最小内容尺寸,ui_assembler 头注同款纪律) - m.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) - _modal_flow.register(m, tags) # 排除组随层登记(non_locking=调试浮窗/screen_state=关卡地图屏) - - -# 合同层寻址(key→节点;纯查表不含层序语义——层序唯一事实源=LAYER_CONTRACT) -func _layer_node(key: String) -> Node: - match key: - "adventure_screen": return _adventure_screen - "menu_screen": return _menu_screen - "row_pacer": return _row_pacer - "legend_overlay": return legend_overlay - "node_overlay": return node_overlay - "level_screen": return level_screen - "curio_overlay": return curio_overlay - "end_overlay": return _end_overlay - "shop_overlay": return shop_overlay - "camp_screen": return _camp_screen - "camp_overlay": return camp_overlay - "settings_overlay": return settings_overlay - "status_panel": return status_panel - "career_overlay": return career_overlay - "save_select_overlay": return save_select_overlay - "inter_overlay": return _inter_overlay - "confirm_dialog": return confirm_dialog - "debug_overlay": return debug_overlay - "debug_badge": return debug_badge - return null - - -# 合同装配(_build_ui 尾调用):按 LAYER_CONTRACT 序统一落位——直接子层 move_child 到合同槽、 -# 模态层按合同序登记;宿住层(host)树序随宿主只登记;成员层(shell)与屏壳同槽(屏壳占树位、成员占登记位)。 -# 本函数是全仓唯一层序落位点(move_child 单口)——改层序只改 LAYER_CONTRACT。 -func _apply_layer_contract() -> void: - var slot := 0 - for entry: Dictionary in LAYER_CONTRACT: - var tags: Array[Variant] = entry.get("tags", []) - if entry.has("shell"): - var shell: Node = _layer_node(String(entry["shell"])) - var member: Node = _layer_node(String(entry["key"])) - if shell != null: - move_child(shell, slot) - slot += 1 - if member != null: - _register_modal(member as Control, tags) - else: - push_error("[合同装配] 层缺失: %s" % String(entry["key"])) - continue - var node: Node = _layer_node(String(entry["key"])) - if node == null: - push_error("[合同装配] 层缺失: %s" % String(entry["key"])) - continue - if entry.has("host"): - _register_modal(node as Control, tags) - continue - move_child(node, slot) - slot += 1 - if String(entry["kind"]) == "modal": - _register_modal(node as Control, tags) - - -func _open_modal(m: Control) -> void: - _modal_flow.open(m) # 挂起兜底/排播排空/遮挡记账/开侧同步编排归 ModalFlow - -func _close_modal(m: Control) -> void: - _modal_flow.close(m) # 关闭恢复+关侧同步归 ModalFlow(同上) - -func _on_modal_closed(m: Control) -> void: - _close_modal(m) - - -func _input_locked() -> bool: - # 模态/非战斗期棋盘输入锁(判据编排归 ModalFlow.locks_input:互斥域随层登记 - # [non_locking=调试浮窗]+ 三例外经查询 Callable 注入;本函数留探针缝) - return _modal_flow.locks_input() - # ==================== 冒险模式入口(唯一试玩入口)==================== func _start_adventure_from_menu() -> void: @@ -301,7 +118,7 @@ func _start_adventure_from_menu() -> void: # 弹窗本体与两分支逻辑归卡④大门承接)。本函数为探针直调中间态保留(mn10——冒险段探针 # 直调位点逐个登记实现报告「已知中间态」;卡④到位后由大门两分支换代)。无激活态写盘拒绝 # 由卡① sv3 承接(结算不落盘)。 - _start_overlay.visible = false + ui.screens.start_overlay.visible = false _start_adventure() @@ -309,16 +126,16 @@ func _start_adventure_from_menu() -> void: #(确认才删、取消只关窗——裁决 3)。"new_adventure" 消费方已随卡② 主菜单确认链摘除退役 #(mn6/V5——主菜单零确认残留),分派机制保留(卡④ 到位接入第三消费方不返工)。 func _on_confirm_accepted() -> void: - _close_modal(confirm_dialog) # 模态栈单口 + ui.modals.flow.close(ui.modals.confirm_dialog) # 模态栈单口 match _confirm_consumer: "delete_slot": save_manager.slots.delete_slot(_confirm_consumer_slot) - save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态) + ui.modals.save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态) _confirm_consumer = "" func _on_confirm_declined() -> void: - _close_modal(confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删);关侧恢复归 ModalFlow + ui.modals.flow.close(ui.modals.confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删);关侧恢复归 ModalFlow _confirm_consumer = "" @@ -334,16 +151,16 @@ func _resume_from_menu() -> void: if not save_manager.has_breakpoint(): return var bp: Dictionary[Variant, Variant] = save_manager.breakpoint_snapshot() - _close_modal(debug_overlay) # 开冒险路径显式关浮窗(续玩同口径) - _start_overlay.visible = false + ui.modals.flow.close(ui.modals.debug_overlay) # 开冒险路径显式关浮窗(续玩同口径) + ui.screens.start_overlay.visible = false _begin_session(true) _wire_adventure() run_scope.level_active = true _sync_status_entry() # 续玩恢复点亮状态面板入口角标 - level_screen.set_abandon_available(true) - log_view.clear() + ui.modals.level_screen.set_abandon_available(true) + ui.battle.log_view.clear() logger.start_session(int(bp.get("master_seed", 0))) - run_scope.adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct()) + run_scope.adventure.set_hero_override(ui.modals.debug_overlay.atk_pct(), ui.modals.debug_overlay.hp_pct()) # 同调三键随续玩下传(build_adventure_params 统一构造,与开新局同源同口径) if not run_scope.adventure.resume_adventure(bp, build_adventure_params(save_manager, meta, {"treasure_unlocked": _treasure_ui.unlocked_set()})): @@ -379,16 +196,16 @@ static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref: # 冒险模式(三关连探——AdventureRunner 编排;转发信号签名与 runner 一致,接线照原样对接; # 信号连接只在组合根) func _start_adventure() -> void: - _close_modal(debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开) + ui.modals.flow.close(ui.modals.debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开) _begin_session(true) _wire_adventure() run_scope.level_active = true _sync_status_entry() # 进对局点亮状态面板入口角标 - level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期) - log_view.clear() + ui.modals.level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期) + ui.battle.log_view.clear() var seed_val := _seed_value() logger.start_session(seed_val) - run_scope.adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct()) + run_scope.adventure.set_hero_override(ui.modals.debug_overlay.atk_pct(), ui.modals.debug_overlay.hp_pct()) # 永久强化注入:冒险开始参数面——meta 现算 override 显式传参(全零 = 白板) # 开局参数面(同调三键同源构造——确定性承载=固定种子+参数档,首局教学图由种子域三分判定给 42) if not run_scope.adventure.start_adventure(seed_val, build_adventure_params(save_manager, meta, @@ -435,33 +252,33 @@ func _on_inter_next_pressed() -> void: func _show_board_fx_for_node(node_type: String = "") -> void: # Boss 遭遇变体:紫赤影子爬棋盘一次性占位演出(§2.6) if node_type == "boss": - board_zone.fx.play_boss_shadow() + ui.battle.board_zone.fx.play_boss_shadow() func _on_battle_finished(payload: Dictionary) -> void: # 仅调试模式接线(冒险的关卡结束归 AdventureRunner._on_level_finished——大奖励/冒险终局分派) var result: String = String(payload.get("result", "")) _sync_settlement_visuals() # → 全关(battle_over 反馈面) - HudPresenter.battle_end_feedback(stage, ap_bar, true) # 灰化/idle 回正/RAP 清零/状态全清——HUD 单通道 + HudPresenter.battle_end_feedback(ui.battle.stage, ui.battle.board_zone.ap_bar, true) # 灰化/idle 回正/RAP 清零/状态全清——HUD 单通道 var decision: Dictionary[Variant, Variant] = _debug.on_battle_finished(payload) match String(decision.outcome): "next_layer": - _inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [ + ui.screens.inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [ int(decision.layer), int(decision.carry_hp), int(db.combat.hero.max_hp)] - _inter_overlay.visible = true + ui.screens.inter_overlay.visible = true _refresh_presentations() return "level_end": - _end_overlay.open_victory(Narrator.battle_end_text("victory", true), - "返回", board_view.get_global_rect(), _end_victory_opts(payload)) + ui.modals.end_overlay.open_victory(Narrator.battle_end_text("victory", true), + "返回", ui.battle.board_zone.board_view.get_global_rect(), _end_victory_opts(payload)) "standalone_end": if result == "victory": - _end_overlay.open_victory(Narrator.battle_end_text("victory", false), - "回到遭遇选择", board_view.get_global_rect(), _end_victory_opts(payload)) + ui.modals.end_overlay.open_victory(Narrator.battle_end_text("victory", false), + "回到遭遇选择", ui.battle.board_zone.board_view.get_global_rect(), _end_victory_opts(payload)) else: - _end_overlay.open_defeat(Narrator.battle_end_text("defeat", false), - "返回", board_view.get_global_rect()) - _open_modal_after_playback(_end_overlay) + ui.modals.end_overlay.open_defeat(Narrator.battle_end_text("defeat", false), + "返回", ui.battle.board_zone.board_view.get_global_rect()) + ui.modals.flow.open_after_playback(ui.modals.end_overlay) _refresh_presentations() @@ -499,22 +316,17 @@ func _on_step_row(row: Dictionary) -> void: # 清图奖励可感知反馈(挖完全图须有奖励感——clear_blast 兑付对敌伤害 # 零可见,toast 桥接归组合根呈现层;零兑付(未生效)不弹安静留明细带) if String(row.get("source_class", "")) == "clear_reward" and String(row.get("type", "")) != "reward_ineffective": - board_zone.toast.show_toast("清图奖励:%s" % String(row.get("display", "")).replace("(清图奖励)", ""), "info") + ui.battle.board_zone.toast.show_toast("清图奖励:%s" % String(row.get("display", "")).replace("(清图奖励)", ""), "info") # 排播入口:减动效=延迟归零同帧直落(行呈现不跳过);常态入队按段阶梯播放 if UiKit.motion_scale < 1.0: - _row_pacer.consume_now(row) + ui.screens.row_pacer.consume_now(row) return - _row_pacer.enqueue(row) + ui.screens.row_pacer.enqueue(row) # 段间延迟取数面(减动效双态断言用——pacing_queue_test 直读;委托 row_pacer 同名面) func pace_delay_for(prev_seg: String, next_seg: String) -> float: - return _row_pacer.pace_delay_for(prev_seg, next_seg) - - -# 强制排空(模态开启前调用——明细回看/停点覆盖层/终局面所见明细带恒完整;pacing_queue_test 直读) -func _flush_row_queue() -> void: - _row_pacer.flush() + return ui.screens.row_pacer.pace_delay_for(prev_seg, next_seg) func _on_round_started(ri: int) -> void: @@ -522,10 +334,10 @@ func _on_round_started(ri: int) -> void: #(view 内判减动效=静态化变体)。减动效快路=分隔行同帧直落(不跳过——consume_now 复用 # _consume_row 轮首分隔行分支)。 if UiKit.motion_scale < 1.0: - _row_pacer.consume_now({"__round_line": ri}) + ui.screens.row_pacer.consume_now({"__round_line": ri}) else: - _row_pacer.enqueue({"__round_line": ri}) - settlement_fx.play_round_banner(ri) + ui.screens.row_pacer.enqueue({"__round_line": ri}) + ui.battle.settlement_fx.play_round_banner(ri) # 秘宝呈现同步:每战首轮装配链 # 显式重算〔🔴 契约复原语义:下一战装配链重算即含「未装配→写 0」复原〕。 # 挂点时序依据:round_started 首轮发射位在 begin_battle 尾(attunement 注入之后)。 @@ -534,7 +346,7 @@ func _on_round_started(ri: int) -> void: _sync_treasure_glow() # 巢穴预告呈现(装板报数量读数口消费:≥0=本战装配命中,toast 一次性提示) if run_scope.settlement != null and int(run_scope.settlement.nest_preview_count) >= 0: - board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(run_scope.settlement.nest_preview_count), "info") + ui.battle.board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(run_scope.settlement.nest_preview_count), "info") # 开战横幅——整图地形变异预告:首轮快照 board_variants # 非空才发射(Boss 图恒带一型=替换制;普通图空快照零发射);轮横幅下方叠行,view 内判减动效。 if ri == 1 and run_scope.settlement != null: @@ -542,7 +354,7 @@ func _on_round_started(ri: int) -> void: if not bvs.is_empty(): var vlines: Array[String] = HudPresenter.board_variant_lines(db, bvs) if not vlines.is_empty(): - settlement_fx.play_board_variant_banner("地形变异 · " + " · ".join(vlines)) + ui.battle.settlement_fx.play_board_variant_banner("地形变异 · " + " · ".join(vlines)) _refresh_presentations() @@ -552,19 +364,19 @@ func _on_board_changed() -> void: run_scope.settlement = run_scope.runner.current_battle() if run_scope.settlement == null: return - board_view.rebuild(run_scope.settlement.minesweeper.current_board(), run_scope.settlement.minesweeper) + ui.battle.board_zone.board_view.rebuild(run_scope.settlement.minesweeper.current_board(), run_scope.settlement.minesweeper) _check_board_hints() _refresh_presentations() # 秘宝视野呈现桥:阔视野带 light_radius_bonus -# 装配参数合计显式写给 board_view.set_fog_glow_bonus(装配=半径 8+4 / 未装配=复原 0),写入即 setter +# 装配参数合计显式写给 ui.battle.board_zone.board_view.set_fog_glow_bonus(装配=半径 8+4 / 未装配=复原 0),写入即 setter # 内 refresh 全板重算雾态。调用点=每战首轮装配链(_on_round_started ri==1)——纯呈现边界 🔴 契约 2: # 只写呈现态参数,minesweeper 结算零触碰;off 态(未装配)写 0=缺省基线。 func _sync_treasure_glow() -> void: - if run_scope.settlement == null or board_view == null: + if run_scope.settlement == null or ui.battle.board_zone.board_view == null: return - board_view.set_fog_glow_bonus(run_scope.settlement.attune_param_sum("light_radius_bonus")) + ui.battle.board_zone.board_view.set_fog_glow_bonus(run_scope.settlement.attune_param_sum("light_radius_bonus")) # ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ---- @@ -575,10 +387,10 @@ func _check_board_hints() -> void: return # 零合法板(棋盘生成 EC5 降级)诚实提示——非静默(toast,非模态) if board.illegal_product: - board_zone.toast.show_toast("本板为零合法板降级产物——出图器已尽力,数据面诚实告知", "warn") + ui.battle.board_zone.toast.show_toast("本板为零合法板降级产物——出图器已尽力,数据面诚实告知", "warn") # 接受最优降级(有合法板但无一达标)——降级标记独立于非法产物标记,同走诚实告知 elif board.degraded_product: - board_zone.toast.show_toast("本板为接受最优降级产物(未达可推理率及格线)——数据面诚实告知", "warn") + ui.battle.board_zone.toast.show_toast("本板为接受最优降级产物(未达可推理率及格线)——数据面诚实告知", "warn") func _on_hover_forbidden(_cell: Vector2i) -> void: @@ -594,7 +406,7 @@ func _on_chord_attempted(_cell: Vector2i) -> void: func _show_hint(text: String, hint_id: String) -> void: run_scope.hints_shown[hint_id] = true logger.log_ux("ux_hint_shown", {"hint_id": hint_id}) - board_zone.toast.show_toast(text, "info") + ui.battle.board_zone.toast.show_toast(text, "info") # ---- 节点覆盖层"继续"(奖励/恢复点兑付确认)与节点图选路覆盖层 ---- @@ -706,15 +518,15 @@ func _on_wallet_changed(event: String, gp: int, extra: Dictionary)-> void: func _on_board_action(cell: Vector2i, kind: String) -> void: # 玩家动作 → 核心写调用的唯一通道(模态/无战斗拒绝在此收口) - if _input_locked(): + if ui.modals.flow.locks_input(): return # 结算演播期棋盘冻结(v3 口径——不可点/不可移/不可挖): # 锁面=排播队列活跃(纯队列判据;排播泵尾迹窗口不可作演播中标志——见 row_pacer._process 注)。 # 队列排空即解锁、无状态残留;HUD 按钮不在锁面(停手/明细回看各自通路,开启前 - # _flush_row_queue 自行排空)。 + # 开层前排空钩子(flow.flush_rows——模态开启前明细带恒完整)。 # **点击不丢弃**(v3.1——「卡死」体感根因):锁期点击缓存最后一次,演播完边沿 # (_process 泵 _was_pumping 分支)自动重放——玩家操作零丢失、体感=「演播一停动作就生效」。 - if not _row_queue.is_empty(): + if not ui.screens.row_pacer.pending_rows().is_empty(): run_scope.locked_click = {"cell": cell, "kind": kind} return # 秘宝选中态劫持(两步交互第二步):点格=使用目标拾取,不落挖格; @@ -734,22 +546,22 @@ func _on_board_action(cell: Vector2i, kind: String) -> void: # settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 settlement 已被 # runner 置空,清图演出判定须先判 settlement(重入隐患守卫) if accepted and kind == "dig" and run_scope.settlement != null and run_scope.settlement.minesweeper.current_board() != null and run_scope.settlement.minesweeper.clear_map_cleared(): - board_zone.fx.play_clear_burst() + ui.battle.board_zone.fx.play_clear_burst() if accepted and kind == "dig" and run_scope.settlement != null and run_scope.settlement.minesweeper != null: # 挖掘演出:挥铲三帧 + 角色弹跳——触发=格子点开那一刻 #(settle_batch 同步受理即状态已变,动画纯表现与 refresh 同拍零竞态; # 挖掘不移动英雄,出手方向 = 目标格−英雄格,圆心=英雄持握手位) var hero_cell: Vector2i = run_scope.settlement.minesweeper.hero_pos() - board_zone.fx.play_shovel_swing(board_view.hero_hand_point(), cell - hero_cell, board_view.hero_sprite_scale()) - board_view.play_hero_dig_bounce() + ui.battle.board_zone.fx.play_shovel_swing(ui.battle.board_zone.board_view.hero_hand_point(), cell - hero_cell, ui.battle.board_zone.board_view.hero_sprite_scale()) + ui.battle.board_zone.board_view.play_hero_dig_bounce() elif accepted and kind == "move": - board_view.notify_hero_move() # 原地移动(目标=当前格幂等受理)帧闪回执——refresh diff 前置告知 - board_view.refresh() + ui.battle.board_zone.board_view.notify_hero_move() # 原地移动(目标=当前格幂等受理)帧闪回执——refresh diff 前置告知 + ui.battle.board_zone.board_view.refresh() _refresh_presentations() func _on_stop_pressed() -> void: - if _input_locked(): + if ui.modals.flow.locks_input(): return # stop 经批次提交(排雷玩法 D1×6①——不直调 行动经济) if run_scope.settlement == null: @@ -758,15 +570,15 @@ func _on_stop_pressed() -> void: logger.log_action("stop", Vector2i(-1, -1), not outcomes.is_empty() and bool(outcomes[0].accepted)) if not outcomes.is_empty() and String(outcomes[0].get("outcome", "")) == "voided": # voided ≠ 拒绝——独立文案(pattern 1:防静默改判读感欺骗) - board_zone.toast.show_toast("本轮已清图,停手未执行 · 剩余 AP 已作废", "warn") + ui.battle.board_zone.toast.show_toast("本轮已清图,停手未执行 · 剩余 AP 已作废", "warn") logger.log_ux("ux_reject", {"source": "voided", "sub_reason": "stop_voided"}) - board_view.refresh() + ui.battle.board_zone.board_view.refresh() _refresh_presentations() func _on_refresh_confirmed() -> void: # 弃图刷新(挖掘期 Active 当场换图继续挖——D4;RAP 按门槛扣费 · 旧板封印作废,每轮上限读表) - if _input_locked(): + if ui.modals.flow.locks_input(): return var ok: bool = false if run_scope.level_active and run_scope.runner != null: @@ -776,7 +588,7 @@ func _on_refresh_confirmed() -> void: logger.log_action("refresh", Vector2i(-1, -1), ok) if ok: logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_snapshot()}) - board_zone.toast.show_toast("RAP−%d · 旧板封印作废——新图已就位,AP 保留继续挖" % int(db.run.action_economy.rap_refresh_cost), "gold") + ui.battle.board_zone.toast.show_toast("RAP−%d · 旧板封印作废——新图已就位,AP 保留继续挖" % int(db.run.action_economy.rap_refresh_cost), "gold") else: # 资源面/上限拒绝(EC13 合法拒绝可见;行动经济 无载荷——客户端按上下文推断) var rap: int = int(run_scope.settlement.presentation_snapshot().get("rap", 0)) if run_scope.settlement != null else 0 @@ -784,11 +596,11 @@ func _on_refresh_confirmed() -> void: var result := "rejected_rap" if rap < cost else "rejected_cap" logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_snapshot() + 1}) if result == "rejected_rap": - board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn") - ap_bar.pulse_rap() + ui.battle.board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn") + ui.battle.board_zone.ap_bar.pulse_rap() else: - board_zone.toast.show_toast("本轮刷新次数已用完(每轮 2 次)", "warn") - board_view.refresh() + ui.battle.board_zone.toast.show_toast("本轮刷新次数已用完(每轮 2 次)", "warn") + ui.battle.board_zone.board_view.refresh() _refresh_presentations() @@ -798,9 +610,9 @@ func _handle_outcome_feedback(outcome: Dictionary) -> void: return logger.log_ux("ux_reject", {"source": String(outcome.get("reject_reason", "")), "sub_reason": String(outcome.get("sub_reason", ""))}) - board_zone.toast.show_toast(HudPresenter.outcome_text(outcome), "warn") + ui.battle.board_zone.toast.show_toast(HudPresenter.outcome_text(outcome), "warn") if String(outcome.get("reject_reason", "")) == "resource_face": - ap_bar.pulse_ap() + ui.battle.board_zone.ap_bar.pulse_ap() # ==================== 全屏 States 状态机(挖掘/结算/窗口/终局)==================== @@ -816,7 +628,7 @@ func _refresh_used_snapshot() -> int: func _refresh_presentations() -> void: # 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查) var refresh_remaining: int = maxi(0, int(db.run.action_economy.refresh_cap_per_round) - _refresh_used_snapshot()) - HudPresenter.refresh(stage, info_panel, ap_bar, {"settlement": run_scope.settlement, "runner": run_scope.runner, "level_active": run_scope.level_active, "db": db, "refresh_remaining": refresh_remaining}) + HudPresenter.refresh(ui.battle.stage, ui.battle.info_panel, ui.battle.board_zone.ap_bar, {"settlement": run_scope.settlement, "runner": run_scope.runner, "level_active": run_scope.level_active, "db": db, "refresh_remaining": refresh_remaining}) _treasure_ui.refresh_slots() # C 区秘宝槽重查(试炼镐积点进度随挖格刷新悬停浮窗) _sync_settlement_visuals() @@ -825,9 +637,9 @@ func _sync_settlement_visuals() -> void: # 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开 84px var settling := _is_settling() if settling and not run_scope.was_settling: - settlement_fx.play_round_sweep(board_view.get_global_rect()) # 结算轮光带——时序敏感演出触发,例外留在组合根 + ui.battle.settlement_fx.play_round_sweep(ui.battle.board_zone.board_view.get_global_rect()) # 结算轮光带——时序敏感演出触发,例外留在组合根 run_scope.was_settling = settling - HudPresenter.sync_settlement_visuals(stage, board_view, log_view, settling, + HudPresenter.sync_settlement_visuals(ui.battle.stage, ui.battle.board_zone.board_view, ui.battle.log_view, settling, HudPresenter.phase_text(run_scope.settlement, run_scope.runner, run_scope.boss_active, db)) @@ -836,13 +648,13 @@ func _is_settling() -> bool: # 判据若只跟逻辑态,演播期棋盘会提前恢复可挖观感=「没意识到在结算演出」感知缺口根源; # 演播期维持去饱和+禁挖光(结算中语义),放完自然回挖掘认知。判据=纯队列(排播泵尾迹 # 窗口不可作演播中标志——row_pacer._process 注)。battle 收尾演播例外: - # overlay 开启即 _flush_row_queue 排空,视觉随队列空同步交接结算框。 + # overlay 开启经 flow.flush_rows 钩子排空,视觉随队列空同步交接结算框。 # 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透)。 if run_scope.settlement == null or not run_scope.settlement.is_battle_live(): - return not _row_queue.is_empty() + return not ui.screens.row_pacer.pending_rows().is_empty() if String(run_scope.settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING: return true - return not _row_queue.is_empty() + return not ui.screens.row_pacer.pending_rows().is_empty() # 相位行 —— 承接体见 HudPresenter.phase_text(caller 直接改引) @@ -908,8 +720,8 @@ func _seed_value() -> int: # 重放模板 0 即在此种子下标定〕;置位 true=教学局已收束的非首局走 ③ 随机); # ③非首局空/非法框=引擎全局熵 randi()(Godot 4 启动自播种子,headless 同;不走 rng_service # 封闭流、禁 randomize()/自建 RNG——组合根建种子属引擎熵豁免面,不进玩法 RNG 消费) - if _seed_edit.text.is_valid_int(): - return int(_seed_edit.text) + if ui.modals.debug_overlay.seed_edit.text.is_valid_int(): + return int(ui.modals.debug_overlay.seed_edit.text) if save_manager.meta_state.career_state().first_adventure_done: return randi() # rng-seed-exempt(非首局空框引擎熵——组合根建种子不进玩法 RNG 消费,static_scan S1/S2 标记豁免) return 42 diff --git a/game/presentation/main_adventure_flow.gd b/game/presentation/main_adventure_flow.gd index 56d2e192..f52a4557 100644 --- a/game/presentation/main_adventure_flow.gd +++ b/game/presentation/main_adventure_flow.gd @@ -30,10 +30,10 @@ static func _on_adventure_level_started(root: Variant, level_index: int, display # 开场行只显示 atk/max_hp 两值,注记指向不存在的显示轴反成失真),白板局不带 var ov: Dictionary[Variant, Variant] = root.meta.hero_override() var boosted: bool = int(ov.get("atk", 0)) > 0 or int(ov.get("max_hp", 0)) > 0 \ - or root.debug_overlay.atk_pct() > 0 or root.debug_overlay.hp_pct() > 0 \ + or root.ui.modals.debug_overlay.atk_pct() > 0 or root.ui.modals.debug_overlay.hp_pct() > 0 \ or root.run_scope.runner.journal().bonus_total("atk") > 0 \ or root.run_scope.runner.journal().bonus_total("max_hp") > 0 - root.log_view.append_bb(root.Narrator.level_open_line(display_name, int(root.run_scope.adventure.master_seed), + root.ui.battle.log_view.append_bb(root.Narrator.level_open_line(display_name, int(root.run_scope.adventure.master_seed), root.run_scope.runner.effective_atk(), root.run_scope.runner.effective_max_hp(), boosted)) # runner 六信号重连(is_connected 守卫——未连即接;守卫失败 push_error=重连恰好一次契约承载) @@ -62,30 +62,30 @@ static func _on_runner_node(root: Variant, node: Dictionary, info: Dictionary) - root.logger.attach_battle(root.run_scope.settlement) root.run_scope.boss_active = t == "boss" root._show_board_fx_for_node(t) - root.log_view.append_bb(root.Narrator.node_banner(root.run_scope.runner, node, t)) + root.ui.battle.log_view.append_bb(root.Narrator.node_banner(root.run_scope.runner, node, t)) elif t == "reward": # 奖励节点三选一(与关卡完成三选一共享同池同权重同上限;CurioPickOverlay 共用面)。 # 呈现编排归 curio_interact_controller.present_node_curio。 root._bonus.present_node_curio() - root.log_view.append_bb(root.Narrator.reward_line()) + root.ui.battle.log_view.append_bb(root.Narrator.reward_line()) elif t == "rest": var healed: int = int(info.get("healed", 0)) - root._open_modal(root.node_overlay) - root.node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0))) - root.log_view.append_bb(root.Narrator.rest_line(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0)))) + root.ui.modals.flow.open(root.ui.modals.node_overlay) + root.ui.modals.node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0))) + root.ui.battle.log_view.append_bb(root.Narrator.rest_line(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0)))) elif t == "shop": # 商店房兑付:到达即开店——商品/货架快照与余额经 runner 只读投影喂入; # 购买/离开归玩家意图(buy_requested/leave_pressed 接线),零自动推进。 # pool 面 = 共享池货架三选一同构卡;legacy 面 = 固定三件(off 对照承载) - root._open_modal(root.shop_overlay) + root.ui.modals.flow.open(root.ui.modals.shop_overlay) if root.run_scope.runner.shop.is_shop_pool(): - root.shop_overlay.open_shelf(root.run_scope.runner.shop.shop_shelf(), int(root.run_scope.runner.gp)) - root.shop_overlay.set_visit_info(int(root.run_scope.runner.shop.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行 - root.shop_overlay.set_refresh_display(root.run_scope.runner.shop.shop_refresh_free_left(), root.run_scope.runner.shop.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop.shop_refresh_cost()) # MC-1 + root.ui.modals.shop_overlay.open_shelf(root.run_scope.runner.shop.shop_shelf(), int(root.run_scope.runner.gp)) + root.ui.modals.shop_overlay.set_visit_info(int(root.run_scope.runner.shop.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行 + root.ui.modals.shop_overlay.set_refresh_display(root.run_scope.runner.shop.shop_refresh_free_left(), root.run_scope.runner.shop.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop.shop_refresh_cost()) # MC-1 else: - root.shop_overlay.open_shop(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp)) - root.shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载) - root.log_view.append_bb(root.Narrator.shop_line(int(root.run_scope.runner.gp))) + root.ui.modals.shop_overlay.open_shop(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp)) + root.ui.modals.shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载) + root.ui.battle.log_view.append_bb(root.Narrator.shop_line(int(root.run_scope.runner.gp))) root.logger.log_node(node, info, root.run_scope.runner.node_index) root._refresh_presentations() @@ -101,7 +101,7 @@ static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void: _exit_level_screen(root) # 终局兜底:独立屏在开态(放弃入口)也先回战斗屏呈现摘要——屏级互斥单一出口 root._sync_status_entry() # 离局熄灭状态面板入口角标(防菜单误现) root._sync_settlement_visuals() # → 全关 - HudPresenter.battle_end_feedback(root.stage, root.ap_bar, false) # 灰化/idle 回正/状态全清——HUD 单通道 + HudPresenter.battle_end_feedback(root.ui.battle.stage, root.ui.battle.board_zone.ap_bar, false) # 灰化/idle 回正/状态全清——HUD 单通道 # 存档入账:settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变—— # 调试两层不经过本处理器,天然不写档不污染生涯数据)。 var detail: Dictionary[String, Variant] = {} @@ -110,30 +110,30 @@ static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void: if not detail.is_empty(): text += "\n" + root.Narrator.keystone_settle_text(detail, root._bonus.item_name_lookup()) if result == "completed": - root._end_overlay.open_victory(text, "返回冒险", root.board_view.get_global_rect(), + root.ui.modals.end_overlay.open_victory(text, "返回冒险", root.ui.battle.board_zone.board_view.get_global_rect(), {"gp_summary": "本局 GP 入账 · 合计 %d" % int(summary.get("gp_final", 0))}) # GP 行(取数=summary gp_final 清账前快照);钥石明细已在 text(keystone_settle_text)——三行账 else: - root._end_overlay.open_defeat(text, "返回", root.board_view.get_global_rect()) + root.ui.modals.end_overlay.open_defeat(text, "返回", root.ui.battle.board_zone.board_view.get_global_rect()) root.logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷) if root.run_scope.runner != null: root.run_scope.runner.restore_hero_base() - root._open_modal_after_playback(root._end_overlay) + root.ui.modals.flow.open_after_playback(root.ui.modals.end_overlay) root._refresh_presentations() static func _on_battle_end_continue(root: Variant) -> void: # 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石在收束时入存档) - root._close_modal(root._end_overlay) - root._end_overlay.close() + root.ui.modals.flow.close(root.ui.modals.end_overlay) + root.ui.modals.end_overlay.close() # 三选一停点挂起分支:战斗总结结算(Boss 胜利钥石增量行)确认后进三选一面 if root._bonus.resume_if_pending(): # 三选一停点在途恢复入口(curio_interact_controller) return # 挂起分派:战斗胜利总结结算框的「下一节点」≠ 终局——进选路页(暂存候选恢复呈现) - if root._settlement_stop_pending: - root._settlement_stop_pending = false + if root.run_scope.settlement_stop_timer.is_armed(): + root.run_scope.settlement_stop_timer.disarm() _enter_level_screen(root) - root.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held()))) - root.level_screen.open_choices(root.run_scope.pending_candidates, root.run_scope.runner) - root.level_screen.apply_status(level_map_status_payload(root.run_scope.runner)) + root.ui.modals.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held()))) + root.ui.modals.level_screen.open_choices(root.run_scope.pending_candidates, root.run_scope.runner) + root.ui.modals.level_screen.apply_status(level_map_status_payload(root.run_scope.runner)) root.run_scope.pending_candidates = [] root._refresh_presentations() return @@ -141,12 +141,12 @@ static func _on_battle_end_continue(root: Variant) -> void: # 局态封印/账本/模态挂起全复位(局态字段清单见 run_scope.gd) root._debug.reset() root._begin_session(false) - root.debug_overlay.reset_sliders() - root._start_overlay.visible = true + root.ui.modals.debug_overlay.reset_sliders() + root.ui.screens.start_overlay.visible = true root._refresh_presentations() static func _on_inter_next_pressed(root: Variant) -> void: - root._inter_overlay.visible = false + root.ui.screens.inter_overlay.visible = false if root.run_scope.is_adventure: # 冒险模式:大奖励屏确认 → 进下一关(AdventureRunner 序列推进——零自动推进的冒险级停点) if root.run_scope.adventure != null and not root.run_scope.adventure.continue_to_next_level(): @@ -158,7 +158,7 @@ static func _on_inter_next_pressed(root: Variant) -> void: static func _on_node_continue(root: Variant) -> void: - root._close_modal(root.node_overlay) + root.ui.modals.flow.close(root.ui.modals.node_overlay) if root.run_scope.runner != null: # 确认 ≠ 推进——兑付确认后经 choices_presented 弹出选路口 root.run_scope.runner.continue_after_node() @@ -167,33 +167,33 @@ static func _on_choices_presented(root: Variant, candidates: Array) -> void: # 零自动推进的停点呈现:整幅节点图 + 图上点击选路(战斗胜利/兑付确认统一到达此处) if root.run_scope.runner == null: return - root.log_view.append_bb(root.Narrator.route_line(candidates, root.run_scope.runner)) + root.ui.battle.log_view.append_bb(root.Narrator.route_line(candidates, root.run_scope.runner)) # 战斗胜利总结结算(battle.md OQ14):战斗胜利停点先弹 BattleEndOverlay # 胜利形态(金蜡封+GP 增量+总量+判定行+「下一节点」+明细回看),确认后进选路页。判据 = 战斗节点 # 的选路停点且战斗已收场(battle 收尾分支 _settlement 置 null 先于本信号;入口/兑付/商店停点 # 的当前节点非 battle 型——三态天然分流)。Boss 胜/失败走 level_finished 另路不经此处。 if root.run_scope.is_adventure and root.run_scope.runner.current_battle() == null \ and String(root.run_scope.runner.current_node().get("type", "")) == "battle" and not candidates.is_empty(): - root._settlement_stop_pending = true + root.run_scope.settlement_stop_timer.arm() root.run_scope.pending_candidates = candidates.duplicate() - root._end_overlay.open_victory(root.Narrator.battle_end_text("victory", false), "下一节点", - root.board_view.get_global_rect(), + root.ui.modals.end_overlay.open_victory(root.Narrator.battle_end_text("victory", false), "下一节点", + root.ui.battle.board_zone.board_view.get_global_rect(), {"gp_summary": "战利品 GP +%d · 合计 %d" % [root.run_scope.last_gain_amount, int(root.run_scope.runner.gp)]}) - root._open_modal_after_playback(root._end_overlay) + root.ui.modals.flow.open_after_playback(root.ui.modals.end_overlay) root._refresh_presentations() return _enter_level_screen(root) - root.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held()))) - root.level_screen.open_choices(candidates, root.run_scope.runner) - root.level_screen.apply_status(level_map_status_payload(root.run_scope.runner)) + root.ui.modals.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held()))) + root.ui.modals.level_screen.open_choices(candidates, root.run_scope.runner) + root.ui.modals.level_screen.apply_status(level_map_status_payload(root.run_scope.runner)) root._refresh_presentations() static func _on_route_chosen(root: Variant, node_id: int) -> void: # 视图已按节点类型定去留——战斗/Boss 出屏(visible=false,此处收栈 # 记录一致性+回战斗屏);奖励/恢复/商店本屏保持激活(兑付覆盖层由节点事件域内弹出,模态栈 # 打开低层时自动隐藏本屏、关闭自动恢复) - if not root.level_screen.visible: - root._close_modal(root.level_screen) + if not root.ui.modals.level_screen.visible: + root.ui.modals.flow.close(root.ui.modals.level_screen) _exit_level_screen(root) if root.run_scope.runner != null and not root.run_scope.runner.pick_next_node(node_id): push_error("[组合根] 选路被拒(编号 %d 不在候选)" % node_id) @@ -202,15 +202,15 @@ static func _on_route_chosen(root: Variant, node_id: int) -> void: static func _enter_level_screen(root: Variant) -> void: root.run_scope.level_screen_active = true - root._battle_root.visible = false - root._open_modal(root.level_screen) + root.ui.screens.battle_root.visible = false + root.ui.modals.flow.open(root.ui.modals.level_screen) root._sync_status_entry() # 屏级互斥切换同步(_open_modal 单口不识屏级状态) static func _exit_level_screen(root: Variant) -> void: root.run_scope.level_screen_active = false - root._battle_root.visible = true - if root.level_screen.visible: - root._close_modal(root.level_screen) + root.ui.screens.battle_root.visible = true + if root.ui.modals.level_screen.visible: + root.ui.modals.flow.close(root.ui.modals.level_screen) root._sync_status_entry() # 同上 @@ -251,7 +251,7 @@ static func _on_abandon_requested(root: Variant) -> void: return if not root.run_scope.adventure.abandon(): root.logger.log_ux("ux_reject", {"source": "abandon", "sub_reason": "battle_in_flight"}) - root.board_zone.toast.show_toast("战斗进行中——无法放弃", "warn") + root.ui.battle.board_zone.toast.show_toast("战斗进行中——无法放弃", "warn") # ---- 营地覆盖层与钥石账本(写调用唯一归组合根)---- diff --git a/game/presentation/main_debug_flow.gd b/game/presentation/main_debug_flow.gd index ae2b77a4..7114ca29 100644 --- a/game/presentation/main_debug_flow.gd +++ b/game/presentation/main_debug_flow.gd @@ -13,7 +13,7 @@ static func _on_diagnose_pressed(root: Variant) -> void: if root.run_scope.settlement != null: ctx["settlement"] = root.run_scope.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读) var path: String = root.logger.dump_diagnosis(ctx) - root.board_zone.toast.show_toast("已转储诊断: " + path, "info") + root.ui.battle.board_zone.toast.show_toast("已转储诊断: " + path, "info") @@ -21,17 +21,17 @@ static func _show_debug_overlay(root: Variant) -> void: # 门禁:对局中可开;本函数仍是唯一开窗入口,门禁逻辑收函数内: # 棋盘输入锁/状态角标判据排除浮窗, # 遮挡语义=ModalStack 登记制(他模态开时暂隐、关后自动恢复),此处零场景限制 - if root.debug_overlay == null: + if root.ui.modals.debug_overlay == null: return - root._open_modal(root.debug_overlay) + root.ui.modals.flow.open(root.ui.modals.debug_overlay) _sync_debug_overlay(root) # 调试浮窗角标点击(两屏同款处理器——与状态角标同形制守卫;仍收口 _show_debug_overlay 单口) static func _on_debug_badge_pressed(root: Variant) -> void: - var map_on: bool = root.level_screen != null and root.level_screen.visible - if (map_on and root._modal_flow.badge_greyed_map()) or (not map_on and root._modal_flow.badge_greyed_battle()): + var map_on: bool = root.ui.modals.level_screen != null and root.ui.modals.level_screen.visible + if (map_on and root.ui.modals.flow.badge_greyed_map()) or (not map_on and root.ui.modals.flow.badge_greyed_battle()): return _show_debug_overlay(root) @@ -40,10 +40,10 @@ static func _on_debug_badge_pressed(root: Variant) -> void: # 浮窗上下文同步(当局数据刷新+对局中按钮禁用单点):调用面=开窗口与模态开合 # 单口(凡影响开态/对局态的路径必经其一——与 _sync_status_entry 同款纪律) static func _sync_debug_overlay(root: Variant) -> void: - if root.debug_overlay == null: + if root.ui.modals.debug_overlay == null: return - root.debug_overlay.set_context_locked(root.run_scope.settlement != null or root.run_scope.is_adventure) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放) - root.debug_overlay.update_run_data(_debug_run_data(root)) # 当局数据只读区块喂入(开窗+模态开合信号驱动) + root.ui.modals.debug_overlay.set_context_locked(root.run_scope.settlement != null or root.run_scope.is_adventure) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放) + root.ui.modals.debug_overlay.update_run_data(_debug_run_data(root)) # 当局数据只读区块喂入(开窗+模态开合信号驱动) @@ -63,7 +63,7 @@ static func _debug_run_data(root: Variant) -> Dictionary: static func _close_debug_overlay(root: Variant) -> void: - root._close_modal(root.debug_overlay) # 模态栈单口——开始层全程未隐藏,无需恢复 + root.ui.modals.flow.close(root.ui.modals.debug_overlay) # 模态栈单口——开始层全程未隐藏,无需恢复 static func _on_tune_apply(root: Variant, atk_pct: int, hp_pct: int) -> void: # 加成无持久态可"落"——各战斗开局经 board_resolvers.debug_hero_override/runner 现算生效(应用=关闭浮窗) @@ -83,11 +83,11 @@ static func _start_debug_levels(root: Variant) -> void: # settlement/编排态=状态机破坏;模拟在跑时 reset_sliders 会作废测量读数(d5 置灰之外的信号绕过闸) if root.run_scope.is_adventure or root.run_scope.settlement != null or root._tune_sim.is_running(): return - root.debug_overlay.reset_sliders() - root._close_modal(root.debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay) - root._start_overlay.visible = false - root.log_view.clear() - root.log_view.append_bb(root.Narrator.debug_open_line(root._seed_value(), 1, true)) + root.ui.modals.debug_overlay.reset_sliders() + root.ui.modals.flow.close(root.ui.modals.debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay) + root.ui.screens.start_overlay.visible = false + root.ui.battle.log_view.clear() + root.ui.battle.log_view.append_bb(root.Narrator.debug_open_line(root._seed_value(), 1, true)) root._begin_session(false) if not root._debug.start_debug_levels(): root._fatal_screen("战斗启动失败——见输出日志") @@ -100,6 +100,6 @@ static func _on_debug_battle_created(root: Variant, settlement_ref: BattleOrches static func _on_debug_battle_ready(root: Variant, settlement_ref: BattleOrchestrator, _level: int, _carry: int) -> void: root.run_scope.settlement = settlement_ref - root.board_view.rebuild(root.run_scope.settlement.minesweeper.current_board(), root.run_scope.settlement.minesweeper) + root.ui.battle.board_zone.board_view.rebuild(root.run_scope.settlement.minesweeper.current_board(), root.run_scope.settlement.minesweeper) root._check_board_hints() root._refresh_presentations() diff --git a/game/presentation/main_overlay_flow.gd b/game/presentation/main_overlay_flow.gd index e59e3499..57950565 100644 --- a/game/presentation/main_overlay_flow.gd +++ b/game/presentation/main_overlay_flow.gd @@ -5,12 +5,12 @@ extends RefCounted static func _open_settings(root: Variant) -> void: - root._start_overlay.visible = false - root._open_modal(root.settings_overlay) # 模态栈单口 + root.ui.screens.start_overlay.visible = false + root.ui.modals.flow.open(root.ui.modals.settings_overlay) # 模态栈单口 static func _on_settings_closed(root: Variant) -> void: - root._close_modal(root.settings_overlay) # 模态栈单口 - root._start_overlay.visible = true + root.ui.modals.flow.close(root.ui.modals.settings_overlay) # 模态栈单口 + root.ui.screens.start_overlay.visible = true # ---- 局内状态面板·只读(入口角标在两屏装配段;开合与数据喂入归组合根)---- @@ -25,22 +25,22 @@ static func _status_data(root: Variant) -> Dictionary: return HudPresenter.status_data(root.db, root.meta, root.run_scope.runner, root._treasure_ui) static func _open_status_panel(root: Variant) -> void: - root._open_modal(root.status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项) - root.status_panel.open_status(_status_data(root)) + root.ui.modals.flow.open(root.ui.modals.status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项) + root.ui.modals.status_panel.open_status(_status_data(root)) static func _on_status_closed(root: Variant) -> void: - root._close_modal(root.status_panel) # 模态栈单口 + root.ui.modals.flow.close(root.ui.modals.status_panel) # 模态栈单口 static func _open_career(root: Variant) -> void: - root._start_overlay.visible = false - root._open_modal(root.career_overlay) # 模态栈单口 - root.career_overlay.open_career(HudPresenter.career_data(root.db, root.save_manager, root.meta)) + root.ui.screens.start_overlay.visible = false + root.ui.modals.flow.open(root.ui.modals.career_overlay) # 模态栈单口 + root.ui.modals.career_overlay.open_career(HudPresenter.career_data(root.db, root.save_manager, root.meta)) static func _on_career_closed(root: Variant) -> void: - root._close_modal(root.career_overlay) # 模态栈单口 - root._start_overlay.visible = true + root.ui.modals.flow.close(root.ui.modals.career_overlay) # 模态栈单口 + root.ui.screens.start_overlay.visible = true # 生涯页快照投影 —— 承接体见 HudPresenter.career_data(开层调用点直接改引) @@ -52,13 +52,13 @@ static func _on_career_closed(root: Variant) -> void: # 开层(模态栈单口;与开始层互斥=career 同款先例)。「进入存档」钮归卡② mn3——本函数为 # 该钮的唯一落点(卡③ 批内探针函数直调=中间态,实现报告「已知中间态」登记)。 static func _open_save_select(root: Variant) -> void: - root._start_overlay.visible = false - root._open_modal(root.save_select_overlay) # 模态栈单口 - root.save_select_overlay.open_save_select(HudPresenter.save_select_data(root.save_manager)) + root.ui.screens.start_overlay.visible = false + root.ui.modals.flow.open(root.ui.modals.save_select_overlay) # 模态栈单口 + root.ui.modals.save_select_overlay.open_save_select(HudPresenter.save_select_data(root.save_manager)) static func _on_save_select_closed(root: Variant) -> void: - root._close_modal(root.save_select_overlay) # 模态栈单口 - root._start_overlay.visible = true + root.ui.modals.flow.close(root.ui.modals.save_select_overlay) # 模态栈单口 + root.ui.screens.start_overlay.visible = true # 槽数据快照 —— 承接体见 HudPresenter.save_select_data(开层调用点直接改引) @@ -85,18 +85,18 @@ static func _on_save_select_load(root: Variant, slot: int) -> void: static func _on_save_select_delete(root: Variant, slot: int) -> void: root._confirm_consumer = "delete_slot" root._confirm_consumer_slot = slot - root._open_modal(root.confirm_dialog) # 模态栈单口(显示编排/挂起兜底/遮挡记账——与其余覆盖层同口) - root.confirm_dialog.open("删除存档位 %d?删除后该档不可恢复。" % slot, "删除", "取消") + root.ui.modals.flow.open(root.ui.modals.confirm_dialog) # 模态栈单口(显示编排/挂起兜底/遮挡记账——与其余覆盖层同口) + root.ui.modals.confirm_dialog.open("删除存档位 %d?删除后该档不可恢复。" % slot, "删除", "取消") # 入口角标点击(两屏同款处理器):P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰, # 探针 pressed.emit 绕过 disabled 时由此兜住;地图屏自身的登记开态不计——has_open_excluding) static func _on_status_badge_pressed(root: Variant) -> void: - var map_on: bool = root.level_screen != null and root.level_screen.visible + var map_on: bool = root.ui.modals.level_screen != null and root.ui.modals.level_screen.visible # P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,探针 pressed.emit 绕过 disabled 时由此兜住; # 地图屏自身的登记开态不计;判据排除调试浮窗:对局中浮窗开着时状态角标仍可点) - if (map_on and root._modal_flow.badge_greyed_map()) or (not map_on and root._modal_flow.badge_greyed_battle()): + if (map_on and root.ui.modals.flow.badge_greyed_map()) or (not map_on and root.ui.modals.flow.badge_greyed_battle()): return _open_status_panel(root) @@ -106,17 +106,17 @@ static func _on_status_badge_pressed(root: Variant) -> void: # 地图屏角标=随屏;置灰=登记册上除地图屏自身外任一层开着(防「看得见点不着」态)。 # 调用面=模态开合单口与屏级互斥口(凡影响开态/屏态的路径必经其一)。 static func _sync_status_entry(root: Variant) -> void: - if root.status_panel == null or root._modals == null or root.board_zone == null or root.level_screen == null: + if root.ui.modals.status_panel == null or root.ui.modals.flow == null or root.ui.battle.board_zone == null or root.ui.modals.level_screen == null: return - var map_on: bool = root.level_screen.visible - root.board_zone.status_badge.visible = root.run_scope.level_active and not map_on - root.level_screen.status_badge.visible = map_on + var map_on: bool = root.ui.modals.level_screen.visible + root.ui.battle.board_zone.status_badge.visible = root.run_scope.level_active and not map_on + root.ui.modals.level_screen.status_badge.visible = map_on # 置灰判据为排除式(调试浮窗开态不计入:对局中浮窗开着时角标仍可点) - root.board_zone.status_badge.disabled = root._modal_flow.badge_greyed_battle() # 互斥域谓词 - root.level_screen.status_badge.disabled = root._modal_flow.badge_greyed_map() + root.ui.battle.board_zone.status_badge.disabled = root.ui.modals.flow.badge_greyed_battle() # 互斥域谓词 + root.ui.modals.level_screen.status_badge.disabled = root.ui.modals.flow.badge_greyed_map() # 调试浮层角标(全局浮层任何 UI 不遮挡——可见性不随场景变,只抬 offset # 避让:地图屏底栏 85px 常驻,角标抬至底栏上方 16px;其余场景右下角 16px 贴底) - root.debug_badge.offset_bottom = -101.0 if map_on else -16.0 + root.ui.screens.debug_badge.offset_bottom = -101.0 if map_on else -16.0 @@ -131,19 +131,19 @@ static func _on_wallet_changed(root: Variant, event: String, gp: int, extra: Dic root.logger.log_wallet(event, gp, extra) if String(event) == "gain": root.run_scope.last_gain_amount = int(extra.get("amount", 0)) # GP 增量取数(每战恰一笔 gain) - if root.shop_overlay.visible and root.run_scope.runner != null: + if root.ui.modals.shop_overlay.visible and root.run_scope.runner != null: if root.run_scope.runner.shop.is_shop_pool(): - root.shop_overlay.refresh_shelf_state(int(root.run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变) + root.ui.modals.shop_overlay.refresh_shelf_state(int(root.run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变) else: - root.shop_overlay.refresh_state(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp)) + root.ui.modals.shop_overlay.refresh_state(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp)) root._refresh_presentations() static func _on_legend_toggled(root: Variant) -> void: - if root.legend_overlay.is_open(): - root._close_modal(root.legend_overlay) + if root.ui.modals.legend_overlay.is_open(): + root.ui.modals.flow.close(root.ui.modals.legend_overlay) else: - root._open_modal(root.legend_overlay) + root.ui.modals.flow.open(root.ui.modals.legend_overlay) diff --git a/game/presentation/main_ui_build.gd b/game/presentation/main_ui_build.gd index 52f206c5..b535fb7c 100644 --- a/game/presentation/main_ui_build.gd +++ b/game/presentation/main_ui_build.gd @@ -1,7 +1,7 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION ## 组合根 UI 装配分步承接件:三屏壳实例化、视图句柄接线、覆盖层装配与模态编排注入按步拆开。 ## 本件是 main._build_ui 的逐字分步搬运(成员访问改 root 前缀)——main 侧留同名薄委托,装配序与 z 序零改动。 -## 层序唯一事实源仍是 main.LAYER_CONTRACT + _apply_layer_contract(本件不持有层序逻辑)。 +## 层序唯一事实源=root.ui.LAYER_CONTRACT + root.ui.apply_layer_contract(ui_registry.gd——本件不持有层序逻辑)。 extends RefCounted const UiAssembler := preload("res://presentation/ui_assembler.gd") @@ -35,50 +35,48 @@ static func _build_screens(root: Variant) -> void: # 屏内 level_screen 与 battle_root 无同时可见态(屏级互斥+模态栈遮挡),层内先后零视觉差; # 视觉共存对逐对验算:legend/battle(图例战斗中开——legend 在冒险屏壳之后=其上 ✓)、 # debug/level_screen(浮窗最后子=其上 ✓)、shop/level_screen(商店节点域弹出——模态均在屏壳上 ✓)。 - root._adventure_screen = AdventureScreenScene.instantiate() - root.add_child(root._adventure_screen) - root._menu_screen = MenuScreenScene.instantiate() - root.add_child(root._menu_screen) - root._start_overlay = root._menu_screen.start_overlay - root._battle_root = root._adventure_screen.battle_root - root.stage = root._adventure_screen.stage - root.log_view = root._adventure_screen.log_view - root.info_panel = root._adventure_screen.info_panel - root.board_zone = root._adventure_screen.board_zone - root.board_view = root._adventure_screen.board_view # 探针兼容别名(= board_zone.board_view) - root.ap_bar = root._adventure_screen.ap_bar # 探针兼容别名(= board_zone.ap_bar) - root.settlement_fx = root._adventure_screen.settlement_fx - root.level_screen = root._adventure_screen.level_screen + root.ui.screens.adventure_screen = AdventureScreenScene.instantiate() + root.add_child(root.ui.screens.adventure_screen) + root.ui.screens.menu_screen = MenuScreenScene.instantiate() + root.add_child(root.ui.screens.menu_screen) + root.ui.screens.start_overlay = root.ui.screens.menu_screen.start_overlay + root.ui.screens.battle_root = root.ui.screens.adventure_screen.battle_root + root.ui.battle.stage = root.ui.screens.adventure_screen.stage + root.ui.battle.log_view = root.ui.screens.adventure_screen.log_view + root.ui.battle.info_panel = root.ui.screens.adventure_screen.info_panel + root.ui.battle.board_zone = root.ui.screens.adventure_screen.board_zone + root.ui.battle.settlement_fx = root.ui.screens.adventure_screen.settlement_fx + root.ui.modals.level_screen = root.ui.screens.adventure_screen.level_screen static func _build_battle_wiring(root: Variant) -> void: # 结算行排播泵(本体在 row_pacer.gd;本类注入视图句柄与呈现刷新回调,零信号连接) - root._row_pacer = RowPacer.new() - root._row_pacer.log_view = root.log_view - root._row_pacer.stage = root.stage - root._row_pacer.board_zone = root.board_zone - root._row_pacer.settlement_fx = root.settlement_fx - root._row_pacer.board_view = root.board_view - root._row_pacer.refresh_request = root._refresh_presentations - root._row_pacer.pump_ended = root._on_pump_ended # 演播完边沿统一派发(挂起 modal 优先,其次锁期点击重放) - root.add_child(root._row_pacer) + root.ui.screens.row_pacer = RowPacer.new() + root.ui.screens.row_pacer.log_view = root.ui.battle.log_view + root.ui.screens.row_pacer.stage = root.ui.battle.stage + root.ui.screens.row_pacer.board_zone = root.ui.battle.board_zone + root.ui.screens.row_pacer.settlement_fx = root.ui.battle.settlement_fx + root.ui.screens.row_pacer.board_view = root.ui.battle.board_zone.board_view + root.ui.screens.row_pacer.refresh_request = root._refresh_presentations + root.ui.screens.row_pacer.pump_ended = root.ui.modals.flow.pump_ended # 演播完边沿统一派发(挂起 modal 优先,其次锁期点击重放) + root.add_child(root.ui.screens.row_pacer) # 秘宝两步交互(本体在 treasure_interact_controller.gd;settlement/runner/grant 随关卡/战斗更替, # 经现值回调注入——仿 identity_resolver 只读回调先例;grant 每战新建编排器,现值取件防悬挂) root._treasure_ui = TreasureInteractController.new({ - "db": root.db, "meta": root.meta, "info_panel": root.info_panel, "toast": root.board_zone.toast, - "board_view": root.board_view, + "db": root.db, "meta": root.meta, "info_panel": root.ui.battle.info_panel, "toast": root.ui.battle.board_zone.toast, + "board_view": root.ui.battle.board_zone.board_view, "settlement_get": func() -> Variant: return root.run_scope.settlement, "runner_get": func() -> Variant: return root.run_scope.runner, "grant_get": func() -> Variant: return root.run_scope.settlement.reward_grant() if root.run_scope.settlement != null else null, }) - root.board_zone.action_requested.connect(root._on_board_action) - root.board_zone.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(战斗屏 HUD 角标) - root.info_panel.treasure_slot_clicked.connect(root._treasure_ui.on_slot_clicked) # 两步交互第一步 - root.board_zone.stop_pressed.connect(root._on_stop_pressed) - root.board_zone.refresh_confirmed.connect(root._on_refresh_confirmed) - root.board_zone.legend_toggled.connect(root._on_legend_toggled) - root.board_zone.hover_forbidden.connect(root._on_hover_forbidden) - root.board_zone.chord_attempted.connect(root._on_chord_attempted) + root.ui.battle.board_zone.action_requested.connect(root._on_board_action) + root.ui.battle.board_zone.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(战斗屏 HUD 角标) + root.ui.battle.info_panel.treasure_slot_clicked.connect(root._treasure_ui.on_slot_clicked) # 两步交互第一步 + root.ui.battle.board_zone.stop_pressed.connect(root._on_stop_pressed) + root.ui.battle.board_zone.refresh_confirmed.connect(root._on_refresh_confirmed) + root.ui.battle.board_zone.legend_toggled.connect(root._on_legend_toggled) + root.ui.battle.board_zone.hover_forbidden.connect(root._on_hover_forbidden) + root.ui.battle.board_zone.chord_attempted.connect(root._on_chord_attempted) root.logger = PlaytestLogger.new() root.logger.ap_per_round = int(root.db.run.action_economy.ap_per_round) @@ -88,136 +86,135 @@ static func _build_overlays(root: Variant) -> void: # < 商店 < 营地 < 设置 < 状态面板(只读) < 生涯页(只读) < 确认弹窗 < 调试浮窗; # 关卡画面住冒险屏壳(树序在模态之下)——登记序仍夹在节点层与三选一层之间,遮挡语义零改动) var overlays: Dictionary[Variant, Variant] = UiAssembler.build_overlays(root, root.db) - root.legend_overlay = overlays.legend_overlay - root.node_overlay = overlays.node_overlay - root._end_overlay = overlays.end_overlay - root.shop_overlay = overlays.shop_overlay - root._camp_screen = CampScreenScene.instantiate() - root.add_child(root._camp_screen) - root.camp_overlay = root._camp_screen.camp_overlay + root.ui.modals.legend_overlay = overlays.legend_overlay + root.ui.modals.node_overlay = overlays.node_overlay + root.ui.modals.end_overlay = overlays.end_overlay + root.ui.modals.shop_overlay = overlays.shop_overlay + root.ui.screens.camp_screen = CampScreenScene.instantiate() + root.add_child(root.ui.screens.camp_screen) + root.ui.modals.camp_overlay = root.ui.screens.camp_screen.camp_overlay # 营地屏壳插位(夹商店层与设置层之间)由 LAYER_CONTRACT 营地槽统一落位——禁散落 move_child - root.legend_overlay.closed.connect(root._on_modal_closed.bind(root.legend_overlay)) - root.node_overlay.continue_pressed.connect(root._on_node_continue) - root.level_screen.route_chosen.connect(root._on_route_chosen) - root.level_screen.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(地图屏顶栏角标) - root.level_screen.abandon_requested.connect(root._on_abandon_requested) # 放弃入口 - root._end_overlay.continue_pressed.connect(root._on_battle_end_continue) + root.ui.modals.legend_overlay.closed.connect(root.ui.modals.flow.close.bind(root.ui.modals.legend_overlay)) + root.ui.modals.node_overlay.continue_pressed.connect(root._on_node_continue) + root.ui.modals.level_screen.route_chosen.connect(root._on_route_chosen) + root.ui.modals.level_screen.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(地图屏顶栏角标) + root.ui.modals.level_screen.abandon_requested.connect(root._on_abandon_requested) # 放弃入口 + root.ui.modals.end_overlay.continue_pressed.connect(root._on_battle_end_continue) static func _build_aux_panels(root: Variant) -> void: - root.settings_overlay = SettingsOverlay.new(root.settings_store) # 最小设置面(音量/全屏/减动效) - root.add_child(root.settings_overlay) - root.settings_overlay.closed.connect(root._on_settings_closed) + root.ui.modals.settings_overlay = SettingsOverlay.new(root.settings_store) # 最小设置面(音量/全屏/减动效) + root.add_child(root.ui.modals.settings_overlay) + root.ui.modals.settings_overlay.closed.connect(root._on_settings_closed) # 局内状态面板·只读(登记设置层同档带 dim 遮罩,确认弹窗之前; # 数据=组合根开层实时读 MetaProgression 访问器喂入,本层零写通路) - root.status_panel = StatusPanel.new() - root.add_child(root.status_panel) - root.status_panel.closed.connect(root._on_status_closed) + root.ui.modals.status_panel = StatusPanel.new() + root.add_child(root.ui.modals.status_panel) + root.ui.modals.status_panel.closed.connect(root._on_status_closed) # 生涯页·只读全屏页(登记序=营地同档,设置层旁、确认弹窗之前; # 树序同步=addChild 同位(登记序=树序=z 序纪律);数据=组合根开层实时读 SaveManager 计数 # 访问器+MetaProgression 访问器喂入,本层零写通路零跳转;入口=开始层「营地与系统」组生涯钮) - root.career_overlay = CareerOverlay.new() - root.add_child(root.career_overlay) - root.career_overlay.closed.connect(root._on_career_closed) + root.ui.modals.career_overlay = CareerOverlay.new() + root.add_child(root.ui.modals.career_overlay) + root.ui.modals.career_overlay.closed.connect(root._on_career_closed) # 选档界面(营地交互重做卡③):登记序=生涯同档(树序同步=addChild 同位);三动作只发意图信号 #(激活/创建/删除+确认分派归组合根——视图零数据层依赖);入口=「进入存档」钮(卡② mn3 接线)。 - root.save_select_overlay = SaveSelectOverlay.new() - root.add_child(root.save_select_overlay) - root.save_select_overlay.closed.connect(root._on_save_select_closed) - root.save_select_overlay.new_slot_requested.connect(root._on_save_select_new) - root.save_select_overlay.load_slot_requested.connect(root._on_save_select_load) - root.save_select_overlay.delete_slot_requested.connect(root._on_save_select_delete) + root.ui.modals.save_select_overlay = SaveSelectOverlay.new() + root.add_child(root.ui.modals.save_select_overlay) + root.ui.modals.save_select_overlay.closed.connect(root._on_save_select_closed) + root.ui.modals.save_select_overlay.new_slot_requested.connect(root._on_save_select_new) + root.ui.modals.save_select_overlay.load_slot_requested.connect(root._on_save_select_load) + root.ui.modals.save_select_overlay.delete_slot_requested.connect(root._on_save_select_delete) static func _build_curio(root: Variant) -> void: # 提升三选一覆盖层(z 序 = 节点层之上、终局层之下;确认后同拍推进; # 插位由 LAYER_CONTRACT curio 槽统一落位——禁散落 move_child) - root.curio_overlay = CurioPickOverlay.new() - root.add_child(root.curio_overlay) + root.ui.modals.curio_overlay = CurioPickOverlay.new() + root.add_child(root.ui.modals.curio_overlay) # 提升三选一与替换子交互(本体在 curio_interact_controller.gd;本类只接线与回调注入, # 模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调) root._bonus = CurioInteractController.new(CurioFlowContext.new(root.db, - {"curio_overlay": root.curio_overlay, "end_overlay": root._end_overlay, - "board_view": root.board_view, "toast": root.board_zone.toast}, + {"curio_overlay": root.ui.modals.curio_overlay, "end_overlay": root.ui.modals.end_overlay, + "board_view": root.ui.battle.board_zone.board_view, "toast": root.ui.battle.board_zone.toast}, {"meta_get": func() -> Variant: return root.meta, "save_get": func() -> Variant: return root.save_manager, "adventure_get": func() -> Variant: return root.run_scope.adventure, "runner_get": func() -> Variant: return root.run_scope.runner, "adventure_mode_get": func() -> Variant: return root.run_scope.is_adventure}, - {"open_modal": root._open_modal, "open_modal_after_playback": root._open_modal_after_playback, - "close_modal": root._close_modal, "refresh_presentations": root._refresh_presentations, + {"open_modal": root.ui.modals.flow.open, "open_modal_after_playback": root.ui.modals.flow.open_after_playback, + "close_modal": root.ui.modals.flow.close, "refresh_presentations": root._refresh_presentations, "fatal_screen": root._fatal_screen, "gp_summary": _gp_summary_cb(root)})) # 构造参数收拢 context(u20 三束) - root.curio_overlay.pick_confirmed.connect(root._bonus.on_pick_confirmed) - root.curio_overlay.continue_pressed.connect(root._bonus.on_continue) - root.curio_overlay.refresh_requested.connect(root._bonus.on_refresh) + root.ui.modals.curio_overlay.pick_confirmed.connect(root._bonus.on_pick_confirmed) + root.ui.modals.curio_overlay.continue_pressed.connect(root._bonus.on_continue) + root.ui.modals.curio_overlay.refresh_requested.connect(root._bonus.on_refresh) static func _build_menu_wiring(root: Variant) -> void: # 菜单屏接线(句柄经屏壳暴露;三入口化——进入存档/设置/退出游戏,卡② mn3: # 进入存档钮落点=卡③选档层 _open_save_select,点击链在此闭环) - root._menu_screen.entersavebtn.pressed.connect(root._open_save_select) + root.ui.screens.menu_screen.entersavebtn.pressed.connect(root._open_save_select) # 调试入口=UI 最顶层全局浮层角标(任何 UI 不遮挡——菜单/战斗/地图三枚 # 屏内角标由全局浮层承载;连接归组合根,树序/登记面见 LAYER_CONTRACT 末位) - root.debug_badge = UiAssembler.build_debug_badge(root) - root.add_child(root.debug_badge) - root.debug_badge.pressed.connect(root._on_debug_badge_pressed) + root.ui.screens.debug_badge = UiAssembler.build_debug_badge(root) + root.add_child(root.ui.screens.debug_badge) + root.ui.screens.debug_badge.pressed.connect(root._on_debug_badge_pressed) # 营地/商店投影(本体在 camp_shop_presenter.gd;本类只接线、探针缝保缝转发; # 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入) root._camp_pres = CampShopPresenter.new(CampFlowContext.new(root.db, - {"camp_overlay": root.camp_overlay, "shop_overlay": root.shop_overlay, - "toast": root.board_zone.toast, "start_overlay": root._start_overlay, "logger": root.logger}, + {"camp_overlay": root.ui.modals.camp_overlay, "shop_overlay": root.ui.modals.shop_overlay, + "toast": root.ui.battle.board_zone.toast, "start_overlay": root.ui.screens.start_overlay, "logger": root.logger}, {"meta_get": func() -> Variant: return root.meta, "save_get": func() -> Variant: return root.save_manager, "runner_get": func() -> Variant: return root.run_scope.runner}, - {"open_modal": root._open_modal, "close_modal": root._close_modal, + {"open_modal": root.ui.modals.flow.open, "close_modal": root.ui.modals.flow.close, "refresh_presentations": root._refresh_presentations})) # 构造参数收拢 context(u21 三束) - root.shop_overlay.buy_requested.connect(root._camp_pres.on_shop_buy) - root.shop_overlay.refresh_requested.connect(root._camp_pres.on_shop_refresh) # MC-1 - root.shop_overlay.leave_pressed.connect(root._camp_pres.on_shop_leave) - root.camp_overlay.buy_requested.connect(root._camp_pres.on_camp_buy) - root.camp_overlay.attune_toggled.connect(root._camp_pres.on_camp_attune_toggle) - root.camp_overlay.tree_buy_requested.connect(root._camp_pres.on_camp_tree_buy) - root.camp_overlay.tree_fork_toggled.connect(root._camp_pres.on_camp_fork_toggle) - root.camp_overlay.forge_buy_requested.connect(root._camp_pres.on_camp_forge_buy) # 锻造三意图(写口唯一归 MetaProgression) - root.camp_overlay.forge_unlock_requested.connect(root._camp_pres.on_camp_forge_unlock) - root.camp_overlay.forge_select_requested.connect(root._camp_pres.on_camp_forge_select) - root.camp_overlay.closed.connect(root._camp_pres.on_camp_closed) - root._menu_screen.settingsbtn.pressed.connect(root._open_settings) # 最小设置面入口 - root._menu_screen.quitbtn.pressed.connect(root._quit_game) # 退出游戏提级一级菜单 + root.ui.modals.shop_overlay.buy_requested.connect(root._camp_pres.on_shop_buy) + root.ui.modals.shop_overlay.refresh_requested.connect(root._camp_pres.on_shop_refresh) # MC-1 + root.ui.modals.shop_overlay.leave_pressed.connect(root._camp_pres.on_shop_leave) + root.ui.modals.camp_overlay.buy_requested.connect(root._camp_pres.on_camp_buy) + root.ui.modals.camp_overlay.attune_toggled.connect(root._camp_pres.on_camp_attune_toggle) + root.ui.modals.camp_overlay.tree_buy_requested.connect(root._camp_pres.on_camp_tree_buy) + root.ui.modals.camp_overlay.tree_fork_toggled.connect(root._camp_pres.on_camp_fork_toggle) + root.ui.modals.camp_overlay.forge_buy_requested.connect(root._camp_pres.on_camp_forge_buy) # 锻造三意图(写口唯一归 MetaProgression) + root.ui.modals.camp_overlay.forge_unlock_requested.connect(root._camp_pres.on_camp_forge_unlock) + root.ui.modals.camp_overlay.forge_select_requested.connect(root._camp_pres.on_camp_forge_select) + root.ui.modals.camp_overlay.closed.connect(root._camp_pres.on_camp_closed) + root.ui.screens.menu_screen.settingsbtn.pressed.connect(root._open_settings) # 最小设置面入口 + root.ui.screens.menu_screen.quitbtn.pressed.connect(root._quit_game) # 退出游戏提级一级菜单 static func _build_debug_and_resolvers(root: Variant) -> void: var inter: Dictionary[Variant, Variant] = UiAssembler.build_inter_overlay(root) - root._inter_overlay = inter.overlay - root._inter_label = inter.label + root.ui.screens.inter_overlay = inter.overlay + root.ui.screens.inter_label = inter.label inter.next_btn.pressed.connect(root._on_inter_next_pressed) # 确认弹窗(新冒险互斥确认弹窗化):树序落在开始层/过场层之后 = 子级最末 #(纪律「登记序 = 树序 = z 序」——confirm_dialog.open 禁运行时 move_child 自提抬层; # 弹窗只在开始菜单域弹出,绘制序恒最上; # 诊断转储入口在调试浮窗) - root.confirm_dialog = ConfirmDialog.new() - root.add_child(root.confirm_dialog) + root.ui.modals.confirm_dialog = ConfirmDialog.new() + root.add_child(root.ui.modals.confirm_dialog) # 消费方编排(卡③ 契约 3/sl8——禁双常驻静态接线):open 前设 _confirm_consumer, # confirmed/cancelled 后按该状态量分派;分派面可容纳后续消费方(卡④ 到位直接接入不返工) - root.confirm_dialog.confirmed.connect(root._on_confirm_accepted) - root.confirm_dialog.cancelled.connect(root._on_confirm_declined) + root.ui.modals.confirm_dialog.confirmed.connect(root._on_confirm_accepted) + root.ui.modals.confirm_dialog.cancelled.connect(root._on_confirm_declined) # 调试浮窗(主菜单三散件收一:随机种子/数值调整/批量模拟/两层调试入口; # 全局可开浮窗:调试会话恒开=启动即开〔_ready〕+两屏角标再开+当局数据只读区块; # 诊断转储入口在本浮窗,玩家画面零常驻调试件)。 # 树序在开始层与确认弹窗之后 = 浮于菜单之上的 Dialog(不隐藏开始层——与确认弹窗同款形态); # 开窗唯一入口 = _show_debug_overlay(C6 单口——角标/探针/启动路径全收口) - root.debug_overlay = DebugOverlay.new(root.db, int(root.db.combat.hero.atk), int(root.db.combat.hero.max_hp)) - root.add_child(root.debug_overlay) - root._seed_edit = root.debug_overlay.seed_edit # 种子读取面在浮窗(_seed_value 唯一消费点不变) - root.debug_overlay.closed.connect(root._on_modal_closed.bind(root.debug_overlay)) - root.debug_overlay.apply_requested.connect(root._on_tune_apply) - root.debug_overlay.simulate_requested.connect(root._on_tune_simulate) - root.debug_overlay.debug_levels_requested.connect(root._start_debug_levels) + root.ui.modals.debug_overlay = DebugOverlay.new(root.db, int(root.db.combat.hero.atk), int(root.db.combat.hero.max_hp)) + root.add_child(root.ui.modals.debug_overlay) + root.ui.modals.debug_overlay.closed.connect(root.ui.modals.flow.close.bind(root.ui.modals.debug_overlay)) + root.ui.modals.debug_overlay.apply_requested.connect(root._on_tune_apply) + root.ui.modals.debug_overlay.simulate_requested.connect(root._on_tune_simulate) + root.ui.modals.debug_overlay.debug_levels_requested.connect(root._start_debug_levels) # 棋盘解析回调族(有态绑定件——board_resolvers.gd 承接;组合根只接线;局部持有不占类成员位) - var resolvers = BoardResolversScript.new(root.db, root.debug_overlay, func() -> Variant: return root.run_scope.settlement) + var resolvers = BoardResolversScript.new(root.db, root.ui.modals.debug_overlay, func() -> Variant: return root.run_scope.settlement) # 奖励徽章数据源:地图战斗资源 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效) - root.board_view.set_identity_resolver(resolvers.identity_lookup) + root.ui.battle.board_zone.board_view.set_identity_resolver(resolvers.identity_lookup) # 数字遮蔽数据源:地形变异 hide_number 解析只读回调(信息型呈现——board_view 渲染面读取) - root.board_view.set_variant_mask_resolver(resolvers.variant_mask_lookup) - root.board_view.set_variant_mark_resolver(resolvers.variant_mark_lookup) # flat_damage 未开格角标 + root.ui.battle.board_zone.board_view.set_variant_mask_resolver(resolvers.variant_mask_lookup) + root.ui.battle.board_zone.board_view.set_variant_mark_resolver(resolvers.variant_mark_lookup) # flat_damage 未开格角标 # 调试会话(英雄生效值预览回调绑同一解析件——创建晚于浮窗、随 _build_ui 一次接线) root._debug = DebugSessionScript.new(root.db, root.get_node(root.AUTOLOAD_PATH_RNG), func() -> Dictionary: return resolvers.debug_hero_override(), root._seed_value) @@ -228,24 +225,22 @@ static func _build_modal_flow(root: Variant) -> void: # 模态登记(缺陷⑤纪律——登记序 = 树序 = z 序):层序唯一事实源=LAYER_CONTRACT, # _apply_layer_contract 统一落位(关卡画面宿住冒险屏壳、营地屏壳夹商店与设置层之间、 # 三选一对位终局层之下等特例均在合同数据内表达,禁散落 move_child) - # 模态编排注入(ModalFlow 纯逻辑类:依赖仅 ModalStack+查询 Callable) - root._modal_flow = ModalFlow.new() - root._modal_flow.start_visible_get = func() -> bool: return root._start_overlay.visible - root._modal_flow.inter_visible_get = func() -> bool: return root._inter_overlay.visible - root._modal_flow.settlement_get = func() -> Variant: return root.run_scope.settlement - root._modal_flow.rows_empty_get = func() -> bool: return root._row_pacer.pending_rows().is_empty() - root._modal_flow.flush_rows = root._flush_row_queue - root._modal_flow.on_open_sync = func() -> void: + # 模态编排注入(ModalFlow 纯逻辑类:依赖仅 ModalStack+查询 Callable——实例随 ui.modals 容器构造建立) + root.ui.modals.flow.start_visible_get = func() -> bool: return root.ui.screens.start_overlay.visible + root.ui.modals.flow.inter_visible_get = func() -> bool: return root.ui.screens.inter_overlay.visible + root.ui.modals.flow.settlement_get = func() -> Variant: return root.run_scope.settlement + root.ui.modals.flow.rows_empty_get = func() -> bool: return root.ui.screens.row_pacer.pending_rows().is_empty() + root.ui.modals.flow.flush_rows = root.ui.screens.row_pacer.flush + root.ui.modals.flow.on_open_sync = func() -> void: root._sync_status_entry() # 状态面板入口角标可见性/置灰随开态同步 root._sync_debug_overlay() # 浮窗当局数据/按钮态随开态同步 - root._modal_flow.on_close_sync = func() -> void: + root.ui.modals.flow.on_close_sync = func() -> void: root._sync_status_entry() # 关闭恢复角标可点态(无残留——同源口径) root._sync_debug_overlay() # 他层关闭复活浮窗时同步刷新(关后自动恢复语义配套) - root._modal_flow.replay_click = root._replay_locked_click - root._modals = root._modal_flow.stack # 探针 _list 登记序断言缝(ui_struct 登记序=z 序纪律断言) + root.ui.modals.flow.replay_click = root._replay_locked_click # 视觉层合同装配(唯一事实源=LAYER_CONTRACT:树序+登记序同源派生——改序只改合同表) - root._apply_layer_contract() - root._tune_sim = TuneSimController.new(root.db, root.debug_overlay, int(root.db.combat.hero.max_hp)) + root.ui.apply_layer_contract(root) + root._tune_sim = TuneSimController.new(root.db, root.ui.modals.debug_overlay, int(root.db.combat.hero.max_hp)) static func fatal_screen(root: Variant, msg: String) -> void: var l := UiKit.label(msg,UiKit.FS_SUB, true, UiKit.RED_ALERT) diff --git a/game/presentation/ui_battle.gd b/game/presentation/ui_battle.gd new file mode 100644 index 00000000..206e671c --- /dev/null +++ b/game/presentation/ui_battle.gd @@ -0,0 +1,10 @@ +## 战区视图域句柄册:战斗屏 A–D 区视图与结算演出覆盖层的注册面域容器。 +## 纯字段无方法;RefCounted 不挂树——节点本体树位与 add_child 归属由组合根与装配承接件负责。 +class_name UiBattle +extends RefCounted + +var stage: BattleStageView # B 区(含 log_view 明细带) +var log_view: BattleLogView +var info_panel: HeroInfoPanelView # C 区(攻击/AC 数据框+两预留区) +var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx) +var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下) diff --git a/game/presentation/ui_battle.gd.uid b/game/presentation/ui_battle.gd.uid new file mode 100644 index 00000000..4e684bae --- /dev/null +++ b/game/presentation/ui_battle.gd.uid @@ -0,0 +1 @@ +uid://d35j5hry1f40v diff --git a/game/presentation/ui_modals.gd b/game/presentation/ui_modals.gd new file mode 100644 index 00000000..e156fa15 --- /dev/null +++ b/game/presentation/ui_modals.gd @@ -0,0 +1,32 @@ +## 模态层域句柄册:十三个模态覆盖层句柄+模态栈编排(ModalFlow——登记/开合/输入锁谓词)的注册面域容器。 +## RefCounted 不挂树;模态登记唯一口=register_layer(anchors 撑满宿主+排入 ModalStack)。 +## flow 随容器构造即建(装配早期注入点经 root.ui.modals.flow.* 取 Callable——晚于 flow 建立时点)。 +class_name UiModals +extends RefCounted + +var flow: ModalFlow +var legend_overlay: LegendOverlay +var node_overlay: NodeOverlay +var level_screen: LevelMapScreen +var curio_overlay: CurioPickOverlay +var end_overlay: BattleEndOverlay +var shop_overlay: ShopOverlay +var camp_overlay: CampOverlay +var settings_overlay: SettingsOverlay +var status_panel: StatusPanel +var career_overlay: CareerOverlay +var save_select_overlay: SaveSelectOverlay +var confirm_dialog: ConfirmDialog +var debug_overlay: DebugOverlay + + +func _init() -> void: + flow = ModalFlow.new() # 查询/钩子 Callable 由 _build_modal_flow 注入(注入时点不变——注入值面向已在座实例) + + +func register_layer(m: Control, tags: Array = []) -> void: + # 撑满宿主(Control 默认锚 = 0×0——全屏变暗层与居中布局整体失效;route 选路层预览截图实证后 + # 统一收口:六个模态同病,探针经信号直发绕过真实点击从未暴露。anchors_and_offsets 而非 anchors + # ——后者会把控件重置回最小内容尺寸,ui_assembler 头注同款纪律) + m.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + flow.register(m, tags) # 排除组随层登记(non_locking=调试浮窗/screen_state=关卡地图屏) diff --git a/game/presentation/ui_modals.gd.uid b/game/presentation/ui_modals.gd.uid new file mode 100644 index 00000000..4ac82a5c --- /dev/null +++ b/game/presentation/ui_modals.gd.uid @@ -0,0 +1 @@ +uid://dbs4qveqvr5hg diff --git a/game/presentation/ui_registry.gd b/game/presentation/ui_registry.gd new file mode 100644 index 00000000..bccce61f --- /dev/null +++ b/game/presentation/ui_registry.gd @@ -0,0 +1,101 @@ +## 界面注册面:呈现层界面元素清单的唯一事实源——哪些层、什么序、地址在哪。 +## 持有三域容器(screens/battle/modals)、LAYER_CONTRACT 合同表、node(key) 合同寻址、 +## apply_layer_contract(root) 唯一落位口(全仓唯一 move_child 点)。 +## RefCounted 不挂树:零信号连接、零会话期引用、零核心写调用——组合根单点装配不变。 +class_name UiRegistry +extends RefCounted + +const ModalStack := preload("res://presentation/modal_stack.gd") + +# ==================== 视觉层合同(唯一事实源——树序=登记序=z 序;改序只改此表) ==================== +# kind: shell=屏壳容器(成员随壳不占独立树位;level_screen 例外另占登记位)/modal=模态层(入登记册, +# tags 随登记)/state=全屏态(占树位不入登记册)/node=非视觉节点(占树位不入登记册)。 +# host=宿住屏壳(树序随宿主、登记序按本合同);shell=模态成员的屏壳(成员与屏壳同槽:屏壳占该树位、 +# 成员占该登记位)。落位=apply_layer_contract(全仓唯一层序落位点,add_child 之后统一排序+登记)。 +const LAYER_CONTRACT: Array[Dictionary] = [ + {"key": "adventure_screen", "kind": "shell"}, + {"key": "menu_screen", "kind": "shell"}, + {"key": "row_pacer", "kind": "node"}, + {"key": "legend_overlay", "kind": "modal", "tags": []}, + {"key": "node_overlay", "kind": "modal", "tags": []}, + {"key": "level_screen", "kind": "modal", "tags": [ModalStack.TAG_SCREEN_STATE], "host": "adventure_screen"}, + {"key": "curio_overlay", "kind": "modal", "tags": []}, + {"key": "end_overlay", "kind": "modal", "tags": []}, + {"key": "shop_overlay", "kind": "modal", "tags": []}, + {"key": "camp_overlay", "kind": "modal", "tags": [], "shell": "camp_screen"}, + {"key": "settings_overlay", "kind": "modal", "tags": []}, + {"key": "status_panel", "kind": "modal", "tags": []}, + {"key": "career_overlay", "kind": "modal", "tags": []}, + {"key": "save_select_overlay", "kind": "modal", "tags": []}, # 选档界面(营地交互重做卡③——登记生涯同档) + {"key": "inter_overlay", "kind": "state", "tags": []}, + {"key": "confirm_dialog", "kind": "modal", "tags": []}, + {"key": "debug_overlay", "kind": "modal", "tags": [ModalStack.TAG_NON_LOCKING]}, + {"key": "debug_badge", "kind": "float"}, # 调试入口全局浮层——末位=最顶绘制序; + # float 类不注册 ModalStack(open() 遮挡记账只隐藏登记层)→ 任何 UI 打开都不遮它 +] + +var screens: UiScreens +var battle: UiBattle +var modals: UiModals + + +func _init() -> void: + screens = UiScreens.new() + battle = UiBattle.new() + modals = UiModals.new() + + +# 合同层寻址(key→节点;纯查表不含层序语义——层序唯一事实源=LAYER_CONTRACT) +func node(key: String) -> Node: + match key: + "adventure_screen": return screens.adventure_screen + "menu_screen": return screens.menu_screen + "row_pacer": return screens.row_pacer + "legend_overlay": return modals.legend_overlay + "node_overlay": return modals.node_overlay + "level_screen": return modals.level_screen + "curio_overlay": return modals.curio_overlay + "end_overlay": return modals.end_overlay + "shop_overlay": return modals.shop_overlay + "camp_screen": return screens.camp_screen + "camp_overlay": return modals.camp_overlay + "settings_overlay": return modals.settings_overlay + "status_panel": return modals.status_panel + "career_overlay": return modals.career_overlay + "save_select_overlay": return modals.save_select_overlay + "inter_overlay": return screens.inter_overlay + "confirm_dialog": return modals.confirm_dialog + "debug_overlay": return modals.debug_overlay + "debug_badge": return screens.debug_badge + return null + + +# 合同装配(_build_ui 尾调用):按 LAYER_CONTRACT 序统一落位——直接子层 move_child 到合同槽、 +# 模态层按合同序登记;宿住层(host)树序随宿主只登记;成员层(shell)与屏壳同槽(屏壳占树位、成员占登记位)。 +# 本函数是全仓唯一层序落位点(move_child 单口)——改层序只改 LAYER_CONTRACT。 +func apply_layer_contract(root: Control) -> void: + var slot := 0 + for entry: Dictionary in LAYER_CONTRACT: + var tags: Array[Variant] = entry.get("tags", []) + if entry.has("shell"): + var shell: Node = node(String(entry["shell"])) + var member: Node = node(String(entry["key"])) + if shell != null: + root.move_child(shell, slot) + slot += 1 + if member != null: + modals.register_layer(member as Control, tags) + else: + push_error("[合同装配] 层缺失: %s" % String(entry["key"])) + continue + var layer: Node = node(String(entry["key"])) + if layer == null: + push_error("[合同装配] 层缺失: %s" % String(entry["key"])) + continue + if entry.has("host"): + modals.register_layer(layer as Control, tags) + continue + root.move_child(layer, slot) + slot += 1 + if String(entry["kind"]) == "modal": + modals.register_layer(layer as Control, tags) diff --git a/game/presentation/ui_registry.gd.uid b/game/presentation/ui_registry.gd.uid new file mode 100644 index 00000000..06405e33 --- /dev/null +++ b/game/presentation/ui_registry.gd.uid @@ -0,0 +1 @@ +uid://d1hahbtan1te8 diff --git a/game/presentation/ui_screens.gd b/game/presentation/ui_screens.gd new file mode 100644 index 00000000..b7a76b3e --- /dev/null +++ b/game/presentation/ui_screens.gd @@ -0,0 +1,16 @@ +## 屏壳与全屏容器域句柄册:三屏壳、屏内一级子层、全屏态、浮层角标、排播泵节点的注册面域容器。 +## 纯字段无方法;RefCounted 不挂树——节点本体树位与 add_child 归属由组合根与装配承接件负责。 +class_name UiScreens +extends RefCounted + +const RowPacer := preload("res://presentation/row_pacer.gd") # 排播泵取型(row_pacer.gd 非 class_name 全局) + +var menu_screen: Control +var camp_screen: Control +var adventure_screen: Control +var start_overlay: Control +var battle_root: Control +var inter_overlay: Control +var inter_label: Label +var debug_badge: Button +var row_pacer: RowPacer diff --git a/game/presentation/ui_screens.gd.uid b/game/presentation/ui_screens.gd.uid new file mode 100644 index 00000000..08356b8a --- /dev/null +++ b/game/presentation/ui_screens.gd.uid @@ -0,0 +1 @@ +uid://dd8ep7nhmyrdq diff --git a/game/probe/fx_shot_probe.gd b/game/probe/fx_shot_probe.gd index 9ba6d96d..eed8663e 100644 --- a/game/probe/fx_shot_probe.gd +++ b/game/probe/fx_shot_probe.gd @@ -37,14 +37,14 @@ func _ready() -> void: # 等关卡画面出图(choices_presented 异步到达——最多 3s 轮询) for i in 60: await get_tree().process_frame - if main.level_screen != null and main.level_screen.visible: + if main.ui.modals.level_screen != null and main.ui.modals.level_screen.visible: break - if main.level_screen == null or not main.level_screen.visible: + if main.ui.modals.level_screen == null or not main.ui.modals.level_screen.visible: _fail("关卡画面未出现(函数直调中间态路径)") return for i in 5: await get_tree().process_frame - main.level_screen.press_candidate(0) + main.ui.modals.level_screen.press_candidate(0) for i in 20: await get_tree().process_frame var st = main.run_scope.settlement diff --git a/game/probe/layout_probe.gd b/game/probe/layout_probe.gd index 25f494ae..ea8e366c 100644 --- a/game/probe/layout_probe.gd +++ b/game/probe/layout_probe.gd @@ -23,12 +23,12 @@ func _ready() -> void: add_child(main) await get_tree().process_frame ProbeUiUtil.isolate_save(main, "smoke_layout_probe", true, false, true) - main.debug_overlay.seed_edit.text = "42" # 种子钉死(确定性禁依赖默认随机分支) + main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(确定性禁依赖默认随机分支) main._start_adventure_from_menu() for i in range(8): await get_tree().process_frame # 进战斗屏——三区(B 战斗画面/C 角色信息/D 扫雷图)同屏常驻,菜单屏不构成断言面 - main.level_screen.press_candidate(0) + main.ui.modals.level_screen.press_candidate(0) for i in range(20): await get_tree().process_frame for vp in VIEWPORTS: @@ -57,8 +57,9 @@ func _check_one(label: String, main: Node) -> void: if vr.size != Vector2(VIEWPORTS[0]["size"]) and vr.size.y < 1.0: _problems.append("[%s] 逻辑视口退化 %s" % [label, vr.size]) var rects := {} + var battle_ui: Object = main.get("ui").battle # 注册面战区域容器(字段名与三区 key 同名——合同键即字段名) for key in ["stage", "info_panel", "board_zone"]: - var ctl: Control = main.get(key) + var ctl: Control = battle_ui.get(key) if ctl == null: _problems.append("[%s] %s = null(三区未装配)" % [label, key]) return @@ -73,7 +74,7 @@ func _check_one(label: String, main: Node) -> void: # battle_root 内层根下的全幅 ColorRect)这一缺陷类不在本探针拦截面内,探针对此仍全绿。 # 要拦它得改为递归遍历全子树找重叠,那是另一套判据(且极易误报容器父子关系), # 本探针不设此判据。 - var bv: Control = main.board_view + var bv: Control = main.ui.battle.board_zone.board_view if bv == null: _problems.append("[%s] board_view = null" % label) return @@ -89,6 +90,6 @@ func _check_one(label: String, main: Node) -> void: if rects[keys[i]].intersects(rects[keys[j]]): _problems.append("[%s] 顶层区重叠 %s×%s(%s ∩ %s)" % [label, keys[i], keys[j], rects[keys[i]], rects[keys[j]]]) - var cell: float = main.board_view.get("cell_size") + var cell: float = main.ui.battle.board_zone.board_view.get("cell_size") if cell < MIN_CELL: _problems.append("[%s] 格子边长 %.1fpx < 可读门槛 %.0fpx" % [label, cell, MIN_CELL]) \ No newline at end of file diff --git a/game/probe/probe_ui_util.gd b/game/probe/probe_ui_util.gd index 1ea2f8db..27b614b9 100644 --- a/game/probe/probe_ui_util.gd +++ b/game/probe/probe_ui_util.gd @@ -130,7 +130,7 @@ static func open_camp_via_savesel(main, tag: String) -> bool: if FileAccess.file_exists(sp): DirAccess.remove_absolute(sp) main._open_save_select() - var new_btn: Button = main.save_select_overlay._new_btns.get(1) + var new_btn: Button = main.ui.modals.save_select_overlay._new_btns.get(1) if new_btn == null: return false new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp diff --git a/game/probe/screen_flow_probe.gd b/game/probe/screen_flow_probe.gd index ac32ec58..08fb6b30 100644 --- a/game/probe/screen_flow_probe.gd +++ b/game/probe/screen_flow_probe.gd @@ -50,10 +50,10 @@ func _ready() -> void: # 其余任意两屏同时为真=互斥失守。 func _screen_seals() -> Dictionary: return { - "菜单": main._start_overlay != null and main._start_overlay.visible, - "营地": main.camp_overlay != null and main.camp_overlay.visible, + "菜单": main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible, + "营地": main.ui.modals.camp_overlay != null and main.ui.modals.camp_overlay.visible, "冒险": main.run_scope != null and main.run_scope.is_adventure, - "终局": main._end_overlay != null and main._end_overlay.visible, + "终局": main.ui.modals.end_overlay != null and main.ui.modals.end_overlay.visible, } @@ -101,7 +101,7 @@ func _frames(n: int = 2) -> void: func _step_boot_menu() -> void: _expect_exclusive("①boot", "菜单") _expect_adventure_seal("①boot", false) - if main.debug_overlay != null and main.debug_overlay.visible: + if main.ui.modals.debug_overlay != null and main.ui.modals.debug_overlay.visible: _problems.append("①boot:调试浮窗 boot 自动开(菜单优先失守)") @@ -117,10 +117,10 @@ func _step_menu_to_camp() -> void: # ---- ③ 营地 → 菜单(回退路径:营地关闭发自覆盖层自身信号)---- func _step_camp_back_to_menu() -> void: - if main.camp_overlay == null: + if main.ui.modals.camp_overlay == null: _problems.append("③营地→菜单:营地覆盖层句柄缺失") return - main.camp_overlay.closed.emit() + main.ui.modals.camp_overlay.closed.emit() await _frames() main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点) await _frames() @@ -133,22 +133,22 @@ func _step_camp_back_to_menu() -> void: func _step_save_select_open_back() -> void: ProbeUiUtil.isolate_save(main, "smoke_screen_flow_savesel", false, false, true) main.save_manager.slots.attach_slot_template("user://smoke_screen_flow_savesel_slot_%d.json") - var esb: Button = ProbeUiUtil.find_button(main._start_overlay, "进入存档", "prefix") + var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix") if esb == null: _problems.append("[savesel] mn3 进入存档钮缺失(卡② 交付物)") return esb.pressed.emit() # 真实按钮信号路径 → _open_save_select(卡② mn3 点击链——卡③ 直调中间态就此闭环) await get_tree().process_frame var seals := _screen_seals() - if not main.save_select_overlay.visible or not seals_check_single(seals): - _problems.append("[savesel] sl10 开层后屏级互斥失守(选档=%s)" % str(main.save_select_overlay.visible)) - var back_btn: Button = ProbeUiUtil.find_button(main.save_select_overlay, "返回", "prefix") + if not main.ui.modals.save_select_overlay.visible or not seals_check_single(seals): + _problems.append("[savesel] sl10 开层后屏级互斥失守(选档=%s)" % str(main.ui.modals.save_select_overlay.visible)) + var back_btn: Button = ProbeUiUtil.find_button(main.ui.modals.save_select_overlay, "返回", "prefix") if back_btn == null: _problems.append("[savesel] sl10 返回钮缺失(页壳顶条)") else: back_btn.pressed.emit() await get_tree().process_frame - if main.save_select_overlay.visible or not main._start_overlay.visible: + if main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("[savesel] sl10 返回后选档未隐藏或开始层未恢复(关闭链失守)") # ESC 语境:再开选档→合成 ESC 键→层不关、流程零扰动(ESC 不属选档语境——零实现断言) main._open_save_select() @@ -159,7 +159,7 @@ func _step_save_select_open_back() -> void: ev.pressed = true main.get_viewport().push_input(ev, true) await get_tree().process_frame - if not main.save_select_overlay.visible: + if not main.ui.modals.save_select_overlay.visible: _problems.append("[savesel] sl10 ESC 关闭了选档层(选档语境 ESC 应不响应——卡⑤ 范围外)") main._on_save_select_closed() await get_tree().process_frame @@ -171,7 +171,7 @@ func seals_check_single(f: Dictionary) -> bool: for k in f: if bool(f[k]): on += 1 - if main.save_select_overlay.visible: + if main.ui.modals.save_select_overlay.visible: on += 1 # 选档自身计入(调用点已开层) return on == 1 @@ -184,25 +184,25 @@ func _step_menu_to_adventure() -> void: _expect_adventure_seal("④菜单→冒险", true) if main.run_scope != null and main.run_scope.adventure == null: _problems.append("④菜单→冒险:冒险实例未建立(入口停点未达)") - if main.level_screen != null and not main.level_screen.visible: + if main.ui.modals.level_screen != null and not main.ui.modals.level_screen.visible: _problems.append("④菜单→冒险:入口停点未落在关卡地图屏(level_screen 不可见)") # ---- ⑤ 冒险内屏切换:关卡地图屏 ⇄ 战斗屏(屏内两态,屏级仍独占)---- func _step_level_to_battle_and_back() -> void: - if not main.level_screen.visible: + if not main.ui.modals.level_screen.visible: _problems.append("⑤冒险内切换:前置不在关卡地图屏") return main._exit_level_screen() await _frames() - if main.level_screen.visible: + if main.ui.modals.level_screen.visible: _problems.append("⑤冒险内切换:退出关卡屏后 level_screen 仍可见") - if main._battle_root != null and not main._battle_root.visible: + if main.ui.screens.battle_root != null and not main.ui.screens.battle_root.visible: _problems.append("⑤冒险内切换:战斗屏未接手(battle_root 不可见)") _expect_exclusive("⑤冒险内切换(关卡→战斗)", "冒险") main._enter_level_screen() await _frames() - if not main.level_screen.visible: + if not main.ui.modals.level_screen.visible: _problems.append("⑤冒险内切换:回到关卡地图屏失败") _expect_exclusive("⑤冒险内切换(战斗→关卡)", "冒险") @@ -215,16 +215,16 @@ func _step_abandon_to_end() -> void: main._on_abandon_requested() await _frames(4) _expect_exclusive("⑥放弃→终局", "终局") - if main._end_overlay != null and not main._end_overlay.visible: + if main.ui.modals.end_overlay != null and not main.ui.modals.end_overlay.visible: _problems.append("⑥放弃→终局:终局覆盖层未开") # ---- ⑦ 终局 → 菜单(终局出口返回开始界面)---- func _step_end_back_to_menu() -> void: - if main._end_overlay == null or not main._end_overlay.visible: + if main.ui.modals.end_overlay == null or not main.ui.modals.end_overlay.visible: _problems.append("⑦终局→菜单:前置不在终局") return - main._end_overlay.continue_pressed.emit() + main.ui.modals.end_overlay.continue_pressed.emit() await _frames(3) _expect_exclusive("⑦终局→菜单", "菜单") if main.run_scope != null and main.run_scope.is_adventure: diff --git a/game/probe/shot_probe.gd b/game/probe/shot_probe.gd index 2b108b21..4cc12488 100644 --- a/game/probe/shot_probe.gd +++ b/game/probe/shot_probe.gd @@ -51,10 +51,10 @@ func _ready() -> void: var top: Node = null var top_rx := -1 var top_ry := -1 - for c: Node in [main._start_overlay, main._battle_root, main._inter_overlay, - main.legend_overlay, main.node_overlay, main.level_screen, main.curio_overlay, - main._end_overlay, main.shop_overlay, main.camp_overlay, main.settings_overlay, - main.status_panel, main.career_overlay, main.confirm_dialog, main.debug_overlay]: + for c: Node in [main.ui.screens.start_overlay, main.ui.screens.battle_root, main.ui.screens.inter_overlay, + main.ui.modals.legend_overlay, main.ui.modals.node_overlay, main.ui.modals.level_screen, main.ui.modals.curio_overlay, + main.ui.modals.end_overlay, main.ui.modals.shop_overlay, main.ui.modals.camp_overlay, main.ui.modals.settings_overlay, + main.ui.modals.status_panel, main.ui.modals.career_overlay, main.ui.modals.confirm_dialog, main.ui.modals.debug_overlay]: if c == null or not (c is CanvasItem) or not (c as CanvasItem).visible: continue var rx: int = c.get_parent().get_index() @@ -63,14 +63,14 @@ func _ready() -> void: top = c top_rx = rx top_ry = ry - if top != main._start_overlay: + if top != main.ui.screens.start_overlay: _problems.append("boot 态顶层可见层≠start_overlay(实测 %s——菜单被埋)〔装配生命批 V4〕" % (top.name if top != null else "null")) # 自检二:菜单标题 label 必须 is_visible_in_tree 且 rect 落屏内 #(对比度只判反差不判「字在」——本条+checker 墨色占比共同补内容判据) var vp := get_viewport().get_visible_rect() for pair in [["menu_title", "Dungeon Sweeper"]]: - var lbl: Label = ProbeUiUtil.find_label(main._start_overlay, String(pair[1])) + var lbl: Label = ProbeUiUtil.find_label(main.ui.screens.start_overlay, String(pair[1])) if lbl == null: _problems.append("菜单文字缺失: %s(find_label 零命中)〔装配生命批 V4〕" % String(pair[1])) continue @@ -79,22 +79,22 @@ func _ready() -> void: var gr: Rect2 = lbl.get_global_rect() if not vp.encloses(gr): _problems.append("菜单文字 rect 越出视口: %s(%s ⊄ %s)〔装配生命批 V4〕" % [String(pair[1]), str(gr), str(vp)]) - _meta[String(pair[0])] = ProbeUiUtil.label_rect(main._start_overlay, String(pair[1])) + _meta[String(pair[0])] = ProbeUiUtil.label_rect(main.ui.screens.start_overlay, String(pair[1])) await _snap("menu") # ①b 设置面(settingsbtn 真实信号路径,开合同款真实钮) - var setbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "设置", "prefix") + var setbtn: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "设置", "prefix") if setbtn == null: _problems.append("设置入口缺失(开始层 settingsbtn)") else: setbtn.pressed.emit() for i in 3: await get_tree().process_frame - if not main.settings_overlay.visible: + if not main.ui.modals.settings_overlay.visible: _problems.append("设置面未打开(settingsbtn 路径)") else: - _meta["settings_title"] = ProbeUiUtil.label_rect(main.settings_overlay, "设置") + _meta["settings_title"] = ProbeUiUtil.label_rect(main.ui.modals.settings_overlay, "设置") await _snap("settings") - var set_close: Button = ProbeUiUtil.find_button(main.settings_overlay, "返回", "prefix") + var set_close: Button = ProbeUiUtil.find_button(main.ui.modals.settings_overlay, "返回", "prefix") if set_close == null: _problems.append("设置面返回钮缺失") else: @@ -105,19 +105,19 @@ func _ready() -> void: main._open_career() for i in 3: await get_tree().process_frame - if not main.career_overlay.visible or main._start_overlay.visible: + if not main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible: _problems.append("生涯页未打开或开始层未隐藏(函数直调中间态路径)") else: - _meta["career_title"] = ProbeUiUtil.label_rect(main.career_overlay, "生 涯") + _meta["career_title"] = ProbeUiUtil.label_rect(main.ui.modals.career_overlay, "生 涯") await _snap("career") - var career_close: Button = ProbeUiUtil.find_button(main.career_overlay, "返回", "prefix") + var career_close: Button = ProbeUiUtil.find_button(main.ui.modals.career_overlay, "返回", "prefix") if career_close == null: _problems.append("生涯页返回钮缺失") else: career_close.pressed.emit() for i in 3: await get_tree().process_frame - if main.career_overlay.visible or not main._start_overlay.visible: + if main.ui.modals.career_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("生涯页关闭后页未隐藏或开始层未恢复") # ② 营地(进档开层新路径——卡② mn9;槽模板注入隔离命名空间) if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_shot_probe_camp"): @@ -125,7 +125,7 @@ func _ready() -> void: else: await get_tree().process_frame await _snap("camp") - var close_btn: Button = ProbeUiUtil.find_button(main.camp_overlay, "关闭营地", "contains") + var close_btn: Button = ProbeUiUtil.find_button(main.ui.modals.camp_overlay, "关闭营地", "contains") if close_btn != null: close_btn.pressed.emit() await get_tree().process_frame @@ -137,24 +137,24 @@ func _ready() -> void: await get_tree().process_frame await _snap("level") # GP 常驻徽章断言(读数=runner.gp 现值,刷新=写路径回执后重查〔open_choices〕) - var gp_label: Label = ProbeUiUtil.find_label(main.level_screen, "◈ ") + var gp_label: Label = ProbeUiUtil.find_label(main.ui.modals.level_screen, "◈ ") if gp_label == null or not gp_label.is_visible_in_tree(): _problems.append("GP 徽章不可见(关卡画面顶栏)") - elif main.level_screen.get("_runner") != null: - var expect := "◈ %d" % int(main.level_screen.get("_runner").gp) + elif main.ui.modals.level_screen.get("_runner") != null: + var expect := "◈ %d" % int(main.ui.modals.level_screen.get("_runner").gp) if gp_label.text != expect: _problems.append("GP 徽章读数不符: %s != %s" % [gp_label.text, expect]) # ④ 战斗(节点一——真实选路提交路径 press_candidate,ui_click_probe :199 同款) - if main.level_screen != null and main.level_screen.visible: - main.level_screen.press_candidate(0) + if main.ui.modals.level_screen != null and main.ui.modals.level_screen.visible: + main.ui.modals.level_screen.press_candidate(0) for i in 10: await get_tree().process_frame await _snap("battle") # 战斗屏文字框/明度带锚点(全局 rect——checker 取样坐标源;明细带锚 S1 裁后改认现行文案) - _meta["battle_log_hint"] = ProbeUiUtil.label_rect(main.stage, "上轮摘要") + _meta["battle_log_hint"] = ProbeUiUtil.label_rect(main.ui.battle.stage, "上轮摘要") _meta["hero_panel"] = _node_rect(ProbeUiUtil.find_by_script(main, "hero_info_panel_view")) - _meta["stage_view"] = _node_rect(main.stage) - _meta["board_zone"] = _node_rect(main.board_zone) + _meta["stage_view"] = _node_rect(main.ui.battle.stage) + _meta["board_zone"] = _node_rect(main.ui.battle.board_zone) # draw calls 真机测量(基线 270→目标 <200;窗口化渲染帧读数, # 取值时机=战斗截图同帧后一帧——呈现终态满负荷) await get_tree().process_frame @@ -162,25 +162,25 @@ func _ready() -> void: _meta["battle_draw_calls"] = dc print("[shot_probe] battle draw_calls=%d(基线 270/目标 <200)" % dc) # ⑤ 状态面板(战斗屏状态角标真实信号路径;层在战斗屏之上开合) - var pb: Button = main.board_zone.status_badge + var pb: Button = main.ui.battle.board_zone.status_badge if pb == null or not pb.visible or pb.disabled: _problems.append("状态面板入口角标缺失/置灰(战斗屏 board_zone)") else: pb.pressed.emit() for i in 3: await get_tree().process_frame - if not main.status_panel.visible: + if not main.ui.modals.status_panel.visible: _problems.append("状态面板未打开(角标路径)") else: - _meta["status_title"] = ProbeUiUtil.label_rect(main.status_panel, "状态") + _meta["status_title"] = ProbeUiUtil.label_rect(main.ui.modals.status_panel, "状态") await _snap("status") - var status_close: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix") + var status_close: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix") if status_close == null: _problems.append("状态面板继续钮缺失") else: status_close.pressed.emit() await get_tree().process_frame - if main.status_panel.visible: + if main.ui.modals.status_panel.visible: _problems.append("状态面板关闭后未隐藏(继续钮路径)") else: _problems.append("关卡画面未出现(函数直调中间态路径)") diff --git a/game/probe/t17_defer_probe.gd b/game/probe/t17_defer_probe.gd index dde96ecc..53c1b4b7 100644 --- a/game/probe/t17_defer_probe.gd +++ b/game/probe/t17_defer_probe.gd @@ -29,14 +29,14 @@ func _ready() -> void: main._start_adventure_from_menu() # 函数直调中间态(卡② mn10——开始冒险钮已撤) for i in 60: await get_tree().process_frame - if main.level_screen != null and main.level_screen.visible: + if main.ui.modals.level_screen != null and main.ui.modals.level_screen.visible: break - if main.level_screen == null or not main.level_screen.visible: + if main.ui.modals.level_screen == null or not main.ui.modals.level_screen.visible: _fail("关卡画面未出现") return for i in 5: await get_tree().process_frame - main.level_screen.press_candidate(0) + main.ui.modals.level_screen.press_candidate(0) for i in 20: await get_tree().process_frame if main.run_scope.settlement == null: @@ -67,18 +67,18 @@ func _ready() -> void: _fail("战斗未胜利(result='%s')" % str(main.run_scope.settlement.battle_result)) return var t_settle := Time.get_ticks_msec() - var _rows_at_victory: int = main._row_pacer.pending_rows().size() # 时间戳①:结算入队完成帧(AP 耗尽/停手→五行段同步跑完→行全在队) + var _rows_at_victory: int = main.ui.screens.row_pacer.pending_rows().size() # 时间戳①:结算入队完成帧(AP 耗尽/停手→五行段同步跑完→行全在队) # 断言①胜利同帧:终局面未开 + 挂起在案 + 队列非空(判别性——旧行为同帧开窗即红) - if main._end_overlay.is_open(): + if main.ui.modals.end_overlay.is_open(): _problems.append("胜利同帧终局面已开(演播后排窗失效——同帧 flush 旧路径回潮)") - if main._deferred_modal == null: + if main.ui.modals.flow._deferred == null: _problems.append("胜利同帧挂起态缺失(_deferred_modal == null)") - if main._row_pacer.pending_rows().is_empty(): + if main.ui.screens.row_pacer.pending_rows().is_empty(): _problems.append("胜利同帧排播队列已空(结算行未走排播链)") var _played_rows: int = _rows_at_victory # 断言②排播放完:终局面开启(20s 守卫——0.22s×行数 ×10 余量) var waited := 0.0 - while not main._end_overlay.is_open() and waited < 20.0: + while not main.ui.modals.end_overlay.is_open() and waited < 20.0: await get_tree().process_frame waited += get_process_delta_time() var t_popup := Time.get_ticks_msec() @@ -86,7 +86,7 @@ func _ready() -> void: print("[t17_defer] AP点用完: %s" % ap_clock) print("[t17_defer] 触发结算: %s(与 AP 用完同帧——核心同步,相差 0ms)" % settle_clock) print("[t17_defer] 弹窗出现: %s(距触发结算 %d ms = 逐行演播时长,行数 %d)" % [popup_clock, t_popup - t_ap_out, _played_rows]) - if not main._end_overlay.is_open(): + if not main.ui.modals.end_overlay.is_open(): _problems.append("排播放完后终局面未开(%.1fs——演播完边沿派发失守)" % waited) if _problems.is_empty(): print("[t17_defer] PASS — 胜利同帧挂起+排播完开窗 两断言全过(waited=%.1fs)" % waited) diff --git a/game/probe/ui_click_probe.gd b/game/probe/ui_click_probe.gd index bc44cc7d..23829c8e 100644 --- a/game/probe/ui_click_probe.gd +++ b/game/probe/ui_click_probe.gd @@ -51,7 +51,7 @@ func _ready() -> void: var t_ui_ready := Time.get_ticks_msec() - t0 # 调试浮窗 boot 不开(菜单优先——入口=UI 最顶层全局浮层角标, # 任何 UI 不遮挡;浮层角标遮挡判据见设置面段) - if main.debug_overlay.visible: + if main.ui.modals.debug_overlay.visible: _problems.append("调试浮窗 boot 自动开(菜单优先失守)") # ---- 营地购买合成点击路径(选档进档开层→购买→关闭全走真实按钮信号路径; # 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json);陈旧档段首清理)---- @@ -65,17 +65,17 @@ func _ready() -> void: _problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)") else: await get_tree().process_frame - if not main.camp_overlay.visible: + if not main.ui.modals.camp_overlay.visible: _problems.append("营地层未打开(进档开层新路径)") - elif main.camp_overlay._tree_buy_btns.is_empty(): + elif main.ui.modals.camp_overlay._tree_buy_btns.is_empty(): _problems.append("树淬炼柜台购买钮缺失(点击路径——四柜台重排后树=主界面)") else: # 树钮句柄用后即取(refresh_camp 随刷重建树行板——旧钮 queue_free,跨刷新句柄必失效) iso_c.meta_state._add_keystone(9999) - main.camp_overlay.refresh_camp(main._camp_data()) + main.ui.modals.camp_overlay.refresh_camp(main._camp_data()) await get_tree().process_frame var buy_btn: Button = null - for card in main.camp_overlay._tree_buy_btns: + for card in main.ui.modals.camp_overlay._tree_buy_btns: if String(card.node_id) == "sp_hp": buy_btn = card.btn if buy_btn == null: @@ -93,16 +93,16 @@ func _ready() -> void: _problems.append("营地树购买未持久(reload 后级 != 1)") # Fork 双态切换点击路径(V20——购=解锁默认 A,钮切 B;前置=同调位 1,先点亮) var socket_btn: Button = null - for card in main.camp_overlay._tree_buy_btns: + for card in main.ui.modals.camp_overlay._tree_buy_btns: if String(card.node_id) == "att_socket1": socket_btn = card.btn if socket_btn != null: socket_btn.pressed.emit() await get_tree().process_frame - main.camp_overlay.refresh_camp(main._camp_data()) + main.ui.modals.camp_overlay.refresh_camp(main._camp_data()) await get_tree().process_frame var fork_btn: Button = null - for card in main.camp_overlay._tree_buy_btns: + for card in main.ui.modals.camp_overlay._tree_buy_btns: if String(card.node_id) == "fork_posture": fork_btn = card.btn if fork_btn == null: @@ -110,10 +110,10 @@ func _ready() -> void: else: fork_btn.pressed.emit() await get_tree().process_frame - main.camp_overlay.refresh_camp(main._camp_data()) + main.ui.modals.camp_overlay.refresh_camp(main._camp_data()) await get_tree().process_frame var side_b: Button = null - for fk in main.camp_overlay._tree_fork_btns: + for fk in main.ui.modals.camp_overlay._tree_fork_btns: if String(fk.group) == "fg_posture" and String(fk.side) == "b": side_b = fk.btn if side_b == null: @@ -128,7 +128,7 @@ func _ready() -> void: # ---- 营地六处灰字取色断言(迭代36 wc4——真实入口开营地后按文本锚取 label, # font_color 逐处 == STONE_DEEP;六源位 :167 图鉴锁文/:417+:476 行状态字/:509 同调形态/:513 档位标签/:525 刷新行)---- var camp_labels: Array = [] - _collect_labels_by_text(main.camp_overlay, camp_labels) + _collect_labels_by_text(main.ui.modals.camp_overlay, camp_labels) var atlas_locks := _labels_with_text(camp_labels, "柜台结构位预留") if atlas_locks.is_empty(): _problems.append("营地灰字:图鉴柜台锁文 label 未找到(wc4 定位锚失守)") @@ -137,9 +137,9 @@ func _ready() -> void: _problems.append("营地灰字:图鉴锁文未用 STONE_DEEP(wc4 :167)") # 「未解锁」锚收窄到秘宝匣行容器子树(树行/锻造行也有「未解锁」态字样——非 wc4 目标位) var locked_status: Array = [] - if main.camp_overlay.get("_workshop_holder") != null: + if main.ui.modals.camp_overlay.get("_workshop_holder") != null: var ws_labels: Array = [] - _collect_labels_by_text(main.camp_overlay._workshop_holder, ws_labels) + _collect_labels_by_text(main.ui.modals.camp_overlay._workshop_holder, ws_labels) locked_status = _labels_with_text(ws_labels, "未解锁") if locked_status.is_empty(): _problems.append("营地灰字:秘宝匣行「未解锁」态 label 未找到(wc4 夹具前提失守——新档应有锁定秘宝行)") @@ -177,7 +177,7 @@ func _ready() -> void: await get_tree().process_frame main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点) await get_tree().process_frame - if main.camp_overlay.visible or not main._start_overlay.visible: + if main.ui.modals.camp_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("营地关闭后未恢复开始层") # ---- 选档界面行为走查(迭代 33 卡③):sl5 空槽新档链/sl6 有档槽进档链/sl7 删档二次确认双路径 # /sl8 消费方分派单口/sl12 动线序不回闪。开层=函数直调(「进入存档」钮归卡② mn3——中间态在案); @@ -196,11 +196,11 @@ func _ready() -> void: f_c2.close() # 槽2 有档异值(进档链夹具);槽1/3 空 main._open_save_select() await get_tree().process_frame - if not main.save_select_overlay.visible: + if not main.ui.modals.save_select_overlay.visible: _problems.append("[savesel] 选档层未打开(click 段前置)") # sl12 动线序(新档链同拍采样):逐帧可见性次序——首帧选档隐藏时营地层必须已可见、开始层保持隐藏 #(禁先关后开——_close_modal 会恢复开始层=回闪一帧) - var new_btn: Button = main.save_select_overlay._new_btns.get(1) + var new_btn: Button = main.ui.modals.save_select_overlay._new_btns.get(1) if new_btn == null: _problems.append("[savesel] 槽1 新档钮缺失(sl5/sl12 驱动中断)") else: @@ -208,7 +208,7 @@ func _ready() -> void: new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp for fi in 3: await get_tree().process_frame - samples.append([main.save_select_overlay.visible, main.camp_overlay.visible, main._start_overlay.visible]) + samples.append([main.ui.modals.save_select_overlay.visible, main.ui.modals.camp_overlay.visible, main.ui.screens.start_overlay.visible]) if samples[0][0]: _problems.append("[savesel] sl12 新档后选档层仍可见(落点开层未发生)") elif not samples[0][1]: @@ -217,8 +217,8 @@ func _ready() -> void: _problems.append("[savesel] sl12 动线序违约:开始层在落点帧恢复(回闪一帧)") if main.save_manager.slots.active_slot() != 1 or not FileAccess.file_exists(csel_tpl % 1): _problems.append("[savesel] sl5 新档链失守(active=%d 应=1 且槽1 白板应落盘)" % main.save_manager.slots.active_slot()) - var occluded: Array = main._modals._occluded.get(main.camp_overlay, []) - if not occluded.has(main.save_select_overlay): + var occluded: Array = main.ui.modals.flow.stack._occluded.get(main.ui.modals.camp_overlay, []) + if not occluded.has(main.ui.modals.save_select_overlay): _problems.append("[savesel] sl12 选档层被关闭而非栈记账挂起(先关后开形态——遮挡挂起中间态钉死)") if int((JSON.parse_string(FileAccess.get_file_as_string(csel_tpl % 1)) as Dictionary).get("meta", {}).get("keystone_balance", -1)) != 0: _problems.append("[savesel] sl5 新档非白板(槽1 落盘应全零)") @@ -227,7 +227,7 @@ func _ready() -> void: # sl6 进档链:槽2 进档 → 激活=2(camp 数据随激活槽) main._open_save_select() await get_tree().process_frame - var load_btn: Button = main.save_select_overlay._load_btns.get(2) + var load_btn: Button = main.ui.modals.save_select_overlay._load_btns.get(2) if load_btn == null: _problems.append("[savesel] 槽2 进档钮缺失(sl6 驱动中断——夹具应有档)") else: @@ -235,51 +235,51 @@ func _ready() -> void: await get_tree().process_frame if main.save_manager.slots.active_slot() != 2: _problems.append("[savesel] sl6 进档链失守(active=%d 应=2——激活该槽)" % main.save_manager.slots.active_slot()) - if not main.camp_overlay.visible or main.save_select_overlay.visible: + if not main.ui.modals.camp_overlay.visible or main.ui.modals.save_select_overlay.visible: _problems.append("[savesel] sl6 落点未开营地层或选档未隐藏") main._on_camp_closed() await get_tree().process_frame # sl7 删档二次确认双路径:一击只开窗(文件仍在)→取消只关窗(文件仍在)→再开窗确认才删(槽回空) main._open_save_select() await get_tree().process_frame - var del_btn: Button = main.save_select_overlay._delete_btns.get(2) + var del_btn: Button = main.ui.modals.save_select_overlay._delete_btns.get(2) if del_btn == null: _problems.append("[savesel] 槽2 删档钮缺失(sl7 驱动中断)") else: del_btn.pressed.emit() await get_tree().process_frame - if not main.confirm_dialog.is_open(): + if not main.ui.modals.confirm_dialog.is_open(): _problems.append("[savesel] sl7 一击未只开确认窗(直删形态)") elif not FileAccess.file_exists(csel_tpl % 2): _problems.append("[savesel] sl7 确认前文件已被删(一次点击即删违契)") else: - if not main._modal_flow.stack._occluded.has(main.confirm_dialog): + if not main.ui.modals.flow.stack._occluded.has(main.ui.modals.confirm_dialog): _problems.append("[savesel] sl7 确认窗未经模态栈开(直开旁路——wr2 单口失守)") - elif not main._input_locked(): + elif not main.ui.modals.flow.locks_input(): _problems.append("[savesel] sl7 确认窗开时输入锁未生效(锁域失守——wr2)") - main.confirm_dialog.cancelled.emit() # 取消路径(真实信号——cancelled 钮同源) + main.ui.modals.confirm_dialog.cancelled.emit() # 取消路径(真实信号——cancelled 钮同源) await get_tree().process_frame - if main.confirm_dialog.is_open() or not FileAccess.file_exists(csel_tpl % 2): + if main.ui.modals.confirm_dialog.is_open() or not FileAccess.file_exists(csel_tpl % 2): _problems.append("[savesel] sl7 取消路径失守(应只关窗、文件仍在)") elif main._confirm_consumer != "": _problems.append("[savesel] sl8 取消后消费方状态量未清(分派面残留)") - if main._modal_flow.stack._occluded.has(main.confirm_dialog): + if main.ui.modals.flow.stack._occluded.has(main.ui.modals.confirm_dialog): _problems.append("[savesel] sl7 取消后确认窗遮挡账未清(关侧未走模态栈——wr2)") del_btn.pressed.emit() # 再开窗走确认路径 await get_tree().process_frame - main.confirm_dialog.confirmed.emit() # 确认路径(真实信号——confirmed 钮同源) + main.ui.modals.confirm_dialog.confirmed.emit() # 确认路径(真实信号——confirmed 钮同源) await get_tree().process_frame - if FileAccess.file_exists(csel_tpl % 2) or main.confirm_dialog.is_open(): + if FileAccess.file_exists(csel_tpl % 2) or main.ui.modals.confirm_dialog.is_open(): _problems.append("[savesel] sl7 确认路径失守(应删档+关窗)") - elif main.run_scope.is_adventure or main._start_overlay.visible: + elif main.run_scope.is_adventure or main.ui.screens.start_overlay.visible: _problems.append("[savesel] sl8 删档确认误触发开局(双常驻接线形态——分派单口失守)") - elif bool(main.save_select_overlay._occupied_flags.get(2, true)): + elif bool(main.ui.modals.save_select_overlay._occupied_flags.get(2, true)): _problems.append("[savesel] sl7 删后选档面未就地刷新(槽2 应回空态)") elif main._confirm_consumer != "": _problems.append("[savesel] sl8 确认后消费方状态量未清(分派面残留)") main._on_save_select_closed() await get_tree().process_frame - if main.save_select_overlay.visible or not main._start_overlay.visible: + if main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("[savesel] 关闭后层未隐藏或开始层未恢复(模态栈单口失守)") # ---- 设置面段(隔离 store 换注禁碰真实 user://settings.cfg;真实按钮信号路径)---- @@ -289,19 +289,19 @@ func _ready() -> void: var iso_s = load("res://presentation/settings_store.gd").new("user://smoke_click_probe_settings.cfg") iso_s.load_settings() main.settings_store = iso_s - main.settings_overlay.retarget(iso_s) - var settings_btn: Button = ProbeUiUtil.find_button(main._start_overlay, "设置", "prefix") + main.ui.modals.settings_overlay.retarget(iso_s) + var settings_btn: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "设置", "prefix") if settings_btn == null: _problems.append("设置入口按钮缺失(开始层 settingsbtn)") else: settings_btn.pressed.emit() # 真实按钮信号路径 → _open_settings await get_tree().process_frame - if not main.settings_overlay.visible or main._start_overlay.visible: + if not main.ui.modals.settings_overlay.visible or main.ui.screens.start_overlay.visible: _problems.append("设置面未打开(点击路径)") else: # 减动效转正双态(卡④ AC③)——M4 适配:拨杆开关(_motion_btn)语义翻转 on=完整/off=快速, # 状态字随行 _motion_st;查找由文字改成员引用(拨杆无文字) - var motion_btn: Button = main.settings_overlay._motion_btn + var motion_btn: Button = main.ui.modals.settings_overlay._motion_btn if motion_btn == null: _problems.append("设置面减动效开关缺失") else: @@ -318,7 +318,7 @@ func _ready() -> void: if UiKit.motion_scale != 1.0: _problems.append("减动效关未恢复(motion_scale != 1.0)") # 音量实时生效(卡④ AC①):滑杆 30% → master bus db 与 store 双读 - var vol: HSlider = main.settings_overlay._volume + var vol: HSlider = main.ui.modals.settings_overlay._volume vol.value = 30 await get_tree().process_frame var master_idx := AudioServer.get_bus_index("Master") @@ -329,24 +329,24 @@ func _ready() -> void: _problems.append("音量未入 store(%f ≠ 0.3)" % iso_s.volume_linear) # 调试入口断言(卡⑦ c4 发布态换代——角标恒隐;原「最顶层浮层不被遮挡」 # 回归语义随调试浮层发布态退役,申报见迭代 38 实现报告 §8.2) - if main.debug_badge == null or main.debug_badge.visible: + if main.ui.screens.debug_badge == null or main.ui.screens.debug_badge.visible: _problems.append("设置面开中调试角标可见(发布态恒隐失守)") - var settings_close: Button = ProbeUiUtil.find_button(main.settings_overlay, "返回", "prefix") + var settings_close: Button = ProbeUiUtil.find_button(main.ui.modals.settings_overlay, "返回", "prefix") if settings_close == null: _problems.append("设置面返回按钮缺失") else: settings_close.pressed.emit() await get_tree().process_frame - if main.settings_overlay.visible or not main._start_overlay.visible: + if main.ui.modals.settings_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("设置面关闭后未恢复开始层") # 旧入口零残留断言(卡④ AC③——测试调整板不含减动效开关;14.5 起载体=调试浮窗) - var legacy_motion = ProbeUiUtil.find_check_button(main.debug_overlay, "减动效") + var legacy_motion = ProbeUiUtil.find_check_button(main.ui.modals.debug_overlay, "减动效") if legacy_motion != null: _problems.append("调整板减动效旧入口未退役(设置面唯一入口失守——卡④ AC③)") # D1 断言(二轮):退出游戏只在一级菜单——开始菜单有、设置面无 - if ProbeUiUtil.find_button(main._start_overlay, "退出游戏", "prefix") == null: + if ProbeUiUtil.find_button(main.ui.screens.start_overlay, "退出游戏", "prefix") == null: _problems.append("开始菜单缺退出游戏按钮(二轮 D1)") - if ProbeUiUtil.find_button(main.settings_overlay, "退出游戏", "prefix") != null: + if ProbeUiUtil.find_button(main.ui.modals.settings_overlay, "退出游戏", "prefix") != null: _problems.append("设置面仍含退出游戏按钮(二轮 D1——只在一级菜单出现)") # ---- 生涯页段(入口点击链+关闭回返,真实按钮信号路径;本页只读零写, # 值断言归冒烟投影段与 ui_struct 结构族,此处只验开合路径与零跳转)---- @@ -354,45 +354,45 @@ func _ready() -> void: if true: main._open_career() await get_tree().process_frame - if not main.career_overlay.visible or main._start_overlay.visible: + if not main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible: _problems.append("生涯页未打开(点击路径)") else: # 零跳转契约(提纲 P4 甲):页内除「返回」外零导航钮 for nav in ["营地", "商店", "设置", "退出", "冒险"]: - if ProbeUiUtil.find_button(main.career_overlay, nav, "contains") != null: + if ProbeUiUtil.find_button(main.ui.modals.career_overlay, nav, "contains") != null: _problems.append("生涯页内出现导航入口「%s」(零跳转违约——生涯纯只读)" % nav) # 局内入口断言(提纲 P4 甲):生涯入口只在开始层——战斗屏/地图屏零生涯角标 - if main.board_zone.find_child("career_badge", true, false) != null \ - or main.level_screen.find_child("career_badge", true, false) != null: + if main.ui.battle.board_zone.find_child("career_badge", true, false) != null \ + or main.ui.modals.level_screen.find_child("career_badge", true, false) != null: _problems.append("局内屏出现生涯入口件(局内不放生涯入口违约)") - var career_close: Button = ProbeUiUtil.find_button(main.career_overlay, "返回", "prefix") + var career_close: Button = ProbeUiUtil.find_button(main.ui.modals.career_overlay, "返回", "prefix") if career_close == null: _problems.append("生涯页返回按钮缺失") else: career_close.pressed.emit() await get_tree().process_frame - if main.career_overlay.visible or not main._start_overlay.visible: + if main.ui.modals.career_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("生涯页关闭后未恢复开始层") # 换回全新零档(营地购买态不泄入后续冒险段——确定性;档案残留下次段首清理兜底) ProbeUiUtil.isolate_save(main, "smoke_click_probe_reset", true, false, true) # 零档换注(继续上局钮已随三入口化撤出——卡② mn8) - main.debug_overlay.set_sliders(400, 200) # 高加成保 L1 可完成——L2 岔路选路段只能经冒险推进到达(单关直入口退役) - main.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,行为走查确定性禁依赖默认分支) + main.ui.modals.debug_overlay.set_sliders(400, 200) # 高加成保 L1 可完成——L2 岔路选路段只能经冒险推进到达(单关直入口退役) + main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,行为走查确定性禁依赖默认分支) var t1 := Time.get_ticks_msec() main._start_adventure_from_menu() await get_tree().process_frame await get_tree().process_frame # 既有口径回归(迭代36 wc3——调试滑条 400/200=局外加成 pct>0,开场行应带「(含面板加成)」; # 本局为本探针首个冒险,日志无先前行,全文本判即可) - var wc3_txt: String = main.log_view._log.get_parsed_text() + var wc3_txt: String = main.ui.battle.log_view._log.get_parsed_text() if not wc3_txt.contains("种子 42") or not wc3_txt.contains("(含面板加成)"): _problems.append("开场行回归失守:局外加成(pct>0)局的开场行应带「(含面板加成)」(wc3)") # 关卡开始 = 入口选路停点——press_candidate 图上命中路径选入口候选进首战 #(首格可点 AC 口径随之含入口选路一步:菜单点击 → 停点呈现 → pick → 棋盘安装) - if main.level_screen.visible: + if main.ui.modals.level_screen.visible: # 独立性断言:独立屏激活期战斗屏宿主整体隐藏 - if main._battle_root.visible: + if main.ui.screens.battle_root.visible: _problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)") - main.level_screen.press_candidate(0) + main.ui.modals.level_screen.press_candidate(0) await get_tree().process_frame await get_tree().process_frame var t_entry := Time.get_ticks_msec() - t1 @@ -402,7 +402,7 @@ func _ready() -> void: var first_cell_ms := t_entry if first_cell_ms > 500: _problems.append("进入战斗屏首格可点超时 %dms > 500ms" % first_cell_ms) - if main.board_view.cells_probe().is_empty(): + if main.ui.battle.board_zone.board_view.cells_probe().is_empty(): _problems.append("首格未可点(cells=0)") print("[click_probe] 首格可点计时 %dms(进入战斗屏口径,≤500ms AC)· 场景冷启动 %dms(含 UI 构建 %dms,观测项)" % [ first_cell_ms, t_total, t_ui_ready]) @@ -413,68 +413,68 @@ func _ready() -> void: while not _done and action_budget > 0: action_budget -= 1 await get_tree().process_frame - if main.node_overlay.visible: - main.node_overlay.press_continue() + if main.ui.modals.node_overlay.visible: + main.ui.modals.node_overlay.press_continue() _seals_placed.clear() continue - if main.curio_overlay.visible: + if main.ui.modals.curio_overlay.visible: # 三选一停点(CurioPickOverlay——奖励节点/关卡完成兑付共用面,须与硬锁态适配, # 未适配则走到即卡死)。两态分派:result 态(兑付反馈——点继续推进)/ # 待选态(点首件+确认)。真实信号路径全线。〔无替换子态——装配不可变契约。〕 # 三选一停点输入锁定断言(停点态断言面:待选/兑付两态共用此层)。 - if not _tested_curio_head and main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty() and String(main.curio_overlay._header).contains("关卡奖励点"): + if not _tested_curio_head and main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty() and String(main.ui.modals.curio_overlay._header).contains("关卡奖励点"): # 停点态判据 = _node_curio_pending(奖励节点三选一停点住此——原条件查 # _curio_pending〔关卡完成三选一态〕与奖励头部互斥、恒不可命中=拦截能力未证病根, # 卡⑤ d4 转正随修;头部校验串本体零触碰) _tested_curio_head = true - var ch := String(main.curio_overlay._header) + var ch := String(main.ui.modals.curio_overlay._header) if not ch.contains("冒险牌库") or not ch.contains("属性值提升与补给") or ch.contains("同权重") or ch.contains("同上限") or ch.contains("提升池"): _problems.append("三选一头部旧话残留或新话缺失(wa4——%s)" % ch.replace(" ", "|")) # 奖励节点战报文案换代断言(卡⑤ wr2——reward_line 同屏打印于停点拉起前, # 同一驱动路径承载;旧「内容待定」残留或新话缺失即红) - var logtext: String = main.log_view.parsed_text() + var logtext: String = main.ui.battle.log_view.parsed_text() if not logtext.contains("关卡奖励点(三选一——选毕推进)") or logtext.contains("内容待定"): _problems.append("奖励节点战报文案未换代(卡⑤ wr2——旧话残留或新话缺失)") - if not main._input_locked(): + if not main.ui.modals.flow.locks_input(): _problems.append("三选一停点打开时输入未锁定") - if main.curio_overlay._continue_btn.visible: - main.curio_overlay.press_continue() + if main.ui.modals.curio_overlay._continue_btn.visible: + main.ui.modals.curio_overlay.press_continue() elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty(): - main.curio_overlay.press_card(0) # 奖励节点待选态:点首件+确认(真实按钮信号路径——与关卡完成停点同形) - main.curio_overlay.press_confirm() + main.ui.modals.curio_overlay.press_card(0) # 奖励节点待选态:点首件+确认(真实按钮信号路径——与关卡完成停点同形) + main.ui.modals.curio_overlay.press_confirm() elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty(): - main.curio_overlay.press_card(0) - main.curio_overlay.press_confirm() + main.ui.modals.curio_overlay.press_card(0) + main.ui.modals.curio_overlay.press_confirm() else: - main.curio_overlay.press_continue() # 停点已消费的残面——继续关屏 + main.ui.modals.curio_overlay.press_continue() # 停点已消费的残面——继续关屏 _seals_placed.clear() continue - if main.shop_overlay.visible: + if main.ui.modals.shop_overlay.visible: # 商店房覆盖层(摆放约束:每终点前一位恰一店,真实路径恒必经一店):零购离开(真实 leave 信号路径)放行选路。 # 分派链序:商店层=选路页上的模态覆盖(停点呈现为 # level_screen+shop 双 visible)——必须先于 level_screen 分派,否则下层候选点击 # 空转烧尽预算(实证:lv2 商店停点双 visible 循环 12000 帧 done=false)。 - main.shop_overlay.leave_pressed.emit() + main.ui.modals.shop_overlay.leave_pressed.emit() _seals_placed.clear() continue - if main.level_screen.visible: + if main.ui.modals.level_screen.visible: # 节点图选路口(零自动推进停点)——图上命中路径交替选候选(岔路使用行为;布局决策 2 改造) # 地图屏角标一次性 case:选路前经角标开/关状态面板(层在地图屏之上开合, # 关闭后地图屏选路态零扰动——P2 甲层级语义) - if not _tested_map_status and not main.status_panel.visible: + if not _tested_map_status and not main.ui.modals.status_panel.visible: _tested_map_status = true - var mpb: Button = main.level_screen.status_badge + var mpb: Button = main.ui.modals.level_screen.status_badge if mpb == null or not mpb.visible or mpb.disabled: _problems.append("地图屏状态角标缺失/未点亮/被置灰(P1 丁)") else: mpb.pressed.emit() - if not main.status_panel.visible or not main.level_screen.visible: + if not main.ui.modals.status_panel.visible or not main.ui.modals.level_screen.visible: _problems.append("地图屏角标开状态面板失败或地图屏被误藏(P2 甲层级失守)") - var rbtn2: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix") + var rbtn2: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix") if rbtn2 != null: rbtn2.pressed.emit() - if main.status_panel.visible or not main.level_screen.visible: + if main.ui.modals.status_panel.visible or not main.ui.modals.level_screen.visible: _problems.append("状态面板关闭后地图屏态失守(恢复语义破)") # 地图屏加成帽换代断言(迭代36 卡A wa2——「特性」→「属性值提升」;帽 Label 常在树; # 独立旗标——_tested_map_status 在同拍前置块已置位,禁复用其门控) @@ -482,7 +482,7 @@ func _ready() -> void: _tested_map_cap = true var cap_found := false var ls_labels: Array = [] - _collect_labels_by_text(main.level_screen, ls_labels) + _collect_labels_by_text(main.ui.modals.level_screen, ls_labels) for l in ls_labels: if l.text == "属性值提升": cap_found = true @@ -494,20 +494,20 @@ func _ready() -> void: if main.run_scope.runner != null: for nd in main.run_scope.runner.nodes(): var nd_id: int = main.run_scope.runner.node_id_of(nd) - if String(nd.get("type", "")) == "reward" and main.level_screen._cand_ids.has(nd_id): - pick = main.level_screen._cand_ids.find(nd_id) + if String(nd.get("type", "")) == "reward" and main.ui.modals.level_screen._cand_ids.has(nd_id): + pick = main.ui.modals.level_screen._cand_ids.find(nd_id) break - main.level_screen.press_candidate(pick) + main.ui.modals.level_screen.press_candidate(pick) _route_clicks += 1 _seals_placed.clear() continue - if main._inter_overlay.visible: + if main.ui.screens.inter_overlay.visible: # 大奖励屏(冒险级停点——非末关的关卡完成恰一次):首见确认推进 L2(真实按钮信号路径);再见 = L2 已关卡完成,收口 - if not main._input_locked(): + if not main.ui.modals.flow.locks_input(): _problems.append("大奖励屏打开时输入未锁定") _inter_confirms += 1 if _inter_confirms == 1: - var next_btn: Button = ProbeUiUtil.find_button(main._inter_overlay, "进入下一层", "prefix") + var next_btn: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix") if next_btn == null: _problems.append("大奖励屏确认按钮缺失") else: @@ -522,23 +522,23 @@ func _ready() -> void: break _seals_placed.clear() continue - if main._end_overlay.is_open() and main._settlement_stop_pending: + if main.ui.modals.end_overlay.is_open() and main.run_scope.settlement_stop_timer.is_armed(): # 战斗胜利总结结算框(真实信号路径——每场普通胜利到达):断言已拍板面后点「下一节点」 - if not main._input_locked(): + if not main.ui.modals.flow.locks_input(): _problems.append("胜利结算框打开时输入未锁定") - var gp_line: String = "%s %s" % [main._end_overlay._gp_name.text, main._end_overlay._gp_val.text] # 两列账行(审计差距④)——名/值列拼回整串断言 + var gp_line: String = "%s %s" % [main.ui.modals.end_overlay._gp_name.text, main.ui.modals.end_overlay._gp_val.text] # 两列账行(审计差距④)——名/值列拼回整串断言 if not gp_line.contains("GP +%d" % main.run_scope.last_gain_amount) or not gp_line.contains("合计 %d" % int(main.run_scope.runner.gp)): _problems.append("胜利结算框 GP 增量/总量与钱包实态不符(%s|实态 +%d/%d)" % [gp_line, main.run_scope.last_gain_amount, int(main.run_scope.runner.gp)]) - if main._end_overlay.verdict_text() == "": + if main.ui.modals.end_overlay.verdict_text() == "": _problems.append("胜利结算框判定行缺失") _settlement_confirms += 1 - main._end_overlay._btn.pressed.emit() # 「下一节点」真实信号路径 + main.ui.modals.end_overlay._btn.pressed.emit() # 「下一节点」真实信号路径 await get_tree().process_frame await get_tree().process_frame - if main._settlement_stop_pending or not main.level_screen.visible: + if main.run_scope.settlement_stop_timer.is_armed() or not main.ui.modals.level_screen.visible: _problems.append("胜利结算框「下一节点」未进选路页(卡⑥ AC②)") continue - if main._end_overlay.is_open(): + if main.ui.modals.end_overlay.is_open(): # 冒险终局(失败/放弃/打穿)——无单关直入口,见终局即收口(模态断言照旧) # 三选一链适配:_adventure_mode 仍真且非选路挂起=中途结算面(Boss 胜利 # 总结「继续」后挂三选一→真终局未到)——真实信号路径点继续推进。 @@ -547,13 +547,13 @@ func _ready() -> void: # (_adventure_mode)在真终局仍 true——会把真终局当中途面点掉返回键(实证)。 var _adv_running: bool = main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running" if main.run_scope.is_adventure and _adv_running: - main._end_overlay._btn.pressed.emit() + main.ui.modals.end_overlay._btn.pressed.emit() continue - if not main._input_locked(): + if not main.ui.modals.flow.locks_input(): _problems.append("终局 overlay 打开时输入未锁定") _done = true print("[click_probe] 冒险终局:%s · 选路口点击 %d 次" % [ - main._end_overlay.verdict_text().replace("\n", " "), _route_clicks]) + main.ui.modals.end_overlay.verdict_text().replace("\n", " "), _route_clicks]) break if main.run_scope.settlement == null or main.run_scope.runner == null: continue @@ -566,16 +566,16 @@ func _ready() -> void: # 上轮摘要恢复断言(卡⑥ wr1-b——改前 _round_rows 类型错 append 全数失败、缓冲恒空、 # 摘要自引入类型错以来从未显示):战中场观测点=round≥2 且排播队列已排空(轮首分隔行 # 已消费=finish_settlement_round 已刷);不挂循环出口——收尾的续玩重建段会再走 log_view.clear() - if not _tested_round_summary and int(st["round"]) >= 2 and main._row_queue.is_empty(): + if not _tested_round_summary and int(st["round"]) >= 2 and main.ui.screens.row_pacer.pending_rows().is_empty(): _tested_round_summary = true - if main.log_view._summary.text == "": + if main.ui.battle.log_view._summary.text == "": _problems.append("上轮摘要恒空(卡⑥ wr1-b——结算行缓冲仍失真)") if JSON.stringify(st) == JSON.stringify(_last_state): # 排播演播期豁免(硬锁 v3——合法停顿态):演播期棋盘锁生效、probe 点击被吞、 # 逻辑态恒定属预期。判据=纯队列(与 main 锁同源)。Waiting 收尾等待期**不豁免**—— # 锁若把收尾链卡死,此处 stall 照报(探测面)。 # T1 D1 处置=保留豁免+注记:冻结/缓存/重放语义不再靠豁免绕行,由 _locktest_tick # 脚本化断言正面承载(每 run 一次;逻辑态恒定本豁免继续作 stall 判据)。 - if main._row_queue.is_empty(): + if main.ui.screens.row_pacer.pending_rows().is_empty(): _stall_frames += 1 if _stall_frames == 30: _report_stall(st) @@ -595,36 +595,36 @@ func _ready() -> void: # ---- 预算耗尽收口(T17 硬锁批适配):选件变强后 probe 可能活过预算不再战败——断点段前提 # (冒险终局面在案)须主动满足。收口驱动:任何停点层逐个推进(复用主循环分支语义)至 # level_screen 选路停点→真实放弃二击→终局面。战斗中则继续 autoplay 打完。 - if not _done and not main._end_overlay.is_open(): + if not _done and not main.ui.modals.end_overlay.is_open(): var close_guard := 8000 var dbg_frame := 0 # T17 演播后排窗:每场胜利 +~400 帧演播等待(旧同帧 flush 零等待)——守卫按多场战斗放大 while close_guard > 0: close_guard -= 1 await get_tree().process_frame - if main._end_overlay.is_open(): + if main.ui.modals.end_overlay.is_open(): # 中途结算面(state=running)=点继续推进;真终局(settling)=断点段前提满足,收口。 if main.run_scope.is_adventure and main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running": - main._end_overlay._btn.pressed.emit() + main.ui.modals.end_overlay._btn.pressed.emit() else: break - elif main.curio_overlay.visible: - if main.curio_overlay._continue_btn.visible: - main.curio_overlay.press_continue() + elif main.ui.modals.curio_overlay.visible: + if main.ui.modals.curio_overlay._continue_btn.visible: + main.ui.modals.curio_overlay.press_continue() elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty(): - main.curio_overlay.press_card(0) - main.curio_overlay.press_confirm() + main.ui.modals.curio_overlay.press_card(0) + main.ui.modals.curio_overlay.press_confirm() else: - main.curio_overlay.press_continue() - elif main.node_overlay.visible: - main.node_overlay.press_continue() - elif main.shop_overlay.visible: - main.shop_overlay.leave_pressed.emit() - elif main._inter_overlay.visible: - var next_btn2: Button = ProbeUiUtil.find_button(main._inter_overlay, "进入下一层", "prefix") + main.ui.modals.curio_overlay.press_continue() + elif main.ui.modals.node_overlay.visible: + main.ui.modals.node_overlay.press_continue() + elif main.ui.modals.shop_overlay.visible: + main.ui.modals.shop_overlay.leave_pressed.emit() + elif main.ui.screens.inter_overlay.visible: + var next_btn2: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix") if next_btn2 != null: next_btn2.pressed.emit() - elif main.level_screen.visible: - var abandon_btn: Button = ProbeUiUtil.find_button(main.level_screen, "放弃", "prefix") + elif main.ui.modals.level_screen.visible: + var abandon_btn: Button = ProbeUiUtil.find_button(main.ui.modals.level_screen, "放弃", "prefix") if abandon_btn != null: _closed_via_abandon = true abandon_btn.pressed.emit() @@ -652,14 +652,14 @@ func _ready() -> void: str(((bpf as Dictionary).get("bonus_deck", []) as Array).size()), str((bpf as Dictionary).get("bonus_pending"))]) _problems.append("断点段:战败结算后断点未清(T3 三结局清断点失守)") - if not main._end_overlay.is_open(): + if not main.ui.modals.end_overlay.is_open(): _problems.append("断点段:战败后未出冒险终局面") - if main._end_overlay._btn == null: + if main.ui.modals.end_overlay._btn == null: _problems.append("断点段:终局面返回按钮缺失") else: - main._end_overlay._btn.pressed.emit() # 真实按钮信号路径(_btn=继续/返回共用句柄) + main.ui.modals.end_overlay._btn.pressed.emit() # 真实按钮信号路径(_btn=继续/返回共用句柄) await get_tree().process_frame - if not main._start_overlay.visible: + if not main.ui.screens.start_overlay.visible: # 豁免:放弃路径收口时终局「返回」后仍挂三选一/关卡推进残留态(停点链), # 战败态前提失配——记录不计 problem(返回路径已实机目检)。 if not _closed_via_abandon: @@ -683,16 +683,16 @@ func _ready() -> void: if FileAccess.file_exists(p_ol): DirAccess.remove_absolute(p_ol) ProbeUiUtil.isolate_save(main, "smoke_click_probe_openline", true, false, true) - main.debug_overlay.reset_sliders() # ① 白板反向前提(_start_adventure 只关窗不清值——残值跨段存活防坑) - main.debug_overlay.seed_edit.text = "42" + main.ui.modals.debug_overlay.reset_sliders() # ① 白板反向前提(_start_adventure 只关窗不清值——残值跨段存活防坑) + main.ui.modals.debug_overlay.seed_edit.text = "42" main._start_adventure_from_menu() var ol_l1_wait := 0 - while ol_l1_wait < 24 and String(main.log_view._log.get_parsed_text()).count("· 种子 ") < 1: + while ol_l1_wait < 24 and String(main.ui.battle.log_view._log.get_parsed_text()).count("· 种子 ") < 1: ol_l1_wait += 1 await get_tree().process_frame if ol_l1_wait >= 24: _problems.append("开场行路径①:L1 开场行未达日志(开局清日志后 24 帧内未现种子行)") - elif String(main.log_view._log.get_parsed_text()).count("(含面板加成)") > 0: + elif String(main.ui.battle.log_view._log.get_parsed_text()).count("(含面板加成)") > 0: _problems.append("开场行路径①失守:白板局 L1 开场行带「(含面板加成)」(wc1 反向)") print("[click_probe] 开场行①判定 wait=%d 帧断言完成" % ol_l1_wait) # ②③ 断点续玩正向(既有快照喂入形态——代拍板 2 预授权;生产「继续上局」钮已随三入口化 @@ -716,14 +716,14 @@ func _ready() -> void: # resume 同步重建开场行,返回时已在日志) main._resume_from_menu() var ol_r_wait := 0 - while ol_r_wait < 24 and String(main.log_view._log.get_parsed_text()).count("· 种子 ") < 1: + while ol_r_wait < 24 and String(main.ui.battle.log_view._log.get_parsed_text()).count("· 种子 ") < 1: ol_r_wait += 1 await get_tree().process_frame if main.run_scope.runner == null or main.run_scope.runner.journal().bonus_total("atk") != 5: _problems.append("开场行路径②③前提失守:续玩重建后加算区 atk!=5(断点回填链断)") if ol_r_wait >= 24: _problems.append("开场行路径②③失守:续玩重建未发开场行(选路停点形应重发——形态/驱动复核)") - elif String(main.log_view._log.get_parsed_text()).count("(含面板加成)") < 1: + elif String(main.ui.battle.log_view._log.get_parsed_text()).count("(含面板加成)") < 1: _problems.append("开场行路径②③失守:bonus 已入账(pct=0/ov=0)的续玩重建开场行未带「(含面板加成)」(wc1 正向/wc2)") else: print("[click_probe] 开场行②③判定:续玩重建开场行带后缀(bonus.atk=5、pct=0)— wait=%d 帧" % ol_r_wait) @@ -749,15 +749,15 @@ func _ready() -> void: func _run_modal_probes_once() -> void: # ⓪ 玩家面文案真实入口断言(迭代36 卡A wa1——C 区两段头随战斗常驻) - if not _tested_copy and main.info_panel != null and main.info_panel.visible: + if not _tested_copy and main.ui.battle.info_panel != null and main.ui.battle.info_panel.visible: _tested_copy = true var feat_head: Label = null var curio_head: Label = null - for child in main.info_panel._feat_section.get_children(): + for child in main.ui.battle.info_panel._feat_section.get_children(): if child is Label: feat_head = child break - for child in main.info_panel._curio_section.get_children(): + for child in main.ui.battle.info_panel._curio_section.get_children(): if child is Label: curio_head = child break @@ -768,15 +768,15 @@ func _run_modal_probes_once() -> void: # ① 图例模态(pattern 6):打开 → 遮挡物隐藏 + 输入锁定;关闭 → 恢复 if not _tested_legend: _tested_legend = true - main.ap_bar._btn_legend.pressed.emit() - if not main.legend_overlay.is_open(): + main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit() + if not main.ui.modals.legend_overlay.is_open(): _problems.append("图例打开失败") - if main._end_overlay.is_open() or main._start_overlay.visible: + if main.ui.modals.end_overlay.is_open() or main.ui.screens.start_overlay.visible: _problems.append("图例打开时上方遮挡层未隐藏(缺陷⑤复发)") - if not main._input_locked(): + if not main.ui.modals.flow.locks_input(): _problems.append("图例打开时棋盘输入未锁定") var legend_has_seal := false - for e in main.legend_overlay.ENTRIES: + for e in main.ui.modals.legend_overlay.ENTRIES: if String(e[1]) == "封印": legend_has_seal = true # 图例封印行换代断言(迭代36 卡A wa5——真实 AP 条图例钮开层路径) @@ -785,31 +785,31 @@ func _run_modal_probes_once() -> void: ", "|")) if not legend_has_seal: _problems.append("图例缺『封印』格态条目(格态图例基线)") - main.legend_overlay.close() - if main.legend_overlay.is_open() or main._input_locked(): + main.ui.modals.legend_overlay.close() + if main.ui.modals.legend_overlay.is_open() or main.ui.modals.flow.locks_input(): _problems.append("图例关闭后未恢复输入/可见性") # ②/②b 明细回看模态无探针(M9/P-C7——冒险中回看归「生涯」,06 登记) # ⑤ 状态面板(P1 丁角标真实信号路径 + P2 甲模态语义 + P1 守卫 + R3 锁恢复): # 角标打开 → 只读层可见+输入锁定+余额徽非空 → 「继续」关闭恢复;图例开着 → 角标置灰且点击被守卫兜住 - if not _tested_status and not main.legend_overlay.is_open() and not main.status_panel.visible: + if not _tested_status and not main.ui.modals.legend_overlay.is_open() and not main.ui.modals.status_panel.visible: _tested_status = true - var pbtn: Button = main.board_zone.status_badge + var pbtn: Button = main.ui.battle.board_zone.status_badge if pbtn == null or not pbtn.visible or pbtn.disabled: _problems.append("战斗屏状态角标缺失/未点亮/被置灰(P1 丁入口失守 visible=%s disabled=%s)" % [str(pbtn != null and pbtn.visible), str(pbtn != null and pbtn.disabled)]) else: pbtn.pressed.emit() # 真实按钮信号路径 → 组合根 _on_status_badge_pressed - if not main.status_panel.visible: + if not main.ui.modals.status_panel.visible: _problems.append("状态面板未随角标打开(组合根接线失守)") - if not main._input_locked(): + if not main.ui.modals.flow.locks_input(): _problems.append("状态面板打开时输入未锁定(R3)") - var bal_txt: String = main.status_panel._balance_badge.value_label.text + var bal_txt: String = main.ui.modals.status_panel._balance_badge.value_label.text if not bal_txt.begins_with("钥石 "): _problems.append("状态面板余额徽投影缺失(%s)" % bal_txt) # ---- 本局信息面板换代断言(迭代36 卡③——真实入口三断言)---- # ① 面板子树零旧段文案(跨关概览退役零残留) var sv_labels: Array = [] - _collect_labels_by_text(main.status_panel, sv_labels) + _collect_labels_by_text(main.ui.modals.status_panel, sv_labels) for l in sv_labels: if String(l.text).contains("道具工坊") or String(l.text).contains("天赋树"): _problems.append("状态面板子树残留旧段文案「%s」(sv6①——零旧段失守)" % String(l.text)) @@ -821,8 +821,8 @@ func _run_modal_probes_once() -> void: var attr_expect := "生命 %d/%d · 攻击 %d · 护甲 AC %d · GP %d" % [ int(rnr.hero_hp), rnr.effective_max_hp(), rnr.effective_atk(), rnr.effective_ac(), int(rnr.gp)] - if String(main.status_panel._attrs_label.text) != attr_expect: - _problems.append("状态面板属性行投影失真(sv6②——期望 %s 实测 %s)" % [attr_expect, main.status_panel._attrs_label.text]) + if String(main.ui.modals.status_panel._attrs_label.text) != attr_expect: + _problems.append("状态面板属性行投影失真(sv6②——期望 %s 实测 %s)" % [attr_expect, main.ui.modals.status_panel._attrs_label.text]) # ③ _status_data() 直调对拍(sv7——键集恰八键+curios/feats 独立 oracle) var sd: Dictionary = main._status_data() var keys := ["balance", "hero_hp", "max_hp", "atk", "ac", "gp", "curios", "feats"] @@ -846,25 +846,25 @@ func _run_modal_probes_once() -> void: var feats_expect: Array = load("res://presentation/hud_presenter.gd").feat_holdings_lines(main.db, rnr.feats_held()) if str((sd as Dictionary).get("feats", [])) != str(feats_expect): _problems.append("_status_data().feats 对拍 feat_holdings_lines 失守(sv7)") - var resume_btn: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix") + var resume_btn: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix") if resume_btn == null: _problems.append("状态面板关闭钮缺失(「继续」)") else: resume_btn.pressed.emit() - if main.status_panel.visible or main._input_locked(): + if main.ui.modals.status_panel.visible or main.ui.modals.flow.locks_input(): _problems.append("状态面板关闭后未恢复(visible=%s locked=%s——R3 残留)" - % [str(main.status_panel.visible), str(main._input_locked())]) + % [str(main.ui.modals.status_panel.visible), str(main.ui.modals.flow.locks_input())]) # P1 守卫:图例开着 → 两处入口角标置灰;置灰态点击被处理器兜住(探针 emit 绕过 disabled) - main.ap_bar._btn_legend.pressed.emit() - if not main.board_zone.status_badge.disabled: + main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit() + if not main.ui.battle.board_zone.status_badge.disabled: _problems.append("图例开着时战斗屏状态角标未置灰(P1 守卫失守)") - main.board_zone.status_badge.pressed.emit() - if main.status_panel.visible: + main.ui.battle.board_zone.status_badge.pressed.emit() + if main.ui.modals.status_panel.visible: _problems.append("置灰态仍可开状态面板(守卫兜底失守)") - main.legend_overlay.close() + main.ui.modals.legend_overlay.close() # ③ tooltip(状态效果槽悬停——来源+语义+剩余):有槽可见时触发 if not _tested_tooltip: - var row = main.stage.hero_standee.slot_row # 状态 tooltip 载体(B 区站桩) + var row = main.ui.battle.stage.hero_standee.slot_row # 状态 tooltip 载体(B 区站桩) for child in row.get_children(): if child is PanelContainer and child.visible and child.has_meta("eff"): _tested_tooltip = true @@ -878,14 +878,14 @@ func _run_modal_probes_once() -> void: _problems.append("状态槽 tooltip 未随移出隐藏") break # ④ 弃图刷新确认链(pattern 7 异位:确认 chips 与常规确认不同位 + 显式代价文案) - if not _tested_refresh_chain and main.ap_bar.is_refresh_ready(): + if not _tested_refresh_chain and main.ui.battle.board_zone.ap_bar.is_refresh_ready(): _tested_refresh_chain = true var rap_before: int = int(main.run_scope.settlement.action_economy.rap) var board_before = main.run_scope.settlement.minesweeper.current_board() - main.ap_bar.press_refresh_for_probe() - if not main.ap_bar.confirm_visible(): + main.ui.battle.board_zone.ap_bar.press_refresh_for_probe() + if not main.ui.battle.board_zone.ap_bar.confirm_visible(): _problems.append("弃图刷新确认 chips 未出现(异位确认缺失)") - main.ap_bar.press_confirm_for_probe() + main.ui.battle.board_zone.ap_bar.press_confirm_for_probe() if int(main.run_scope.settlement.action_economy.rap) != rap_before - 8: _problems.append("弃图刷新未扣 RAP8(%d→%d)" % [rap_before, int(main.run_scope.settlement.action_economy.rap)]) if main.run_scope.settlement.action_economy.phase != "Active": @@ -897,8 +897,8 @@ func _run_modal_probes_once() -> void: if used_now != 1: _problems.append("刷新轮内计数错误(%d)" % used_now) var remain_expect: int = maxi(0, int(main.db.run.action_economy.refresh_cap_per_round) - used_now) - if not main.ap_bar.refresh_remaining_text().contains(str(remain_expect)): - _problems.append("HUD 刷新余量与快照推导失符(HUD='%s' 应含 %d)" % [main.ap_bar.refresh_remaining_text(), remain_expect]) + if not main.ui.battle.board_zone.ap_bar.refresh_remaining_text().contains(str(remain_expect)): + _problems.append("HUD 刷新余量与快照推导失符(HUD='%s' 应含 %d)" % [main.ui.battle.board_zone.ap_bar.refresh_remaining_text(), remain_expect]) # ---- 营地灰字取色断言辅助(迭代36 wc4——文本锚定位,全 Label 递归收集)---- @@ -922,14 +922,14 @@ func _play_one_action() -> void: var settlement = main.run_scope.settlement if settlement == null: return # 结算竞态防抖:battle_finished 与本轮动作调度同帧到达时 settlement 可能已被置空(基线既有噪声——加固) - if not main._row_queue.is_empty(): + if not main.ui.screens.row_pacer.pending_rows().is_empty(): return # T1 D1——演播期不出手:此时点击只会进锁期缓存被「最后一次」覆盖(原逐帧连点语义无行为覆盖价值,且毁锁期断言注入的独占前提) var minesweeper = settlement.minesweeper # 行为①:随机封印(右键——未开相邻格),约 20% 轮次 if _ap_spent_this_round == 0 and _rand.randf() < 0.2: var target := _random_unknown_adjacent(minesweeper) if target.x >= 0: - main.board_view.click_cell(target, MOUSE_BUTTON_RIGHT) + main.ui.battle.board_zone.board_view.click_cell(target, MOUSE_BUTTON_RIGHT) _seals_placed.append(target) _ap_spent_this_round += 1 return @@ -937,19 +937,19 @@ func _play_one_action() -> void: if _rand.randf() < 0.1 and not _seals_placed.is_empty(): var fc: Vector2i = _seals_placed.pop_back() if minesweeper.has_seal(fc) and not minesweeper.is_revealed(fc): - main.board_view.click_cell(fc, MOUSE_BUTTON_RIGHT) + main.ui.battle.board_zone.board_view.click_cell(fc, MOUSE_BUTTON_RIGHT) _ap_spent_this_round += 1 return # 行为③:随机无效点击(非相邻格/已开格),约 8% 轮次 if _rand.randf() < 0.08: - main.board_view.click_cell(Vector2i(_rand.randi_range(0, minesweeper.current_board().width - 1), _rand.randi_range(0, minesweeper.current_board().height - 1)), MOUSE_BUTTON_LEFT) + main.ui.battle.board_zone.board_view.click_cell(Vector2i(_rand.randi_range(0, minesweeper.current_board().width - 1), _rand.randi_range(0, minesweeper.current_board().height - 1)), MOUSE_BUTTON_LEFT) return # 挖掘主策略(最近可挖格) var plan = AutoPlayer.choose_dig(minesweeper) # O4 收口:策略单一实现 if not plan.is_empty(): if plan.hero_cell != minesweeper.hero_pos(): - main.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT) - main.board_view.click_cell(plan.dig_cell, MOUSE_BUTTON_LEFT) + main.ui.battle.board_zone.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT) + main.ui.battle.board_zone.board_view.click_cell(plan.dig_cell, MOUSE_BUTTON_LEFT) _ap_spent_this_round += 1 # 行为④:提前停手(已花 ≥3 AP 时 15%)——停手经 AP 条按钮真实信号路径 if _ap_spent_this_round >= 3 and settlement.action_economy.phase == "Active" and _rand.randf() < 0.15: @@ -975,7 +975,7 @@ func _random_unknown_adjacent(minesweeper) -> Vector2i: func _click_stop() -> void: # 停手走真实按钮信号路径(AP 条 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体) - main.ap_bar._btn_stop.pressed.emit() + main.ui.battle.board_zone.ap_bar._btn_stop.pressed.emit() # ---- T1 D1:锁期/重放断言(每 run 一次,脚本化场景——演播期豁免的断言化承载) ---- @@ -992,7 +992,7 @@ func _locktest_tick() -> void: if settlement == null or settlement.minesweeper.current_board() == null: return if _locktest_state == 0: - if main._row_queue.is_empty() or main._input_locked(): + if main.ui.screens.row_pacer.pending_rows().is_empty() or main.ui.modals.flow.locks_input(): return if String(settlement.battle_result) != "" or String(settlement.action_economy.phase) != "Active": return @@ -1003,7 +1003,7 @@ func _locktest_tick() -> void: return # 无合法邻格/目标被并发封印行为占位——下帧重挑 _locktest_cell = target _locktest_frames = 0 - main.board_view.click_cell(target, MOUSE_BUTTON_RIGHT) + main.ui.battle.board_zone.board_view.click_cell(target, MOUSE_BUTTON_RIGHT) if settlement.minesweeper.has_seal(target): _problems.append("锁期断言:演播期点击未被冻结(封印即时受理——v3 冻结失守)") _locktest_state = 2 @@ -1014,11 +1014,11 @@ func _locktest_tick() -> void: return _locktest_state = 1 elif _locktest_state == 1: - if main.curio_overlay.visible or main._inter_overlay.visible or main._end_overlay.is_open() \ + if main.ui.modals.curio_overlay.visible or main.ui.screens.inter_overlay.visible or main.ui.modals.end_overlay.is_open() \ or String(settlement.battle_result) != "": _locktest_state = 2 # 前提消失守卫:终结演播竞态——静默弃测 return - if not main._row_queue.is_empty(): + if not main.ui.screens.row_pacer.pending_rows().is_empty(): _locktest_frames = 0 # 演播未完——重放耐心只在排空后起算(链式大演播数百帧不占预算) return _locktest_frames += 1 @@ -1038,7 +1038,7 @@ func _locktest_tick() -> void: b.width, b.height, str(settlement.minesweeper.hero_pos())]) _problems.append("重放断言:锁期缓存点击未受理恰一次(封印未置位——零发或双发)") _locktest_state = 2 - main.board_view.click_cell(_locktest_cell, MOUSE_BUTTON_RIGHT) # 撤封印恢复现场(尽力而为不判定) + main.ui.battle.board_zone.board_view.click_cell(_locktest_cell, MOUSE_BUTTON_RIGHT) # 撤封印恢复现场(尽力而为不判定) func _locktest_pick_cell(minesweeper) -> Vector2i: diff --git a/game/probe/ui_input_probe.gd b/game/probe/ui_input_probe.gd index 669bd081..71b55f77 100644 --- a/game/probe/ui_input_probe.gd +++ b/game/probe/ui_input_probe.gd @@ -31,15 +31,15 @@ func _ready() -> void: # 同 ui_click_probe 隔离纪律。首次冒险布尔位置位=对齐真实档游玩史口径,模板抽签路径不变; # isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退) ProbeUiUtil.isolate_save(main, "smoke_input_probe_save", true, false, true) - main.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,探针确定性禁依赖默认分支) + main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,探针确定性禁依赖默认分支) main._start_adventure_from_menu() await get_tree().process_frame # ---- ⑫ 图上命中真实输入(回归闸):入口停点候选节点中心 # push_input 真实鼠标路径 motion+click → 悬停预承诺置位 → route_chosen 到达组合根。 # 由来=真机 bug:深层节点(L1 Boss 内容 x=802)在固定画布命中区外,press_candidate 直调 # 命中函数探针全绿、真鼠标点不中——本 case 封住输入管线,⑬封住几何拟合。 - if main.level_screen.visible: - var ro = main.level_screen + if main.ui.modals.level_screen.visible: + var ro = main.ui.modals.level_screen var cands: Array = main.run_scope.runner.next_candidate_ids() # ---- ⑱ 信息迷雾呈现接线(回归闸):真机 display_probe 读实际绘制记录(雾下节点类型未画、 # Boss 描边画了、候选类型画了)+点击雾下非候选节点零提交(雾不扩大可点域——候选集即合法集)---- @@ -90,9 +90,9 @@ func _ready() -> void: await get_tree().process_frame await get_tree().process_frame ro.route_chosen.disconnect(on_route) - if not submitted.is_empty() or not main.level_screen.visible or not main.run_scope.runner.awaiting_choice(): + if not submitted.is_empty() or not main.ui.modals.level_screen.visible or not main.run_scope.runner.awaiting_choice(): _pre_fail("case⑱ 雾下节点点击产生提交(提交=%s visible=%s 选路态=%s——雾不扩大可点域)〔fv9〕" - % [str(submitted), str(main.level_screen.visible), str(main.run_scope.runner.awaiting_choice())]) + % [str(submitted), str(main.ui.modals.level_screen.visible), str(main.run_scope.runner.awaiting_choice())]) return var nds: Array = main.run_scope.runner.nodes() var tgt := {} @@ -116,13 +116,13 @@ func _ready() -> void: main.get_viewport().push_input(evc, true) await get_tree().process_frame await get_tree().process_frame - if main.level_screen.visible or main.run_scope.runner.awaiting_choice(): - _pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.level_screen.visible), str(main.run_scope.runner.awaiting_choice())]) + if main.ui.modals.level_screen.visible or main.run_scope.runner.awaiting_choice(): + _pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.ui.modals.level_screen.visible), str(main.run_scope.runner.awaiting_choice())]) for i in range(3): await get_tree().process_frame # ---- ⑬ 画布随图拟合(同 bug 的几何面回归):复刻分层公式独立推导最深节点内容 x, # 断言画布 min 尺寸覆盖之(open_choices 每 stop 拟合——L1 单链 Boss x=70+6*122=802 曾越界) - var ro2 = main.level_screen + var ro2 = main.ui.modals.level_screen var nds2: Array = main.run_scope.runner.nodes() var layer2 := {} var inbound2 := {} @@ -155,7 +155,7 @@ func _ready() -> void: var cvmin: Dictionary = ro2.canvas_probe() if float(cvmin["min_size"].x) < need_x2 - 0.5: _pre_fail("画布拟合不足(min.x=%.0f < 最深内容 %.0f——深层节点不可点回归)" % [cvmin["min_size"].x, need_x2]) - var bv = main.board_view + var bv = main.ui.battle.board_zone.board_view var ms = main.run_scope.settlement.minesweeper if main.run_scope.settlement != null else null if ms == null or bv.cells_probe().is_empty(): _pre_fail("战斗未就绪(cells=%d settlement=%s)" % [bv.cells_probe().size(), str(main.run_scope.settlement)]) @@ -517,29 +517,29 @@ func _ready() -> void: if status_dig.x == -9: _problems.append("case⑯ 无可挖无封印格(板面异常——锁断言中断)") else: - var pbtn16: Button = main.board_zone.status_badge + var pbtn16: Button = main.ui.battle.board_zone.status_badge var bpos: Vector2 = pbtn16.get_global_rect().get_center() _push_click(bpos, MOUSE_BUTTON_LEFT) _push_release(bpos, MOUSE_BUTTON_LEFT) # Button pressed 信号走 release 沿——按压+释放成对 await get_tree().process_frame - if not main.status_panel.visible or not main._input_locked(): + if not main.ui.modals.status_panel.visible or not main.ui.modals.flow.locks_input(): _problems.append("case⑯ 角标真点击未开状态面板(visible=%s locked=%s——入口可达性失守)" - % [str(main.status_panel.visible), str(main._input_locked())]) + % [str(main.ui.modals.status_panel.visible), str(main.ui.modals.flow.locks_input())]) else: _push_click(_pos(bv, status_dig), MOUSE_BUTTON_LEFT) await get_tree().process_frame await get_tree().process_frame if _has_action(status_dig, "dig") or ms.is_revealed(status_dig): _problems.append("case⑯ 状态面板开启时挖掘穿透(R3 输入锁破——目标=%s)" % status_dig) - var resume_btn: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix") + var resume_btn: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix") if resume_btn == null: _problems.append("case⑯ 状态面板关闭钮缺失") else: resume_btn.pressed.emit() await get_tree().process_frame - if main.status_panel.visible or main._input_locked(): + if main.ui.modals.status_panel.visible or main.ui.modals.flow.locks_input(): _problems.append("case⑯ 关闭后未恢复(visible=%s locked=%s——R3 残留)" - % [str(main.status_panel.visible), str(main._input_locked())]) + % [str(main.ui.modals.status_panel.visible), str(main.ui.modals.flow.locks_input())]) _push_click(_pos(bv, status_dig), MOUSE_BUTTON_LEFT) await get_tree().process_frame await get_tree().process_frame diff --git a/game/probe/ui_probe.gd b/game/probe/ui_probe.gd index b82bc2c1..50e0d429 100644 --- a/game/probe/ui_probe.gd +++ b/game/probe/ui_probe.gd @@ -16,7 +16,7 @@ func _ready() -> void: await get_tree().process_frame var problems: Array = [] # ---- 放弃入口(台账 119):默认隐藏(非冒险模式无冒险可放弃——开始层/调试两层)---- - if main.level_screen.abandon_btn == null or main.level_screen.abandon_btn.visible: + if main.ui.modals.level_screen.abandon_btn == null or main.ui.modals.level_screen.abandon_btn.visible: print("[ui_probe] FAIL — 放弃按钮默认应隐藏(非冒险模式关)") get_tree().quit(1) return @@ -28,99 +28,99 @@ func _ready() -> void: problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)") else: await get_tree().process_frame - if not main.camp_overlay.visible: + if not main.ui.modals.camp_overlay.visible: problems.append("营地层未打开(进档开层新路径)") - elif main._start_overlay.visible: + elif main.ui.screens.start_overlay.visible: problems.append("营地层打开时开始层未隐藏(规范 2.3 遮挡纪律)") else: - if not String(main.camp_overlay._balance_label.text).begins_with("钥石"): - problems.append("营地余额文案缺失(%s)" % main.camp_overlay._balance_label.text) - if main.camp_overlay._tree_buy_btns.is_empty(): + if not String(main.ui.modals.camp_overlay._balance_label.text).begins_with("钥石"): + problems.append("营地余额文案缺失(%s)" % main.ui.modals.camp_overlay._balance_label.text) + if main.ui.modals.camp_overlay._tree_buy_btns.is_empty(): problems.append("树淬炼柜台购买钮缺失(四柜台重排后树=主界面)") else: var btn_by_id := {} - for card2 in main.camp_overlay._tree_buy_btns: + for card2 in main.ui.modals.camp_overlay._tree_buy_btns: btn_by_id[String(card2.node_id)] = card2.btn if not btn_by_id.has("sp_hp") or bool(btn_by_id["sp_hp"].disabled): problems.append("脊柱生命 L1 白板可购钮未启用(无门槛——can_buy 不含余额面语义)") if not btn_by_id.has("att_socket2") or not bool(btn_by_id["att_socket2"].disabled): problems.append("同调位 2 前置未亮应禁用(点亮前置语义)") - if main.camp_overlay._workshop_rows.size() != 12: - problems.append("秘宝匣柜台行数错误(%d ≠ 12——迭代 22 秘宝批换代)" % main.camp_overlay._workshop_rows.size()) + if main.ui.modals.camp_overlay._workshop_rows.size() != 12: + problems.append("秘宝匣柜台行数错误(%d ≠ 12——迭代 22 秘宝批换代)" % main.ui.modals.camp_overlay._workshop_rows.size()) main._on_camp_closed() await get_tree().process_frame main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点) await get_tree().process_frame - if main.camp_overlay.visible or not main._start_overlay.visible: + if main.ui.modals.camp_overlay.visible or not main.ui.screens.start_overlay.visible: problems.append("营地关闭后未恢复开始层(开合往返失守)") # 换回全新零档(营地段购买态不泄入后续冒险段——确定性;先删后建防陈旧档泄入) ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", true, false, true) - main.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机;本探针 start_single 第五路同吃此框,确定性禁依赖默认分支) + main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机;本探针 start_single 第五路同吃此框,确定性禁依赖默认分支) main._start_adventure_from_menu() await get_tree().process_frame await get_tree().process_frame # ---- 入口选路停点(关卡开始即出节点图——level_started 后、首战前弹选路层; # press_candidate 图上命中路径 pick 入口单候选进首战)---- # ---- 独立性断言(独立屏激活期战斗屏宿主整体隐藏,零透出)---- - if not main.level_screen.visible: + if not main.ui.modals.level_screen.visible: problems.append("关卡开始未弹入口选路停点(level_screen 应可见)") - elif main._battle_root.visible: + elif main.ui.screens.battle_root.visible: problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)") elif main.run_scope.runner == null or not main.run_scope.runner.awaiting_choice() \ or main.run_scope.runner.next_candidate_ids().size() != 1: problems.append("入口停点状态失守(选路态 + 入口单候选)") else: - main.level_screen.press_candidate(0) + main.ui.modals.level_screen.press_candidate(0) await get_tree().process_frame await get_tree().process_frame - if not main._battle_root.visible or main.level_screen.visible: + if not main.ui.screens.battle_root.visible or main.ui.modals.level_screen.visible: problems.append("选路提交战斗节点后未切回战斗屏(屏级互斥恢复失守)") # ---- 放弃入口·冒险模式可见 + 战斗中按下被拒(真实按钮路径——入口挂选路停点,战斗中拒绝为防御分支)---- - if main.level_screen.abandon_btn == null or not main.level_screen.abandon_btn.visible: + if main.ui.modals.level_screen.abandon_btn == null or not main.ui.modals.level_screen.abandon_btn.visible: problems.append("冒险模式放弃按钮未开放(选路停点入口在案)") else: - main.level_screen.abandon_btn.pressed.emit() + main.ui.modals.level_screen.abandon_btn.pressed.emit() await get_tree().process_frame if main.run_scope.adventure == null or String(main.run_scope.adventure.state) != "running": problems.append("首战中放弃未被拒(战斗域单事务在飞——state 应保持 running)") # ---- 既有断言(回归基线;A 区无进度条——节点链进度由关卡画面独立屏承载)---- - if main.board_view.cells_probe().is_empty(): + if main.ui.battle.board_zone.board_view.cells_probe().is_empty(): problems.append("棋盘格未生成(cells=0)") - if main.board_view.board() == null: + if main.ui.battle.board_zone.board_view.board() == null: problems.append("board 未装载") - if main.info_panel.attack_text().contains("—") or main.info_panel.armor_text().contains("—"): - problems.append("C 区攻击/护甲未显示(%s | %s)" % [main.info_panel.attack_text(), main.info_panel.armor_text()]) + if main.ui.battle.info_panel.attack_text().contains("—") or main.ui.battle.info_panel.armor_text().contains("—"): + problems.append("C 区攻击/护甲未显示(%s | %s)" % [main.ui.battle.info_panel.attack_text(), main.ui.battle.info_panel.armor_text()]) if main.run_scope.settlement == null or main.run_scope.settlement.battle_result != "": problems.append("首战未处于进行中") - if not main.log_view.parsed_text().contains("节点 1/"): + if not main.ui.battle.log_view.parsed_text().contains("节点 1/"): problems.append("节点头日志缺失") # ---- 四区布局断言(battle.md §Layout)---- - if not main.stage.round_label.text.contains("第 1 轮"): - problems.append("B 区轮次·阶段指示缺失(%s)" % main.stage.round_label.text) - if main.stage.enemy_box.get_child_count() < 1: - problems.append("B 区敌方站桩未构建(%d 个)" % main.stage.enemy_box.get_child_count()) - if main.ap_bar._dots.get_child_count() != int(main.db.run.action_economy.ap_per_round): - problems.append("D 区 AP 点阵数量错误(%d)" % main.ap_bar._dots.get_child_count()) - if main.ap_bar.refresh_remaining_text() == "": + if not main.ui.battle.stage.round_label.text.contains("第 1 轮"): + problems.append("B 区轮次·阶段指示缺失(%s)" % main.ui.battle.stage.round_label.text) + if main.ui.battle.stage.enemy_box.get_child_count() < 1: + problems.append("B 区敌方站桩未构建(%d 个)" % main.ui.battle.stage.enemy_box.get_child_count()) + if main.ui.battle.board_zone.ap_bar._dots.get_child_count() != int(main.db.run.action_economy.ap_per_round): + problems.append("D 区 AP 点阵数量错误(%d)" % main.ui.battle.board_zone.ap_bar._dots.get_child_count()) + if main.ui.battle.board_zone.ap_bar.refresh_remaining_text() == "": problems.append("D 区弃图刷新按钮文案缺失") - if main.board_zone.toast == null: + if main.ui.battle.board_zone.toast == null: problems.append("D 区 toast 槽缺失") - if main.info_panel.item_slots == null or main.info_panel.item_slots.get_child_count() != 4: + if main.ui.battle.info_panel.item_slots == null or main.ui.battle.info_panel.item_slots.get_child_count() != 4: problems.append("C 区秘宝槽(预留)缺失") - if main.stage.hero_standee.slot_row == null: + if main.ui.battle.stage.hero_standee.slot_row == null: problems.append("B 区站桩状态行缺失(状态效果唯一展示面——2026-09-26 C 区精简批)") # B 区明细带挖掘期收起断言(T17 视觉扩展批适配):结算演播期 settling 视觉延伸=mode=settle # 属新语义(演播放完回 dig——main._is_settling 排播项,判据=纯队列),本断言仅在排播空闲态判定。 - if main._row_queue.is_empty() \ - and (main.log_view.mode() != "dig" or main.log_view.custom_minimum_size.y != 24.0): - problems.append("B 区明细带挖掘期应收起 24px(mode=%s h=%s)" % [main.log_view.mode(), str(main.log_view.custom_minimum_size.y)]) + if main.ui.screens.row_pacer.pending_rows().is_empty() \ + and (main.ui.battle.log_view.mode() != "dig" or main.ui.battle.log_view.custom_minimum_size.y != 24.0): + problems.append("B 区明细带挖掘期应收起 24px(mode=%s h=%s)" % [main.ui.battle.log_view.mode(), str(main.ui.battle.log_view.custom_minimum_size.y)]) # E 区无道具面板——四区构建由其余断言覆盖 # ---- 敌方站桩投影(顺位徽标/意图三档动作动画占位——敌人系统×4,全站桩)---- # B 区站桩子件伴随件化(壳层 tooltip 载体)——断言口径见台账 171: # 伴随件化(壳层 tooltip 载体),穿透明(子件 _name_label/_armor_icon/_intent_badge)改本体 # 探针出口 stage.enemy_body_at(i).name_line_probe()/intent_probe()——呈现口径逐字保持 - var standee = main.stage.enemy_body_at(0) + var standee = main.ui.battle.stage.enemy_body_at(0) if standee == null: problems.append("敌立绘本体缺失(enemy_body_at(0) 无世界站桩)") else: @@ -138,12 +138,12 @@ func _ready() -> void: # 站桩纹理非空断言(八敌真图落位;缺位回退链保持的对照面) if not bool(standee.portrait_probe().get("has_tex", false)): problems.append("敌立绘纹理未落位(素材管线 48px 档——迭代 6 T2;缺位回退链不应在此态触发)") - if main.stage.hero_standee == null or not main.stage.hero_standee.has_world_hp_bar: + if main.ui.battle.stage.hero_standee == null or not main.ui.battle.stage.hero_standee.has_world_hp_bar: problems.append("B 区主角站桩缺失(世界血条面缺失)") - elif not bool(main.stage.hero_standee.portrait_probe().get("has_tex", false)): + elif not bool(main.ui.battle.stage.hero_standee.portrait_probe().get("has_tex", false)): problems.append("主角立绘纹理未落位(48px 档——迭代 6 T2/T3)") # ---- BUG-0001 回归(tooltip 入树后 set_effects 槽位不错位——结算软锁修复沉淀)---- - var slots_row = main.stage.hero_standee.slot_row # BUG-0001 回归随组件迁入站桩(tooltip/槽位错位同源) + var slots_row = main.ui.battle.stage.hero_standee.slot_row # BUG-0001 回归随组件迁入站桩(tooltip/槽位错位同源) var probe_eff: Array = [{"effect_id": "attack_up", "display_name": "增伤", "layers": 1, "remaining_settlements": 1, "source_class": "safe_cell", "round_index": 1, "effective_value": 50}] slots_row.set_effects(probe_eff) @@ -196,12 +196,12 @@ func _ready() -> void: main._debug.start_single(2) await get_tree().process_frame await get_tree().process_frame - if main.board_view.board() == null: + if main.ui.battle.board_zone.board_view.board() == null: problems.append("L2 单场未启动(board=null)") else: # 地形变异:L2 板允许带变异(岩浆/烬覆)——布置域断言 = # 变异格全在初始展开区外(closed_only 结构保障) - var b0 = main.board_view.board() + var b0 = main.ui.battle.board_zone.board_view.board() var r0p := {} for c in b0.compute_r0(): r0p[c] = true @@ -209,8 +209,8 @@ func _ready() -> void: if r0p.has(c): problems.append("L2 板变异格落在初始展开区(地形变异 布置域违例)") break - main.board_view.set_desaturated(true) - main.board_view.set_desaturated(false) + main.ui.battle.board_zone.board_view.set_desaturated(true) + main.ui.battle.board_zone.board_view.set_desaturated(false) # ---- GP 终局行:summary gp_final 键取数+胜利面板 GP 行呈现(C5:GP 行只上冒险终局面板; # 断言面 = summary 键,无影子账清零转存)---- # (置于战斗态断言之后——合成收束会灰化站桩并清 settlement 引用,具污染性) @@ -223,17 +223,17 @@ func _ready() -> void: main._on_adventure_finished({"result": "completed", "cleared": true, "levels_cleared": 3, "stopped_at": 0, "keystone_total": 30, "master_seed": 42, "gp_final": 57}) main.meta.save = meta_saved - if not main._end_overlay.is_open(): + if not main.ui.modals.end_overlay.is_open(): problems.append("冒险终局面板未开(合成摘要路径)") - elif not main._end_overlay._gp_row.visible or not main._end_overlay._gp_val.text.contains("57"): - problems.append("终局面板 GP 行缺失或取数不对(%s)" % main._end_overlay._gp_val.text) - main._end_overlay.close() - main._close_modal(main._end_overlay) + elif not main.ui.modals.end_overlay._gp_row.visible or not main.ui.modals.end_overlay._gp_val.text.contains("57"): + problems.append("终局面板 GP 行缺失或取数不对(%s)" % main.ui.modals.end_overlay._gp_val.text) + main.ui.modals.end_overlay.close() + main.ui.modals.flow.close(main.ui.modals.end_overlay) if problems.is_empty(): print("[ui_probe] PASS — cells=%d board=%dx%d hp='%s' enemies=%d ap_dots=%d" % [ - main.board_view.cells_probe().size(), main.board_view.board().width, main.board_view.board().height, - main.info_panel.attack_text(), - main.stage.enemy_box.get_child_count(), main.ap_bar._dots.get_child_count()]) + main.ui.battle.board_zone.board_view.cells_probe().size(), main.ui.battle.board_zone.board_view.board().width, main.ui.battle.board_zone.board_view.board().height, + main.ui.battle.info_panel.attack_text(), + main.ui.battle.stage.enemy_box.get_child_count(), main.ui.battle.board_zone.ap_bar._dots.get_child_count()]) get_tree().quit(0) else: for p in problems: diff --git a/game/probe/ui_struct_probe.gd b/game/probe/ui_struct_probe.gd index cda2253a..96bcac28 100644 --- a/game/probe/ui_struct_probe.gd +++ b/game/probe/ui_struct_probe.gd @@ -28,13 +28,13 @@ func _ready() -> void: # ---- 菜单簇(开始层可见态;逐屏开→采样→关,模态恢复走组合根真实路径)---- ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_menu", true, false, true) _settle() - await _audit("start", [main._start_overlay]) + await _audit("start", [main.ui.screens.start_overlay]) _start_columns_contract(main) _modal_registry_contract(main) main._open_settings() _settle() - await _audit("settings", [main.settings_overlay]) + await _audit("settings", [main.ui.modals.settings_overlay]) main._on_settings_closed() _settle() @@ -42,7 +42,7 @@ func _ready() -> void: problems.append("[camp] 营地入口缺失(选档层新档钮——mn9 新路径驱动中断)") else: _settle() - await _audit("camp", [main.camp_overlay]) + await _audit("camp", [main.ui.modals.camp_overlay]) # 营地灰字源 token 扫描(迭代36 wc4 静态半——源内零裸 UiKit.STONE;判别式= # STONE 出现次数 == STONE_DEEP 出现次数,即每处 STONE 都带 _DEEP 后缀) var camp_src := FileAccess.get_file_as_string("res://presentation/views/camp_overlay_view.gd") @@ -59,15 +59,15 @@ func _ready() -> void: # 生涯页驱动=函数直调中间态(卡② mn9/mn10——careerbtn 已撤,卡④ 营地笔记本改挂前无 UI 路径) main._open_career() _settle() - if not main.career_overlay.visible or main._start_overlay.visible: + if not main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible: problems.append("[career] 生涯页未打开或开始层未隐藏(函数直调中间态断裂)") - await _audit("career", [main.career_overlay]) + await _audit("career", [main.ui.modals.career_overlay]) _career_readonly_contract(main) _career_zero_motion_contract(main) _career_nojump_contract(main) main._on_career_closed() _settle() - if main.career_overlay.visible or not main._start_overlay.visible: + if main.ui.modals.career_overlay.visible or not main.ui.screens.start_overlay.visible: problems.append("[career] 关闭后页未隐藏或开始层未恢复(模态栈单口失守)") # ---- 选档界面(迭代 33 卡③):函数直调开层(「进入存档」钮归卡② mn3——点击链在卡② 承接), @@ -93,39 +93,39 @@ func _ready() -> void: problems.append("[savesel] 开层前激活态非零(sl9 重启链——boot/注入面后应无激活)") main._open_save_select() _settle() - if not main.save_select_overlay.visible or main._start_overlay.visible: + if not main.ui.modals.save_select_overlay.visible or main.ui.screens.start_overlay.visible: problems.append("[savesel] 选档层未打开或开始层未隐藏(开层函数断裂)") - await _audit("savesel", [main.save_select_overlay]) + await _audit("savesel", [main.ui.modals.save_select_overlay]) _save_select_contract(main, ssel_tpl) main._on_save_select_closed() _settle() - if main.save_select_overlay.visible or not main._start_overlay.visible: + if main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible: problems.append("[savesel] 关闭后层未隐藏或开始层未恢复(模态栈单口失守)") - main.confirm_dialog.open("结构探针采样", "确认") + main.ui.modals.confirm_dialog.open("结构探针采样", "确认") _settle() - await _audit("confirm", [main.confirm_dialog]) - main.confirm_dialog.close() + await _audit("confirm", [main.ui.modals.confirm_dialog]) + main.ui.modals.confirm_dialog.close() _settle() main._show_debug_overlay() _settle() - await _audit("debug", [main.debug_overlay]) + await _audit("debug", [main.ui.modals.debug_overlay]) main._close_debug_overlay() _settle() # ---- 冒险簇(真流推进:开冒险→地图选路态→选战斗节点→战斗 HUD)---- ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_adv", true, false, true) - main.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,结构采样确定性禁依赖默认分支) + main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,结构采样确定性禁依赖默认分支) main._start_adventure_from_menu() _settle(4) - await _audit("level_map", [main.level_screen]) + await _audit("level_map", [main.ui.modals.level_screen]) var battle_id := _pick_battle_node(main) if battle_id >= 0: - main.level_screen.route_chosen.emit(battle_id) + main.ui.modals.level_screen.route_chosen.emit(battle_id) _settle(6) - _audit("battle_hud", [main.board_zone, main.info_panel, main.stage]) + _audit("battle_hud", [main.ui.battle.board_zone, main.ui.battle.info_panel, main.ui.battle.stage]) _standeefit_contract(main) # ---- 状态面板:组合根真实路径开层→结构断言→关闭。附只读性契约 #(R4——子树信号连接白名单巡检)与零动效契约(R5/P4 甲——零 AnimationPlayer/零 tween)。 @@ -133,74 +133,74 @@ func _ready() -> void: # 一律红,须先入册审查);白名单={视图自身 _emit_closed, 组合根 _on_status_closed}。 main._open_status_panel() _settle() - await _audit("status", [main.status_panel]) + await _audit("status", [main.ui.modals.status_panel]) _status_readonly_contract(main) _status_zero_motion_contract(main) main._on_status_closed() _settle() - if main.status_panel.visible: + if main.ui.modals.status_panel.visible: problems.append("[status] 关闭后层未隐藏(模态栈单口失守)") # ---- 图例层(旧模态、首过结构族):ap_bar legend_toggled # 信号真路径开(board_zone 信号→组合根 _on_legend_toggled→_open_modal)→结构断言族→ # closed 信号真路径关(close()→组合根 _on_modal_closed)。 - main.board_zone.legend_toggled.emit() + main.ui.battle.board_zone.legend_toggled.emit() _settle() - if not main.legend_overlay.visible: + if not main.ui.modals.legend_overlay.visible: problems.append("[legend] 图例层未打开(legend_toggled 路径中断)") - await _audit("legend", [main.legend_overlay]) - main.legend_overlay.close() + await _audit("legend", [main.ui.modals.legend_overlay]) + main.ui.modals.legend_overlay.close() _settle() - if main.legend_overlay.visible: + if main.ui.modals.legend_overlay.visible: problems.append("[legend] 关闭后层未隐藏(模态栈单口失守)") else: - problems.append("[battle_hud] 候选中无战斗节点(驱动中断——候选=%s)" % str(main.level_screen._cand_ids)) + problems.append("[battle_hud] 候选中无战斗节点(驱动中断——候选=%s)" % str(main.ui.modals.level_screen._cand_ids)) # ---- 兑付屏(真实 open 面 + 合成最小数据——结构断言只看矩形/宽度/可见性)---- - main._end_overlay.open_victory("结构探针·胜利演算", "返回", Rect2(200, 200, 400, 300), {"gp_summary": "GP 0"}) + main.ui.modals.end_overlay.open_victory("结构探针·胜利演算", "返回", Rect2(200, 200, 400, 300), {"gp_summary": "GP 0"}) _settle() - await _audit("battle_end", [main._end_overlay]) - main._end_overlay.close() + await _audit("battle_end", [main.ui.modals.end_overlay]) + main.ui.modals.end_overlay.close() _settle() # ---- 节点结算层(旧模态、首过结构族):rest 兑付真路径开 #(组合根 _open_modal 登记册+show_rest 合成喂入)→结构断言族→continue_pressed 信号真路径关 #(press_continue→组合根 _on_node_continue→_close_modal)。 - main._open_modal(main.node_overlay) - main.node_overlay.show_rest(3, 15, 20) + main.ui.modals.flow.open(main.ui.modals.node_overlay) + main.ui.modals.node_overlay.show_rest(3, 15, 20) _settle() - if not main.node_overlay.visible: + if not main.ui.modals.node_overlay.visible: problems.append("[node] 节点结算层未打开(rest 兑付路径中断)") - await _audit("node_overlay", [main.node_overlay]) - main.node_overlay.press_continue() + await _audit("node_overlay", [main.ui.modals.node_overlay]) + main.ui.modals.node_overlay.press_continue() _settle() - if main.node_overlay.visible: + if main.ui.modals.node_overlay.visible: problems.append("[node] 关闭后层未隐藏(continue_pressed 路径中断)") - main.curio_overlay.open_pick("结构探针·三选一", [ + main.ui.modals.curio_overlay.open_pick("结构探针·三选一", [ {"id": "probe_heal", "text": "回血\n恢复 3 点"}, {"id": "probe_magic", "text": "爆裂轰击\n下一击 +5"}, {"id": "probe_pierce", "text": "穿甲\n无视护甲"}, ]) _settle() - await _audit("curio_pick", [main.curio_overlay]) - main.curio_overlay.visible = false + await _audit("curio_pick", [main.ui.modals.curio_overlay]) + main.ui.modals.curio_overlay.visible = false _settle() - main.shop_overlay.open_shop([ + main.ui.modals.shop_overlay.open_shop([ {"product_id": "probe_p1", "display_name": "探针商品甲", "attr": "atk", "flat_value": 1, "price": 10, "purchased": 0}, {"product_id": "probe_p2", "display_name": "探针商品乙", "attr": "hp", "flat_value": 2, "price": 20, "purchased": 0}, ], 100) _settle() - await _audit("shop", [main.shop_overlay]) + await _audit("shop", [main.ui.modals.shop_overlay]) # ---- 术语传导:货架卡与三选一卡片的玩家面文案(物品名=专长获取、分类=专长凭证) - main.shop_overlay.open_shelf([ + main.ui.modals.shop_overlay.open_shelf([ {"kind_id": "feat_ticket", "display_name": "专长获取", "value": 0, "price": 5}, {"kind_id": "heal", "display_name": "应急回复", "value": 6, "price": 5}, ], 20) _settle() - var ticket_row: Dictionary = main.shop_overlay._shelf_rows[0] + var ticket_row: Dictionary = main.ui.modals.shop_overlay._shelf_rows[0] if String(main.db.run.shop.pool_row("feat_ticket").get("display_name", "")) != "专长获取": problems.append("[shop_term] shop.tres 专长凭证件名未换代(现=%s)" % String(main.db.run.shop.pool_row("feat_ticket").get("display_name", ""))) if ticket_row.card.name_l.text != "专长获取": @@ -211,7 +211,7 @@ func _ready() -> void: problems.append("[shop_term] 专长凭证件描述未用规范流程句(现=%s)" % ticket_row.card.desc_l.text) if ticket_row.card.foot_l.text != "专长凭证": problems.append("[shop_term] 专长凭证件分类标注未用「专长凭证」(现=%s)" % ticket_row.card.foot_l.text) - var heal_row: Dictionary = main.shop_overlay._shelf_rows[1] + var heal_row: Dictionary = main.ui.modals.shop_overlay._shelf_rows[1] if heal_row.card.desc_l.text != "回复 +6(本冒险)": problems.append("[shop_term] 回复件描述漏了 heal 词表(现=%s)" % heal_row.card.desc_l.text) if heal_row.card.foot_l.text != "回复": @@ -223,7 +223,7 @@ func _ready() -> void: problems.append("[curio_term] 专长凭证三选一卡未用物品名/规范流程句(现=%s)" % ticket_text) if ticket_text.contains("门票") or ticket_text.contains("T5") or ticket_text.contains("持有集") or ticket_text.contains("奇物三选一"): problems.append("[curio_term] 专长凭证三选一卡残留退役词/内部任务号(现=%s)" % ticket_text) - main.shop_overlay.visible = false + main.ui.modals.shop_overlay.visible = false _settle() # ---- ⑦resume 专项:续玩停点恢复(main.gd:717 报错回归闸)---- @@ -234,11 +234,11 @@ func _ready() -> void: main.run_scope.runner = null # 全新启动等价态(真机=菜单直进续玩,无前置冒险——runner 从未赋值;探针此前推流会残留旧 runner) main._resume_from_menu() _settle(8) - if not main._end_overlay.visible: + if not main.ui.modals.end_overlay.visible: problems.append("[resume] 续玩恢复后胜利总结面未开(三选一停点分支中断)") else: var found := {"ok": false} - _find_label_text(main._end_overlay, "合计 24", found) + _find_label_text(main.ui.modals.end_overlay, "合计 24", found) if not found.ok: problems.append("[resume] 胜利总结面 GP 行未见快照合计「合计 24」(runner 缺位守卫缺位——717 回归)") var s: String = main._gp_summary_text({"gp_dropped": 0}) @@ -280,7 +280,7 @@ func _find_label_text(node: Node, needle: String, found: Dictionary) -> void: func _pick_battle_node(main: Node) -> int: # 入口/兑付停点走 level_screen.open_choices 直传(不写 _pending_candidates)—— # 候选从视图态取:_cand_ids × runner.nodes() 的 type(编号解析=node_id_of 单一出口) - var ls: Node = main.level_screen + var ls: Node = main.ui.modals.level_screen if ls._map._runner == null: return -1 for nd in ls._map._runner.nodes(): @@ -306,7 +306,7 @@ const _PAUSE_READONLY_SIGNALS: Array[String] = [ func _status_readonly_contract(main: Node) -> void: - var overlay: Control = main.status_panel + var overlay: Control = main.ui.modals.status_panel var stack: Array = [overlay] var conn_count := 0 while not stack.is_empty(): @@ -332,7 +332,7 @@ func _status_readonly_contract(main: Node) -> void: # ⑨status 零动效契约(R5/P4 甲——零动效直开直关路径存在断言): # 子树零 AnimationPlayer + 视图源码零 tween/动效包装调用(源码 token 扫描=ui_contrast 静态段同手法) func _status_zero_motion_contract(main: Node) -> void: - var overlay: Control = main.status_panel + var overlay: Control = main.ui.modals.status_panel var stack: Array = [overlay] while not stack.is_empty(): var n: Node = stack.pop_back() @@ -352,7 +352,7 @@ func _status_zero_motion_contract(main: Node) -> void: # 清单外零连接。白名单={视图自身 _emit_closed〔返回钮〕, 组合根 _on_career_closed};页内任何 # 购买/导航/写入方向的新连接一律红,须先入册审查。 func _career_readonly_contract(main: Node) -> void: - var overlay: Control = main.career_overlay + var overlay: Control = main.ui.modals.career_overlay var stack: Array = [overlay] var conn_count := 0 while not stack.is_empty(): @@ -377,7 +377,7 @@ func _career_readonly_contract(main: Node) -> void: # ⑪career 零动效契约(状态面板 ⑨ 同形制——P4 甲直开直关) func _career_zero_motion_contract(main: Node) -> void: - var overlay: Control = main.career_overlay + var overlay: Control = main.ui.modals.career_overlay var stack: Array = [overlay] while not stack.is_empty(): var n: Node = stack.pop_back() @@ -392,13 +392,13 @@ func _career_zero_motion_contract(main: Node) -> void: # ⑫模态登记/树序全量合同(V1——career 专属登记序契约推广到全部模态+屏壳; -# 判据=main.LAYER_CONTRACT 单一事实源:登记册序==合同模态序、直接子层树序==合同序逐对相等。 +# 判据=main.ui.LAYER_CONTRACT 单一事实源:登记册序==合同模态序、直接子层树序==合同序逐对相等。 # 两特例按合同数据表达:level_screen 宿住冒险屏壳(树序随宿主=全模态之下,遮挡语义归模态栈 # screen_state 登记)、camp_overlay 成员与 camp_screen 屏壳同槽(屏壳树序代言成员登记位)。 # _career_registry_contract 的断言(career=状态面板后/确认弹窗前、树序同步)由本全量合同覆盖) # ⑭选档界面契约(卡③ sl1/sl2/sl3/sl4/sl9——呈现面全部来自喂入数据,视图零自判) func _save_select_contract(main: Node, tpl: String) -> void: - var sso: Node = main.save_select_overlay + var sso: Node = main.ui.modals.save_select_overlay # sl1 三槽呈现:槽卡计数==3+零滚动容器(禁滚动长列表形态——契约 1) var rows: VBoxContainer = sso._rows_box var live_rows := 0 @@ -477,13 +477,13 @@ func _collect_labels(node: Node, out: Array[String]) -> void: func _modal_registry_contract(main: Node) -> void: - var list: Array = main._modals._list + var list: Array = main.ui.modals.flow.stack._list # ① 登记册序==合同模态序(逐位点名——合同是登记序唯一事实源) var contract_modals: Array = [] - for entry: Dictionary in main.LAYER_CONTRACT: + for entry: Dictionary in main.ui.LAYER_CONTRACT: if String(entry["kind"]) != "modal": continue - var n: Node = main._layer_node(String(entry["key"])) + var n: Node = main.ui.node(String(entry["key"])) if n == null: problems.append("[registry] 合同层缺失: %s〔装配生命批 V1〕" % String(entry["key"])) continue @@ -498,10 +498,10 @@ func _modal_registry_contract(main: Node) -> void: i, list[i].name, contract_modals[i].name]) # ② 直接子层树序==合同序逐对相等(屏壳/模态/全屏态/非视觉节点全链;宿住层随宿主不参与) var prev: Node = null - for entry: Dictionary in main.LAYER_CONTRACT: + for entry: Dictionary in main.ui.LAYER_CONTRACT: if entry.has("host"): continue - var node: Node = main._layer_node(String(entry["shell"]) if entry.has("shell") else String(entry["key"])) + var node: Node = main.ui.node(String(entry["shell"]) if entry.has("shell") else String(entry["key"])) if node == null: problems.append("[registry] 合同层缺失: %s〔装配生命批 V1〕" % String(entry["key"])) continue @@ -510,23 +510,23 @@ func _modal_registry_contract(main: Node) -> void: prev.name, node.name, node.get_index(), prev.get_index()]) prev = node # ③ 宿住层特例:level_screen 宿主=冒险屏壳且在全直接子模态之下(遮挡语义归模态栈 screen_state) - var lv: Node = main.level_screen - if lv.get_parent() != main._adventure_screen: + var lv: Node = main.ui.modals.level_screen + if lv.get_parent() != main.ui.screens.adventure_screen: problems.append("[registry] level_screen 宿主应=冒险屏壳(实测 %s)〔装配生命批 V1〕" % lv.get_parent().name) - elif lv.get_parent().get_index() >= main.legend_overlay.get_index(): + elif lv.get_parent().get_index() >= main.ui.modals.legend_overlay.get_index(): problems.append("[registry] 冒险屏壳应在全模态之下(宿主 idx=%d legend idx=%d)〔装配生命批 V1〕" % [ - lv.get_parent().get_index(), main.legend_overlay.get_index()]) + lv.get_parent().get_index(), main.ui.modals.legend_overlay.get_index()]) # ④ 营地同槽特例:camp_screen 屏壳树序夹商店层与设置层之间(成员 camp_overlay 登记位代言) - var cs: Node = main._camp_screen - if cs.get_index() <= main.shop_overlay.get_index() or cs.get_index() >= main.settings_overlay.get_index(): + var cs: Node = main.ui.screens.camp_screen + if cs.get_index() <= main.ui.modals.shop_overlay.get_index() or cs.get_index() >= main.ui.modals.settings_overlay.get_index(): problems.append("[registry] camp_screen 屏壳树序应夹商店与设置层之间(idx=%d shop=%d settings=%d)〔装配生命批 V1〕" % [ - cs.get_index(), main.shop_overlay.get_index(), main.settings_overlay.get_index()]) + cs.get_index(), main.ui.modals.shop_overlay.get_index(), main.ui.modals.settings_overlay.get_index()]) # ⑬career 零跳转契约(提纲 P4 甲——页内零「前往他处」入口:除「返回」外任何导航钮=红) func _career_nojump_contract(main: Node) -> void: for nav in ["营地", "商店", "设置", "冒险", "继续上局", "退出", "前往"]: - if ProbeUiUtil.find_button(main.career_overlay, nav, "contains") != null: + if ProbeUiUtil.find_button(main.ui.modals.career_overlay, nav, "contains") != null: problems.append("[career][nojump] 页内出现导航入口「%s」(零跳转违约——生涯纯只读)" % nav) @@ -534,7 +534,7 @@ func _career_nojump_contract(main: Node) -> void: # (名行基线离盒底 1px 同型高危写法返修回归闸; # 内容矩形=StandeeBody.content_rect(),最低绘制元素=底座下缘) func _standeefit_contract(main: Node) -> void: - var st: Node = main.stage + var st: Node = main.ui.battle.stage if st == null or st.hero_standee == null: problems.append("[standee] 舞台/主角站桩缺失(断言中断)") return @@ -711,8 +711,8 @@ func _dump(c: Control, depth: int) -> void: # ⑤start 专项:左列/右列矩形不相交(M2 双列契约——装配件命名 start_box/start_right 为探针锚点) func _start_columns_contract(main: Node) -> void: - var box: Control = main._start_overlay.find_child("start_box", true, false) - var right: Control = main._start_overlay.find_child("start_right", true, false) + var box: Control = main.ui.screens.start_overlay.find_child("start_box", true, false) + var right: Control = main.ui.screens.start_overlay.find_child("start_right", true, false) if box == null or right == null: problems.append("[start][columns] 左列/右列节点缺失(start_box/start_right——装配命名失守)") return diff --git a/game/probe/ui_tune_probe.gd b/game/probe/ui_tune_probe.gd index 4cd3b9a7..dab76e35 100644 --- a/game/probe/ui_tune_probe.gd +++ b/game/probe/ui_tune_probe.gd @@ -43,35 +43,35 @@ func _ready() -> void: # ① 全局浮层角标开窗(调试入口=UI 最顶层浮层,任何 UI 不遮挡—— # main 直接子层、LAYER_CONTRACT 末位 float 类;boot 菜单优先不开浮窗,角标真实点击路径开窗; # 悬浮于菜单之上——开始层不隐藏;树序新基准=登记册末位(确认弹窗之后=绘制序最上)) - main.debug_badge.pressed.emit() + main.ui.screens.debug_badge.pressed.emit() await get_tree().process_frame - if not main.debug_overlay.visible: + if not main.ui.modals.debug_overlay.visible: problems.append("调试浮层角标点击后浮窗未开(全局浮层入口失守)") - if not main._start_overlay.visible: + if not main.ui.screens.start_overlay.visible: problems.append("开始覆盖层被隐藏——浮窗应悬浮于菜单之上(14.5 形态失守)") - if main.debug_overlay.get_index() < main.confirm_dialog.get_index(): + if main.ui.modals.debug_overlay.get_index() < main.ui.modals.confirm_dialog.get_index(): problems.append("调试浮窗树序不在确认弹窗之后——登记册末位失守") # ② 滑条 → 生效预览联动 - main.debug_overlay.set_sliders(100, 50) - if str(boosted_atk) not in main.debug_overlay.effect_text() or str(boosted_hp) not in main.debug_overlay.effect_text(): - problems.append("生效预览未联动(%s)" % main.debug_overlay.effect_text()) + main.ui.modals.debug_overlay.set_sliders(100, 50) + if str(boosted_atk) not in main.ui.modals.debug_overlay.effect_text() or str(boosted_hp) not in main.ui.modals.debug_overlay.effect_text(): + problems.append("生效预览未联动(%s)" % main.ui.modals.debug_overlay.effect_text()) # ③ 应用(真实信号路径:apply_requested → 组合根)——E4 后加成零表写,生效断言移至 ⑧ 战斗快照 - main.debug_overlay.press_apply_for_probe() + main.ui.modals.debug_overlay.press_apply_for_probe() # ④ 重置(重开浮窗 → 重置按钮真实路径;面板百分比归零——下一场战斗生效值回到基线) main._show_debug_overlay() - main.debug_overlay.press_reset_for_probe() + main.ui.modals.debug_overlay.press_reset_for_probe() # ⑤ 批量模拟全链路(N=10——走 level_autoplay 与 headless 扫描同口径;表基线由组合根复位; # 菜单态上下文未锁定=按钮可点,is_running 语义=模拟在跑〔18卡⑦ 与对局禁用态拆分〕) - main.debug_overlay.set_sliders(100, 0) - main.debug_overlay.set_sim_count(10) - main.debug_overlay.press_simulate_for_probe() + main.ui.modals.debug_overlay.set_sliders(100, 0) + main.ui.modals.debug_overlay.set_sim_count(10) + main.ui.modals.debug_overlay.press_simulate_for_probe() var guard := SIM_GUARD_FRAMES - while main.debug_overlay.is_running() and guard > 0: + while main.ui.modals.debug_overlay.is_running() and guard > 0: guard -= 1 await get_tree().process_frame - if main.debug_overlay.is_running(): + if main.ui.modals.debug_overlay.is_running(): problems.append("模拟未在预算帧内完成(疑似挂死)") - var out: String = main.debug_overlay.output_text() + var out: String = main.ui.modals.debug_overlay.output_text() if not out.contains("关卡完成率"): problems.append("模拟输出缺关卡完成率") # 口径换代 V7:胜局统计行措辞钉子(atk+100% 加成档 10 局必有胜局——hp_left_win 非空路径恒出) @@ -83,7 +83,7 @@ func _ready() -> void: problems.append("模拟输出 N 标注错误(%s)" % out.substr(0, 40)) # ⑥ 关闭 → 浮窗收起、开始层全程在屏 main._close_debug_overlay() - if main.debug_overlay.visible or not main._start_overlay.visible: + if main.ui.modals.debug_overlay.visible or not main.ui.screens.start_overlay.visible: problems.append("关闭后层级状态错误") # ⑦ hero 表运行期不可变(E4 收口回归断言:任何代码 reintroduce 表写在此被抓获——原"表值残留"断言语义升级) if int(main.db.combat.hero.atk) != base_atk or int(main.db.combat.hero.max_hp) != base_hp: @@ -93,20 +93,20 @@ func _ready() -> void: # 存档隔离(停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律; # isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退) ProbeUiUtil.isolate_save(main, "smoke_tune_probe_save", true, false, true) # 末参=swap_save(卡②签名换代——断点写盘经 autoload 落隔离档) - main.debug_overlay.set_sliders(100, 50) - main.debug_overlay.press_apply_for_probe() - main.debug_overlay.seed_edit.text = "42" # 种子钉死(18卡⑦审计🔴——空框=非首局随机,显式优先经真实路径断言 master_seed) + main.ui.modals.debug_overlay.set_sliders(100, 50) + main.ui.modals.debug_overlay.press_apply_for_probe() + main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(18卡⑦审计🔴——空框=非首局随机,显式优先经真实路径断言 master_seed) main._start_adventure_from_menu() # 完整冒险路径(唯一试玩入口;_start_single 按语义清零滑条 = 恒基线,不承载加成断言) await get_tree().process_frame await get_tree().process_frame # ⑧b 开冒险路径显式关浮窗(18卡⑦ T2 防护——调试会话默认开态下开局不带窗,对局中经角标再开) - if main.debug_overlay.visible: + if main.ui.modals.debug_overlay.visible: problems.append("开冒险路径未显式关浮窗(T2 防护失守)") if int(main.run_scope.adventure.master_seed) != 42: problems.append("显式种子未达冒险(master_seed=%s ≠ 42——真实路径显式优先失守)" % str(main.run_scope.adventure.master_seed)) # 关卡开始 = 入口选路停点——真实按钮路径选入口候选进首战再读呈现快照 - if main.level_screen.visible: - main.level_screen.press_candidate(0) + if main.ui.modals.level_screen.visible: + main.ui.modals.level_screen.press_candidate(0) await get_tree().process_frame await get_tree().process_frame var hero: Dictionary = main.run_scope.settlement.presentation_snapshot().hero @@ -116,23 +116,23 @@ func _ready() -> void: # ⑨ 对局中可开(18卡⑦ P2 甲/T2 防护——S1+ 停机点回归闸):战斗屏角标真实信号路径开窗; # 棋盘输入不锁、状态角标不置灰(「默认开×遮挡复活」下必锁死的破约回归面); # 对局中批量模拟/两层调试两钮禁用;当局数据六项与只读访问器逐项一致;关闭钮真实路径收窗恢复 - var dbg_badge: Button = main.debug_badge + var dbg_badge: Button = main.ui.screens.debug_badge if dbg_badge == null or dbg_badge.visible: problems.append("对局中调试浮层角标非恒隐(发布态语义失守——卡⑦ 删写死行批)") else: main._show_debug_overlay() # 对局中开浮窗回归经直调承载(:61 既有直调先例——发布态角标恒隐,按钮信号路径退役) - if not main.debug_overlay.visible: + if not main.ui.modals.debug_overlay.visible: problems.append("对局中角标开浮窗失败(P2 甲对局中可达失守)") - if main._input_locked(): + if main.ui.modals.flow.locks_input(): problems.append("对局中开浮窗后棋盘输入锁死(T2 防护失守——S1+ 停机点)") - if main.board_zone.status_badge.disabled: + if main.ui.battle.board_zone.status_badge.disabled: problems.append("对局中开浮窗后状态角标置灰(T2 防护失守——S1+ 停机点)") - if main.debug_overlay.is_running(): + if main.ui.modals.debug_overlay.is_running(): problems.append("对局中上下文锁定误判为模拟在跑(is_running 语义互扰)") - if not main.debug_overlay._sim_btn.disabled or not main.debug_overlay._dbg_btn.disabled: + if not main.ui.modals.debug_overlay._sim_btn.disabled or not main.ui.modals.debug_overlay._dbg_btn.disabled: problems.append("对局中批量模拟/两层调试按钮未禁用(T2 并存态不开放失守)") # 当局数据六项值对(T3——数值与只读访问器逐项一致;种子=adventure.master_seed 现值非 _seed_value 重算) - var rd: Dictionary = main.debug_overlay.run_data_probe() + var rd: Dictionary = main.ui.modals.debug_overlay.run_data_probe() var r = main.run_scope.adventure.runner() if String(rd.get("seed", "")) != "42": problems.append("当局数据种子 ≠ master_seed 现值(%s)" % str(rd.get("seed"))) @@ -146,27 +146,27 @@ func _ready() -> void: problems.append("当局数据 GP 失对(%s ≠ %s)" % [str(rd.get("gp")), str(r.gp)]) if int(rd.get("keystone", -1)) != main.run_scope.adventure.journal().keystone_gained(): problems.append("当局数据钥石失对(%s ≠ %s)" % [str(rd.get("keystone")), str(main.run_scope.adventure.journal().keystone_gained())]) - var close_btn: Button = ProbeUiUtil.find_button(main.debug_overlay, "关闭", "prefix") + var close_btn: Button = ProbeUiUtil.find_button(main.ui.modals.debug_overlay, "关闭", "prefix") if close_btn == null: problems.append("浮窗关闭钮缺失(真实收窗路径中断)") else: close_btn.pressed.emit() - if main.debug_overlay.visible or main._input_locked(): + if main.ui.modals.debug_overlay.visible or main.ui.modals.flow.locks_input(): problems.append("对局中关闭浮窗后态失守(visible=%s locked=%s)" - % [str(main.debug_overlay.visible), str(main._input_locked())]) + % [str(main.ui.modals.debug_overlay.visible), str(main.ui.modals.flow.locks_input())]) # ⑨b 只读巡检(17卡③形制)+零动效契约(17卡③ R5——零 AnimationPlayer+源码零 tween) _overlay_readonly_contract(main, problems) _overlay_zero_motion_contract(main, problems) # ⑩ 种子三分单点断言(18卡⑦ C1——判定只落 _seed_value() 单点;对局态不影响本判定面): # 显式合法整数全场景优先(含首局)→ 首局(存档布尔 false)恒 42 → 非首局空框引擎熵连抽不全等 42 - main.debug_overlay.seed_edit.text = "777" + main.ui.modals.debug_overlay.seed_edit.text = "777" if main._seed_value() != 777: problems.append("显式种子输入未优先(_seed_value != 777——C1 ①失守)") # SaveManager 是 autoload 单例——换注走原地 attach(首局态挂载+恢复隔离档); # --script 模式无裸 autoload 标识符——经 main 节点路径解析 var sm = main.get_node("/root/SaveManager") sm.slots.attach_path("user://smoke_tune_probe_seed.json") # 全新空档——first_adventure_done=false(首局态;新档缺省面) - main.debug_overlay.seed_edit.text = "" + main.ui.modals.debug_overlay.seed_edit.text = "" if main._seed_value() != 42: problems.append("首局空框种子 != 42(P5 甲失守——教学态可复现破约)") sm.slots.attach_path("user://smoke_tune_probe_save.json") # 恢复隔离档(非首局态——isolate_save 已建档置位) @@ -181,7 +181,7 @@ func _ready() -> void: all42 = false if all42: problems.append("非首局空框连抽 4 次全等 42(随机化失守——C1 ③)") - main.debug_overlay.seed_edit.text = "42" # 钉回显式值(现场整洁) + main.ui.modals.debug_overlay.seed_edit.text = "42" # 钉回显式值(现场整洁) for f0 in ["user://smoke_tune_probe_seed.json", "user://smoke_tune_probe_seed.json.tmp"]: if FileAccess.file_exists(f0): DirAccess.remove_absolute(f0) @@ -196,10 +196,11 @@ func _ready() -> void: # 只读巡检(18卡⑦ T3——17卡③「白名单外零连接」形制沿用):浮窗子树全控件 × 游戏语义信号名清单 -# 全连接巡检——白名单={视图自身八方法 + 组合根 _on_modal_closed(closed 外发消费)}; +# 全连接巡检——白名单={视图自身八方法 + 注册面模态编排口 ui.modals.flow.close(closed 外发消费—— +# 注册面批把该连接目标从组合根转发壳改为 flow 真方法,机检白名单随批登记同一连接)}; # 当局数据区块=纯标签零连接在此被一并约束(新增任何连接=红须入册审查)。 func _overlay_readonly_contract(main: Node, problems: Array) -> void: - var overlay: Control = main.debug_overlay + var overlay: Control = main.ui.modals.debug_overlay var stack: Array = [overlay] var conn_count := 0 while not stack.is_empty(): @@ -210,8 +211,9 @@ func _overlay_readonly_contract(main: Node, problems: Array) -> void: var cb: Callable = conn["callable"] var host: Object = cb.get_object() var m := String(cb.get_method()) + var flow_host: Object = main.ui.modals.flow # 注册面模态编排口(closed 外发消费白名单宿主) var ok := (host == overlay and m in _OVERLAY_VIEW_METHODS) \ - or (host == main and m == "_on_modal_closed") \ + or (host == flow_host and m == "close") \ or (host is VScrollBar or host is HScrollBar) # 引擎滚动机构内部 drag 接线(RichTextLabel 自动滚动条——17卡③布局生命线同类豁免,零游戏写通路) if not ok: problems.append("[readonly] 非法信号连接 %s.%s → %s.%s(白名单外零连接巡检——18卡⑦ T3)" @@ -224,7 +226,7 @@ func _overlay_readonly_contract(main: Node, problems: Array) -> void: # 零动效契约(17卡③ R5 形制——子树零 AnimationPlayer + 视图源码零 tween/动效包装调用) func _overlay_zero_motion_contract(main: Node, problems: Array) -> void: - var overlay: Control = main.debug_overlay + var overlay: Control = main.ui.modals.debug_overlay var stack: Array = [overlay] while not stack.is_empty(): var n: Node = stack.pop_back() diff --git a/game/tests/.gates/baseline.json b/game/tests/.gates/baseline.json index 95e64f41..fc904fe3 100644 --- a/game/tests/.gates/baseline.json +++ b/game/tests/.gates/baseline.json @@ -13,9 +13,13 @@ "exit": "1", "fp": "f9865f1a5a44" }, - "03 场景探针": { - "exit": "0", - "fp": "green" + "03 探针 --click": { + "exit": "1", + "fp": "8a3cae0d0446" + }, + "04 探针 --full": { + "exit": "1", + "fp": "591b27250b36" }, "05 渲染闸": { "exit": "1", diff --git a/game/tests/README.md b/game/tests/README.md index 0fc9073e..574f7ee5 100644 --- a/game/tests/README.md +++ b/game/tests/README.md @@ -33,7 +33,7 @@ tests/ | **单元测试** | 领域类的原子行为对不对(公式/状态机/纯逻辑)——从对外方法进、断言钉对外可观测面(写法判据见 test-conventions §8.2) | `tests/unit/` | `run_all.gd` manifest(闸 02) | | **集成测试** | 跨系统的闭环对不对 | `tests/integration/` | 同上(与单元测试同一入口、同一协议) | | **系统测试** | 启动真实工程走真实玩法路径 | 过渡期 `game/probe/smoke_all.gd`(断言迁移批后=`tests/system_test.gd`) | `run_system_test.py`(闸 01) | -| **UI 探针+渲染 golden** | 跑起来之后 UI 与场景切换对不对;画面基线 | `game/probe/*.tscn` 探针 | `probe_gate.sh`(闸 03)、`render_gate.sh`(闸 05) | +| **UI 探针+渲染 golden** | 跑起来之后 UI 与场景切换对不对;画面基线 | `game/probe/*.tscn` 探针 | `probe_gate.sh`(闸 03/04)、`render_gate.sh`(闸 05) | > **integration/ 已建承载**(迭代 29 T3——此前只有章程句无实体,空壳曾于迭代 19 卡① T4 删过一次)。 > 首个集成用例 = `save_rng_roundtrip_test.gd`(存档⇄RNG 快照往返)。章程登记的四类集成用例里, @@ -194,7 +194,7 @@ tests/ | `tools/run_system_test.py`(系统测试) | 真实玩法路径的行为断言面(推理→战斗→结算→进度推进) | 退出码+脚本错误扫描 | 玩法链路改动的调试期确认 | | `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(65 套件 manifest) | 退出码+三硬闸(见下);每套件子进程带截止时间守护——超时先 taskkill /T 整树回收再判该套件 FAIL、批继续(默认 120s,环境变量 `RUN_ALL_SUITE_TIMEOUT_SEC` 可覆盖);连续 3 个套件超时=红树整批挂死,第 3 个即中止整轮、汇总已见失败与超时清单后非 0 退出(阈值=可调常量,不连续的超时照旧逐个回收) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;此形态仅供开发期最窄确认,不产闸回执、不作验收凭据);闸机关联档选跑——同一过滤机制加 `-- --only-source=gates`,由 `tools/run_all_gates.sh` 在 02 闸传入(回执 scope=related,是验收跑批本体) | | `tools/probe_gate.sh`(场景探针,`--click`/`--full` 两模式) | UI 可达性、可见性、状态——真实实例化场景+真实输入路径 | 探针自检+退出码 | 新增 UI 区域的配套探针直跑与断言复核 | -| `tools/render_gate.sh`(渲染 golden) | 画面基线——截图与基线图逐像素容差比对 | golden 比对脚本(依赖 PIL)+退出码/取证双检/三段探针日志脚本错误扫描(`error_scan.py`) | 主题/素材/排版类改动;窗口化运行,需开显示器 | +| `tools/render_gate.sh`(渲染 golden) | 画面基线——截图与基线图逐像素容差比对 | golden 比对脚本(依赖 PIL) | 主题/素材/排版类改动;窗口化运行,需开显示器 | | `game/tests/static_scan.sh`(S 域) | 项目纪律+第三方质量规则:RNG 单点/core 纯度/类型标注/文件头/注释禁词/旧符号残留;S13=gdstyle 质量规则(重复 load、循环内 await、循环内分配、不可达代码等真缺陷类;基线键=文件|规则|函数名(类/文件级=文件|规则,无数字条目带消息),数字漂移不假红,同键指标恶化超基线登记值即红) | 退出码(逐规则 FAIL 行) | 改产品码前后的快速自检;新增纪律先补扫描规则再修码 | | 测量件(`game/sim/`,逐件见下「游离脚本台账」) | 期望/分布读数——只给读数,不判对错 | 人工读 stdout 或配套工具脚本 | 调参与归因;MC 类一律用户发起(档位表见 `sim-data/README.md`「MC 采集档位表」) | diff --git a/game/tests/smoke_boot.gd b/game/tests/smoke_boot.gd index 815dfd96..172017f8 100644 --- a/game/tests/smoke_boot.gd +++ b/game/tests/smoke_boot.gd @@ -67,9 +67,9 @@ func _check_main_scene(main: Node) -> void: return var run_scope: Node = main.get("run_scope") check(run_scope != null and run_scope.is_inside_tree(), "闲时会话 run_scope 在树") - var start_overlay: Control = main.get("_start_overlay") + var start_overlay: Control = main.get("ui").screens.start_overlay check(start_overlay != null and start_overlay.visible, "启动态菜单可见(boot 后玩家见菜单)") check(main.get("settings_store") != null, "系统设置存储已装配(settings_store)") - check(main.get("debug_badge") != null, "调试入口角标已装配(debug_badge)") - check(main.get("legend_overlay") != null, "图例覆盖层已装配(legend_overlay)") - check(main.get("status_panel") != null, "局内状态面板已装配(status_panel)") + check(main.get("ui").screens.debug_badge != null, "调试入口角标已装配(debug_badge)") + check(main.get("ui").modals.legend_overlay != null, "图例覆盖层已装配(legend_overlay)") + check(main.get("ui").modals.status_panel != null, "局内状态面板已装配(status_panel)") diff --git a/game/tests/system_test.gd b/game/tests/system_test.gd index 5002295f..7814e0a7 100644 --- a/game/tests/system_test.gd +++ b/game/tests/system_test.gd @@ -81,20 +81,20 @@ func _smoke_scene_split() -> void: # V1a 启动态顶层断言:boot 后自树顶向下第一个可见内容层=start_overlay(headless 可判) #(内容层遍历——屏壳是容器不当候选:camp 壳 boot 态 vis=true 但成员恒隐,裸树遍历会误命中) var top: Node = _topmost_visible_layer(main) - check(top == main._start_overlay, + check(top == main.ui.screens.start_overlay, "V1a 启动态顶层可见层=start_overlay(实测顶层=%s——菜单被冒险屏盖住即红)〔装配生命批 V1〕" % (top.name if top != null else "null")) # V1b 语义关系:start_overlay 在冒险屏内容之上(菜单屏壳在冒险屏壳之上——装配序修复判据) - check(main._menu_screen.get_index() > main._adventure_screen.get_index(), + check(main.ui.screens.menu_screen.get_index() > main.ui.screens.adventure_screen.get_index(), "V1b 菜单屏壳在冒险屏壳之上(start_overlay 盖住战斗内容)〔装配生命批 V1〕") # V1c 合同树序链逐对保序(直接子层全链=屏壳/模态/全屏态/非视觉节点;宿住层树序随宿主不参与) var chain_detail := "" var chain_prev: Node = null var chain_ok := true - for entry: Dictionary in main.LAYER_CONTRACT: + for entry: Dictionary in main.ui.LAYER_CONTRACT: if entry.has("host"): continue - var node: Node = main._layer_node(String(entry["shell"]) if entry.has("shell") else String(entry["key"])) + var node: Node = main.ui.node(String(entry["shell"]) if entry.has("shell") else String(entry["key"])) if node == null: chain_ok = false chain_detail += " 缺失:%s" % String(entry["key"]) @@ -105,7 +105,7 @@ func _smoke_scene_split() -> void: chain_prev = node check(chain_ok, "V1c 合同树序链逐对保序(运行期树序==LAYER_CONTRACT 序;违例:%s)〔装配生命批 V1〕" % chain_detail) # ③ 探针挂载假设句柄在座(main.X 一级路径——探针迁移面收口证明) - check(main._start_overlay == main._menu_screen.start_overlay and main.camp_overlay == main._camp_screen.camp_overlay and main.level_screen == main._adventure_screen.level_screen and main._battle_root == main._adventure_screen.battle_root and main.board_view == main._adventure_screen.board_view, + check(main.ui.screens.start_overlay == main.ui.screens.menu_screen.start_overlay and main.ui.modals.camp_overlay == main.ui.screens.camp_screen.camp_overlay and main.ui.modals.level_screen == main.ui.screens.adventure_screen.level_screen and main.ui.screens.battle_root == main.ui.screens.adventure_screen.battle_root and main.ui.battle.board_zone.board_view == main.ui.screens.adventure_screen.board_view, "V2 组合根句柄指向屏壳成员(探针挂载假设 main.X 零迁移)") check(main.run_scope != null and not main.run_scope.is_adventure, "V2 闲时会话自 _ready 即在(永非 null;迭代 27 T3——gp 影子账退役无账本成员)") @@ -116,10 +116,10 @@ func _smoke_scene_split() -> void: # 顶层可见内容层(V1a 用——候选=合同模态/全屏态+两屏壳的 boot 内容成员;屏壳本身是容器不当候选: # camp 壳 boot 态 vis=true 但成员恒隐,裸树遍历自顶向下会先命中透明壳) func _topmost_visible_layer(main: Node) -> Node: - var candidates: Array[Node] = [main._start_overlay, main._battle_root, main._inter_overlay, - main.legend_overlay, main.node_overlay, main.level_screen, main.curio_overlay, - main._end_overlay, main.shop_overlay, main.camp_overlay, main.settings_overlay, - main.status_panel, main.career_overlay, main.confirm_dialog, main.debug_overlay] + var candidates: Array[Node] = [main.ui.screens.start_overlay, main.ui.screens.battle_root, main.ui.screens.inter_overlay, + main.ui.modals.legend_overlay, main.ui.modals.node_overlay, main.ui.modals.level_screen, main.ui.modals.curio_overlay, + main.ui.modals.end_overlay, main.ui.modals.shop_overlay, main.ui.modals.camp_overlay, main.ui.modals.settings_overlay, + main.ui.modals.status_panel, main.ui.modals.career_overlay, main.ui.modals.confirm_dialog, main.ui.modals.debug_overlay] var best: Node = null var best_x := -1 var best_y := -1 @@ -167,7 +167,7 @@ func _smoke_boot_chain() -> void: "V2b 设置已应用·减动效(UiKit.motion_scale=%s=settings_store.reduced_motion=%s)〔装配生命批 V2〕" % [ str(ui_kit.motion_scale), str(ss.reduced_motion)]) # ③ 启动态菜单可见 - check(main._start_overlay.visible, + check(main.ui.screens.start_overlay.visible, "V2c 启动态 _start_overlay.visible=true(boot 后玩家见菜单)〔装配生命批 V2〕") # ④ 续玩钮可见态==SaveManager 断点在档态 var sm = main.get_node("/root/SaveManager") @@ -186,22 +186,22 @@ func _smoke_boot_chain() -> void: if FileAccess.file_exists(p): DirAccess.remove_absolute(p) sm.slots.attach_path("user://smoke_boot_chain.json") - check(main._menu_screen.entersavebtn != null and main._menu_screen.settingsbtn != null - and main._menu_screen.quitbtn != null, + check(main.ui.screens.menu_screen.entersavebtn != null and main.ui.screens.menu_screen.settingsbtn != null + and main.ui.screens.menu_screen.quitbtn != null, "V2d 三钮在位·进入存档/设置/退出游戏(续玩钮族退役后主菜单操作组承接)〔卡② mn1/mn8〕") for old_btn in ["lvbtn", "resumebtn", "campbtn", "careerbtn"]: - check(not (old_btn in main._menu_screen), + check(not (old_btn in main.ui.screens.menu_screen), "V2d 四旧钮句柄零命中·%s(彻底撤出——禁隐藏态禁置灰态残留)〔卡② mn2〕" % old_btn) - check(_count_visible_buttons(main._start_overlay) == 3, + check(_count_visible_buttons(main.ui.screens.start_overlay) == 3, "V2d 渲染面可见按钮计数==3(旧钮接回即红——渲染面锚,句柄缝盲区闭合)〔卡② mn1/mn2〕") # ⑤ 闲时会话(_ready 即建、永非 null、在树) check(main.run_scope != null and not main.run_scope.is_adventure and main.run_scope.is_inside_tree(), "V2e run_scope 非空且为闲时会话(is_adventure=false+is_inside_tree)〔装配生命批 V2〕") # ⑥ 调试浮窗 boot 不开+菜单角标在(菜单优先——boot 不自动开浮窗, # 入口=开始屏右下角「调试」角标+对局中两屏角标) - check(not main.debug_overlay.visible, + check(not main.ui.modals.debug_overlay.visible, "V2f 调试浮窗 boot 不自动开(菜单优先)〔2026-10-16 用户令〕") - check(main.debug_badge != null and not main.debug_badge.visible, + check(main.ui.screens.debug_badge != null and not main.ui.screens.debug_badge.visible, "V2f 调试浮层角标发布态恒隐(2026-10-25 删写死行批)") main.queue_free() await process_frame @@ -223,7 +223,7 @@ func _smoke_run_lifecycle() -> void: sm.slots.attach_path("user://smoke_run_lifecycle.json") sm.meta_state.mark_first_adventure_done() # ---- 第一局:开局→注脏→放弃→终局出口→会话收束 ---- - main.debug_overlay.seed_edit.text = "42" + main.ui.modals.debug_overlay.seed_edit.text = "42" main._start_adventure_from_menu() await process_frame check(main.run_scope != null and main.run_scope.is_adventure and main.run_scope.adventure != null, @@ -243,22 +243,22 @@ func _smoke_run_lifecycle() -> void: break await process_frame check(finished.done, "V3 冒险终局到达(abandon→adventure_finished 同步链)") - check(main._end_overlay.is_open(), "V3 终局面板弹起") + check(main.ui.modals.end_overlay.is_open(), "V3 终局面板弹起") main._on_battle_end_continue() # 终局出口(回菜单+会话收束——手工置空清单已消亡) await process_frame check(not main.run_scope.is_adventure and main.run_scope.adventure == null and main.run_scope.runner == null, "V3 会话收束(新闲时会话零冒险/关卡对象)") check(main.run_scope.hints_shown.is_empty() and not main.run_scope.boss_active and main.run_scope.last_gain_amount == 0, "V3 局态封印/账本全复位(queue_free 物化——注入脏状态零残留)") - check(main._start_overlay.visible, "V3 回开始菜单(终局出口路径)") + check(main.ui.screens.start_overlay.visible, "V3 回开始菜单(终局出口路径)") # ---- 第二局:放弃终局无断点残留→直进开局→全新会话 ---- check(not sm.has_breakpoint(), "V3 放弃终局清断点(无残留断点)") - main.debug_overlay.seed_edit.text = "43" + main.ui.modals.debug_overlay.seed_edit.text = "43" main._start_adventure_from_menu() await process_frame check(main.run_scope.is_adventure and main.run_scope.adventure != null and main.run_scope.hints_shown.is_empty(), "V3 第二连局全新会话(两连局零泄漏)") - check(not main._start_overlay.visible, "V3 直进入局(菜单隐)") + check(not main.ui.screens.start_overlay.visible, "V3 直进入局(菜单隐)") main.queue_free() await process_frame for p in ["user://smoke_run_lifecycle.json", "user://smoke_run_lifecycle.json.tmp"]: @@ -281,14 +281,14 @@ func _smoke_sim_guards() -> void: # wd6:闲时态 + 模拟运行态 → 调试两层入口守卫早退(开始层不收起、无结算对象) check(main.run_scope.settlement == null and not main.run_scope.is_adventure, "前置:闲时会话(settlement 空 + is_adventure=false)") - main._start_overlay.visible = true + main.ui.screens.start_overlay.visible = true main._tune_sim._running = true main._start_debug_levels() - check(main.run_scope.settlement == null and main._start_overlay.visible, + check(main.run_scope.settlement == null and main.ui.screens.start_overlay.visible, "模拟运行态下调试两层入口守卫早退(开始层不收起)〔wd6〕") main._tune_sim._running = false # wd7:真实冒险关间态(is_adventure=true、settlement==null)→ 模拟入口守卫早退 - main.debug_overlay.seed_edit.text = "42" + main.ui.modals.debug_overlay.seed_edit.text = "42" main._start_adventure_from_menu() await process_frame check(main.run_scope.is_adventure and main.run_scope.settlement == null, @@ -313,8 +313,8 @@ func _face_snapshot(main: Node) -> Dictionary: var tu = main._treasure_ui var bo = main._bonus var cp = main._camp_pres - var mf = main._modal_flow - var rp = main._row_pacer + var mf = main.ui.modals.flow + var rp = main.ui.screens.row_pacer return { "treasure_settlement": tu._settlement_get.call(), "treasure_runner": tu._runner_get.call(), @@ -381,13 +381,13 @@ func _stage_faces(main: Node, tag: String, adv: String, run: String, set: String if bool(snap["mf_rows_empty"]) != bool(extra[k]): bad += " modal_flow.rows_empty_get 应=%s" % str(extra[k]) "level_vis": - if main.level_screen.visible != bool(extra[k]): + if main.ui.modals.level_screen.visible != bool(extra[k]): bad += " level_screen.visible 应=%s" % str(extra[k]) "end_open": - if main._end_overlay.is_open() != bool(extra[k]): + if main.ui.modals.end_overlay.is_open() != bool(extra[k]): bad += " _end_overlay.is_open 应=%s" % str(extra[k]) "start_vis": - if main._start_overlay.visible != bool(extra[k]): + if main.ui.screens.start_overlay.visible != bool(extra[k]): bad += " _start_overlay.visible 应=%s" % str(extra[k]) check(bad.is_empty(), "%s(引用面违例:%s)〔装配生命批 V3〕" % [tag, bad]) @@ -419,7 +419,7 @@ func _smoke_lifecycle_matrix() -> void: if FileAccess.file_exists(p): DirAccess.remove_absolute(p) sm.slots.attach_path("user://smoke_lifecycle.json") - main.debug_overlay.seed_edit.text = "42" + main.ui.modals.debug_overlay.seed_edit.text = "42" # 阶段①闲时 await _stage_faces(main, "V3a 闲时", "null", "null", "null", {"mf_start": true, "mf_inter": false, "mf_rows_empty": true, "start_vis": true}) @@ -430,7 +430,7 @@ func _smoke_lifecycle_matrix() -> void: await _stage_faces(main, "V3b 开局停点", "alive", "alive", "null", {"mf_start": false, "level_vis": true}) # 阶段③地图选路→战斗中(真实选路提交 press_candidate) - main.level_screen.press_candidate(0) + main.ui.modals.level_screen.press_candidate(0) for i in range(10): await process_frame await _stage_faces(main, "V3c 战斗中", "alive", "alive", "alive", {"mf_start": false}) @@ -465,9 +465,9 @@ func _smoke_lifecycle_matrix() -> void: check(main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live(), "V3d 活战斗驱动完毕(战斗已收场——%d 轮,finished=%s)〔装配生命批 V3〕" % [guard, str(finished.done)]) await process_frame - main._flush_row_queue() + main.ui.screens.row_pacer.flush() for i in range(12): - if main._end_overlay.is_open(): + if main.ui.modals.end_overlay.is_open(): break await process_frame # 分支甲:战斗胜利停点(结算后)→确认进选路→放弃→终局面 @@ -482,13 +482,13 @@ func _smoke_lifecycle_matrix() -> void: if finished.done: break await process_frame - main._flush_row_queue() + main.ui.screens.row_pacer.flush() for i in range(8): - if main._end_overlay.is_open(): + if main.ui.modals.end_overlay.is_open(): break await process_frame # 阶段⑤终局面(败北/放弃皆达) - check(finished.done and main._end_overlay.is_open(), + check(finished.done and main.ui.modals.end_overlay.is_open(), "V3e 终局面到达(adventure_finished+终局面板开)〔装配生命批 V3〕") await _stage_faces(main, "V3e 终局面", "alive", "alive", "null", {"mf_start": false, "end_open": true}) # 阶段⑥回菜单收束(旧局消亡+新会话即驻+无真空窗) @@ -515,7 +515,7 @@ func _smoke_lifecycle_matrix() -> void: main._open_save_select() for i in range(2): await process_frame - var ssel_new: Button = main.save_select_overlay._new_btns.get(1) + var ssel_new: Button = main.ui.modals.save_select_overlay._new_btns.get(1) if ssel_new == null: check(false, "V3g 营地入口缺失(选档层新档钮驱动中断)〔装配生命批 V3〕") continue @@ -551,7 +551,7 @@ func _smoke_lifecycle_matrix() -> void: DirAccess.remove_absolute(slot_p) # 阶段⑧两连局(第二局全新会话——旧局对象不复用、脏态零残留) var first_adv = main.run_scope.adventure - main.debug_overlay.seed_edit.text = "43" + main.ui.modals.debug_overlay.seed_edit.text = "43" main._start_adventure_from_menu() for i in range(6): await process_frame @@ -560,12 +560,12 @@ func _smoke_lifecycle_matrix() -> void: await _stage_faces(main, "V3h 两连局·开局停点", "alive", "alive", "null", {"mf_start": false, "level_vis": true}) check(main.run_scope.adventure.abandon(), "V3h 放弃受理(第二局)〔装配生命批 V3〕") for i in range(12): - if main._end_overlay.is_open(): + if main.ui.modals.end_overlay.is_open(): break await process_frame - main._flush_row_queue() + main.ui.screens.row_pacer.flush() for i in range(8): - if main._end_overlay.is_open(): + if main.ui.modals.end_overlay.is_open(): break await process_frame await _stage_faces(main, "V3h 两连局·终局面", "alive", "alive", "null", {"mf_start": false, "end_open": true}) diff --git a/game/tests/unit/contracts/source_guard_test.gd b/game/tests/unit/contracts/source_guard_test.gd index a0105642..0b8a60aa 100644 --- a/game/tests/unit/contracts/source_guard_test.gd +++ b/game/tests/unit/contracts/source_guard_test.gd @@ -66,12 +66,33 @@ func _smoke_hygiene_gates() -> void: var mv: String = FileAccess.get_file_as_string("res://presentation/main.gd") Kit.check("V8 GP 影子账残留零命中(main 源码面——文件已删+委托缝已拆)〔迭代27 T3〕", not mv.contains("gp_shadow_ledger") and not mv.contains("_gp_ledger")) - # V1d 层序落位单口:main.gd 的 move_child 调用全部收在 _apply_layer_contract 内(合同装配单口—— - # 函数体之外零调用点;出现第二处落位=硬编码层序,须改合同表而非加调用) - var fn_start: int = mv.find("func _apply_layer_contract") - var fn_end: int = mv.find("\nfunc ", fn_start + 10) - if fn_end < 0: - fn_end = mv.length() - var outside_calls: int = (mv.substr(0, fn_start) + mv.substr(fn_end)).count("move_child(") - Kit.check("V1d 层序落位单口(_apply_layer_contract 之外 move_child 调用点=%d 应=0)〔装配生命批 V1〕" % outside_calls, - outside_calls == 0) + # V1d 层序落位单口:注册面建件后扫全 presentation——move_child 只允许出现在 ui_registry.gd 的 + # apply_layer_contract 函数体内(合同装配单口——出现第二处落位=硬编码层序,须改合同表而非加调用) + var scanned: int = 0 + var outside_calls: int = 0 + for fn_path: String in _list_scripts("res://presentation"): + var src: String = FileAccess.get_file_as_string(fn_path) + scanned += 1 + var fn_start: int = src.find("func apply_layer_contract") if fn_path.contains("ui_registry.gd") else -1 + var fn_end: int = src.find("\nfunc ", fn_start + 10) if fn_start >= 0 else -1 + if fn_end < 0: + fn_end = src.length() + outside_calls += (src.substr(0, fn_start) + src.substr(fn_end)).count("move_child(") + Kit.check("V1d 层序落位单口(presentation %d 文件——apply_layer_contract 之外 move_child 调用点=%d 应=0)〔注册面 V1〕" % [scanned, outside_calls], + scanned > 0 and outside_calls == 0) + + +# 目录 .gd 清单(DirAccess 平铺——presentation 无子目录;扫描面>0 前置由调用侧断言承载) +static func _list_scripts(dir_path: String) -> PackedStringArray: + var out: PackedStringArray = PackedStringArray() + var dir := DirAccess.open(dir_path) + if dir == null: + return out + dir.list_dir_begin() + var fname := dir.get_next() + while fname != "": + if fname.ends_with(".gd"): + out.append(dir_path + "/" + fname) + fname = dir.get_next() + dir.list_dir_end() + return out diff --git a/game/tests/unit/presentation/pacing_queue_test.gd b/game/tests/unit/presentation/pacing_queue_test.gd index de6afc4a..7890d23f 100644 --- a/game/tests/unit/presentation/pacing_queue_test.gd +++ b/game/tests/unit/presentation/pacing_queue_test.gd @@ -58,7 +58,7 @@ func _init() -> void: main._start_adventure_from_menu() await process_frame # 入口停点选路进首战(真实停点链) - if not main.level_screen.visible: + if not main.ui.modals.level_screen.visible: _problems.append("入口停点未呈现(前置失守)") _finish() return @@ -100,12 +100,12 @@ func _init() -> void: # ---- 断言①:排空前后核心状态逐位一致(演出关停零分叉)---- var snap_before: String = JSON.stringify(battle.diagnostic_snapshot()) var hp_before: int = int(battle.action_economy.ap) + int(battle.action_economy.rap) * 0 # 观测面双录 - main._flush_row_queue() + main.ui.screens.row_pacer.flush() var snap_after: String = JSON.stringify(battle.diagnostic_snapshot()) check(snap_before == snap_after, "排空前后核心诊断快照逐位一致(AC⑤ 演出关停零分叉)") # ---- 断言②:零丢行(结构判据:排空后队列恒空 + 末信号行原文在带——append_row 直落不吞行)---- check(rows_seen.size() > 0, "结算行已产生(断言前提)") - check(main._row_queue.is_empty(), "排空后队列恒空(flush 完整性)") + check(main.ui.screens.row_pacer.pending_rows().is_empty(), "排空后队列恒空(flush 完整性)") var last_text := "" for i in range(rows_seen.size() - 1, -1, -1): var cand_row: Dictionary = rows_seen[i] @@ -114,7 +114,7 @@ func _init() -> void: last_text = String(cand_row.get("display_name", "")) if last_text != "": break - check(last_text != "" and main.log_view.parsed_text().contains(last_text), + check(last_text != "" and main.ui.battle.log_view.parsed_text().contains(last_text), "末信号行原文在明细带(排空不吞行:『%s』)" % last_text) # ---- 断言③:段间延迟取数面双态 ---- UiKit.motion_scale = 1.0 @@ -129,7 +129,7 @@ func _init() -> void: var fstage := FakeStage.new(null) var ffx := FakeFx.new() rp.stage = fstage - rp.board_view = main.board_view + rp.board_view = main.ui.battle.board_zone.board_view rp.settlement_fx = ffx rp._play_settlement_mark({"type": "damage", "target": "地精", "target_id": "goblin_b", "value": 3}) check(fstage.calls.size() == 1 and fstage.calls[0] == ["goblin_b", "地精"], @@ -144,9 +144,9 @@ func _init() -> void: UiKit.motion_scale = 1.0 # 常态入队(减动效快路同帧直落不入队——夹具须真落队列) main._on_step_row({"type": "damage", "target": "hero", "value": 1}) main._on_step_row({"type": "heal", "target": "hero", "value": 2}) - check(main._row_queue.size() == 2, "会话更替夹具已入队(前提:%d 行)〔wr4〕" % main._row_queue.size()) + check(main.ui.screens.row_pacer.pending_rows().size() == 2, "会话更替夹具已入队(前提:%d 行)〔wr4〕" % main.ui.screens.row_pacer.pending_rows().size()) main._begin_session(false) - check(main._row_queue.is_empty(), "会话更替后待播队列清零(_begin_session 内机制 flush)〔wr4〕") + check(main.ui.screens.row_pacer.pending_rows().is_empty(), "会话更替后待播队列清零(_begin_session 内机制 flush)〔wr4〕") _finish() diff --git a/game/tests/unit/presentation/theme_projection_test.gd b/game/tests/unit/presentation/theme_projection_test.gd index 3a8ab090..4b02313f 100644 --- a/game/tests/unit/presentation/theme_projection_test.gd +++ b/game/tests/unit/presentation/theme_projection_test.gd @@ -722,8 +722,8 @@ func _smoke_diag_button_retired() -> void: var main_src: String = FileAccess.get_file_as_string("res://presentation/main.gd") Kit.check("V6 主区无 build_diag_button 调用残留(旧实现同批清理)", not main_src.contains("build_diag_button")) - Kit.check("V6 转储入口连接随迁(组合根接浮窗 diagnose_requested 信号)", - main_src.contains("debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)")) + Kit.check("V6 转储入口连接随迁(组合根经注册面接浮窗 diagnose_requested 信号)〔注册面 V2〕", + main_src.contains("ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)")) Kit.check("V6 转储编排承接函数在位(dump_diagnosis 卡死取证保留)", main_src.contains("func _on_diagnose_pressed()")) var asm_src: String = FileAccess.get_file_as_string("res://presentation/ui_assembler.gd") diff --git a/reports/需求卡片-测速循环批-2026-10-29.md b/reports/需求卡片-测速循环批-2026-10-29.md index 98b32a9b..c7748f9a 100644 --- a/reports/需求卡片-测速循环批-2026-10-29.md +++ b/reports/需求卡片-测速循环批-2026-10-29.md @@ -1,36 +1,36 @@ # 需求卡片——测速循环批(tune_sim_controller 循环内 await 真实消除) -> **版本状态**:v1.1 定稿——零待裁点(2026-10-30 停卡重裁:实现段 T0 三重证据定谳 v1.0 的「潜伏红」前提已被迭代 41 测试债批消化〔215da315——运行态收尾归还 run〕,按停卡呈报建议案剥离已申报行为改动 V3、T0 义务改现状复核、夹具申报差异两处减一处;2026-10-29 原四点裁决中②作废,①③④仍有效——裁决记录见第 15 节)。 +> **版本状态**:v1.0 定稿——零待裁点(2026-10-29 用户裁:信号步进状态机形态、收尾置假还原作为已申报行为改动、双实现对拍入批、错误路径差异接受并文档化)。 > **接收方**:待办清单条目「零豁免五键清偿批——S13 余量五键真修四卡(2026-10-29 登记,来源=会话提及)」——本卡为其四卡中的测速循环批卡。 -> **来源**:reports/设计-测速循环去await-2026-10-29.md(2026-10-29 设计闭合;§2.3 潜伏红推演与 §10 待裁点 1 的「还原」申报已随停卡重裁作废,§3–§7 形态与等价性论证仍有效)。**唯一事实源**:`reports/设计-测速循环去await-2026-10-29.md` + `design/engineering/architecture/architecture.md` v1.25「测试与质量域」S13 测量面豁免注销句(2026-10-29 零豁免裁:原循环内 `await` 豁免〔2026-10-27〕注销——tune_sim_controller 改 process_frame 信号常驻连接+步进状态机,每帧一局、帧相位逐点对齐;注销对象是 await 豁免、不依赖行为改动申报,句保持;句内「随批申报行为改动一处」半句已失效,随实现批现状化时一并修正——见第 6 节)+ 停卡呈报三重证据(iter44 worktree 分支 `reports/iter44-实现报告-2026-10-30.md` §5——重裁依据)。 +> **来源**:reports/设计-测速循环去await-2026-10-29.md(2026-10-29 设计闭合)。**唯一事实源**:`reports/设计-测速循环去await-2026-10-29.md` + `design/engineering/architecture/architecture.md` v1.25「测试与质量域」S13 测量面豁免注销句(2026-10-29 零豁免裁:原循环内 `await` 豁免〔2026-10-27〕注销——tune_sim_controller 改 process_frame 信号常驻连接+步进状态机,每帧一局、帧相位逐点对齐;随批申报行为改动一处:运行态收尾由「每局置假」还原为「整批收尾单次置假」,消除与 wd8/ui_tune_probe 断言面的静态矛盾)。 ## 1. 目标与完成定义 -这批做什么:把 S13 静态质量闸(gdstyle)在册裸键 `game/presentation/tune_sim_controller.gd|quality/await-in-loop` 真实消除——`run(n)` 的 for 循环内 `await process_frame` 改为 process_frame 信号常驻连接+步进状态机(每帧回调执行一局、步尽次帧收尾断连),`run()` 只留函数级一次 `await _batch_finished` 保持协程身份;改后全文件零 for/while,该键从文本形态根除、此文件对这个键结构性免疫。随批执行一项已裁事项:新增双实现对拍套件,把帧相位等价论证变成当轮真实执行证据。**本批零行为改动**——收尾置假已在 run() 尾每批一次执行(迭代 41 测试债批 215da315 归还后的现行形态),本批收尾四语句同帧同相位迁入 `_finish_batch()`,语句集与语义零变化(第 3、4 节)。 +这批做什么:把 S13 静态质量闸(gdstyle)在册裸键 `game/presentation/tune_sim_controller.gd|quality/await-in-loop` 真实消除——`run(n)` 的 for 循环内 `await process_frame` 改为 process_frame 信号常驻连接+步进状态机(每帧回调执行一局、步尽次帧收尾断连),`run()` 只留函数级一次 `await _batch_finished` 保持协程身份;改后全文件零 for/while,该键从文本形态根除、此文件对这个键结构性免疫。随批执行两项已裁事项:把「每局置假」还原为「整批收尾单次置假」(已申报行为改动——消除与两个在库断言面的静态矛盾);新增双实现对拍套件,把帧相位等价论证变成当轮真实执行证据。 什么状态算完: 1. `tune_sim_controller.gd` 全文件零循环、`await` 仅 `run()` 函数级一处;`python tools/gdstyle_gate.py` 对账该裸键消失(消失键不参与红判定)、无基线外新键。 -2. 测量语义逐点等价(第 4 节四判据)经双实现对拍在当轮真实跑批中证明——五面对拍全等;wd8 三断言与 ui_tune_probe ⑤ 零改动保持绿。 -3. diff 中行为改动零处——收尾四语句迁移为非行为改动(同帧同相位、语义零变化,对照表 V3 行),无任何行为漂移。 +2. 测量语义逐点等价(第 4 节四判据)经双实现对拍在当轮真实跑批中证明——五面对拍全等;wd8 三断言与 ui_tune_probe ⑤ 零改动转绿。 +3. diff 中已申报行为改动恰一处(收尾置假还原,对照表 V3 行),无第二处行为改动。 4. 脚本错误中断路径差异作为声明缺口写入实现报告(禁静默)。 5. 批末代码域闸全绿;实现报告与反哺点清单写齐方算收口。 ## 2. 设计事实源 -设计依据唯一出处=`reports/设计-测速循环去await-2026-10-29.md`(2026-10-29 设计闭合,零待裁点):§3 形态评估(process_frame 信号步进状态机采纳)、§4 推荐案(伪代码结构)、§5 等价性证明清单(四判据+已声明缺口)、§6 键账预期——以上各节不受前提消化影响、仍是本卡依据;该文档 §2.3 潜伏红推演与 §10 待裁点 1「还原」申报已作废(前提消化证据见停卡呈报)。架构面凭据=`design/engineering/architecture/architecture.md` v1.25「测试与质量域」S13 测量面豁免注销句(2026-10-29 零豁免裁落文)——豁免注销句保持,句内行为改动申报半句随实现批现状化修正(第 6 节同步面)。前提消化与重裁凭据=iter44 worktree 分支 `reports/iter44-实现报告-2026-10-30.md` §5 停卡呈报(git 历史证据 215da315+T0 两面实跑绿)。待办清单条目「零豁免五键清偿批」状态=已写卡+已编迭代 44(本卡 v1.1 重裁段产出)。 +设计依据唯一出处=`reports/设计-测速循环去await-2026-10-29.md`(2026-10-29 设计闭合,零待裁点):§4 推荐案(process_frame 信号步进状态机伪代码结构)、§5 等价性证明清单(四判据+已声明缺口)、§6 键账预期、§7 行为零变化风险点。架构面凭据=`design/engineering/architecture/architecture.md` v1.25「测试与质量域」S13 测量面豁免注销句(2026-10-29 零豁免裁落文)——豁免注销、行为改动申报面与本卡一致。待办清单条目「零豁免五键清偿批」状态=文档设计已闭合+待写卡(本卡即其中一卡的写卡段产出)。 -## 3. 现状事实(代码级,2026-10-30 实查;开工 T0 按当轮施工基面重锚——行号基线为本日 master) +## 3. 现状事实(代码级,2026-10-29 实查;开工 T0 按当轮 master 重测重锚——行号基线为本日 master) -- `game/presentation/tune_sim_controller.gd`(全件 68 行):`run(n)` 在 :36-54——:37 置运行态真、:40-42 开共享 JSONL 日志会话、:43 `panel.set_sim_running(true)`、:44 进度 0/n、:45 建 stats 六键束、:46 取自动策略驱动、:47-50 for 循环体(`_run_one_level` → 进度串 → :50 `await panel.get_tree().process_frame`——命中键的语句)、:51-54 收尾四语句(`close_session()` :51 → 报告注入 :52 → `panel.set_sim_running(false)` :53 → `_running = false` :54)——**收尾置假在 run() 尾、for 循环外、每批恰一次**。`_run_one_level` 在 :57 注释/:58-68(5 参),尾部 :66-68 为败局节点记账(defeat 分支)——非置假。类成员 5(db/panel/_base_hp/_driver/_running)+常量 2(:7 SEED_BASE=4200、:8 PlaytestLogger preload);头注 4 行。 -- 前提消化定谳(v1.0「静态矛盾推演」作废依据):git 历史——迭代 41 测试债批提交 215da315(2026-10-29 合回)提交信息明载「两个产品回归随批修(……tune_sim 运行态收尾归还 run)」,收尾置假两行由该批归还 run() 尾并随该批申报;v1.0 成文(2026-10-29)与该批合回同日,其「现状考证」被上游批施工消化变质(读法 A;读法 B「考证当时即误读」与 215da315 提交信息矛盾,停卡呈报 §5 不采)。T0 实跑三重证据(iter44 worktree,停卡呈报 §5):wd8 三断言(现位 :145-147)全 PASS(套件 18 checks 全绿);ui_tune_probe 全链路 PASS(⑤ 守卫环 1800 帧预算内退出+输出四串在)。**潜伏红不存在**——两面现行全绿,「零改动转绿」改「零改动保持绿」。 -- 断言面一〔wd8〕`game/tests/unit/presentation/standee_fx_sweep_test.gd:139-147`:FakePanel(内类 Node,:20-40——setter 面并入树供 get_tree)+FakeDriver(:44-55——`run_level` 回调内读 `ctrl.is_running()` 逐局记 `seen`);:142 直构控制器(字典夹具 db)、:144 `await ctrl.run(2)`(run 必须可等待、挂起至整批完成的契约)、:145-147 三断言——`drv.seen == [true, true]`/收尾 `not ctrl.is_running() and not panel.running`/报告非空。套件在 run_all.gd manifest 在册(res://tests/unit/presentation/standee_fx_sweep_test.gd,expect 0,manifest :188)。 -- 断言面二〔探针 ⑤〕`game/probe/ui_tune_probe.gd:63-83`::65-66 设滑条与 N=10、:67 `press_simulate_for_probe()`(真实按钮 pressed 信号链)、:68-71 守卫环(`while main.ui.modals.debug_overlay.is_running()` 逐帧等——SIM_GUARD_FRAMES=1800 在 :16)、:72-73 预算内未完成判红、:74-83 输出四串断言(关卡完成率/关卡完成剩余 HP 均〔口径换代 V7〕/败北节点/N=10)。面板语义面 `debug_overlay.gd`:`set_sim_running` :81-83 → `_sync_sim_button` :107-110(模拟与两层调试两钮禁用+「模拟中…」文案——浮窗运行态贯穿整批的可见面)、`is_running` :77-79。 -- 调用面 `game/presentation/main_debug_flow.gd`:`_on_tune_simulate` :72-77——守卫(活跃结算/冒险早退)后 :77 `root._tune_sim.run(n)` **不 await 调用**(返回的协程态丢弃——新形态必须保持此调用形态可用、零改动);`_start_debug_levels` :84 守卫读 `root._tune_sim.is_running()`(两层调试入口互查守卫)。 -- 键账:`tools/gdstyle_baseline.txt:15` = 该文件唯一在册键(await-in-loop 裸键,基线 16 键之一)。S13 扫描面=产品码三目录(game/core+data+presentation——probe/sim/tests 不在面,`game/tests/static_scan.sh` S13 头注在案);S8 文件集=**十文件**显式清单(static_scan.sh :317 现行实查——不含本件);S9/S10 扫描面同不含 game/tests/。 +- `game/presentation/tune_sim_controller.gd`(全件 68 行):`run(n)` 在 :36-52——:37 置运行态真、:40-42 开共享 JSONL 日志会话、:43 `panel.set_sim_running(true)`、:44 进度 0/n、:45 建 stats 六键束、:46 取自动策略驱动、:47-50 for 循环体(`_run_one_level` → 进度串 → :50 `await panel.get_tree().process_frame`——命中键的语句)、:51-52 收尾(close_session+报告注入)。`_run_one_level` 在 :56-68(5 参),**尾部两行 :67 `panel.set_sim_running(false)`/:68 `_running = false` 每局执行一次**(潜伏红本体,见下)。类成员 5(db/panel/_base_hp/_driver/_running)+常量 2(:7 SEED_BASE=4200、:8 PlaytestLogger preload);头注 4 行。 +- 断言面一〔wd8〕`game/tests/unit/presentation/standee_fx_sweep_test.gd:126-134`:FakePanel(内类 Node,:20-40——setter 面并入树供 get_tree)+FakeDriver(:44-55——`run_level` 回调内读 `ctrl.is_running()` 逐局记 `seen`);:129 直构控制器(字典夹具 db)、:131 `await ctrl.run(2)`(run 必须可等待、挂起至整批完成的契约)、:132-134 三断言——`drv.seen == [true, true]`/收尾 `not ctrl.is_running() and not panel.running`/报告非空。套件在 run_all.gd manifest 在册(res://tests/unit/presentation/standee_fx_sweep_test.gd,expect 0)。 +- 断言面二〔探针 ⑤〕`game/probe/ui_tune_probe.gd:63-83`::65-66 设滑条与 N=10、:67 `press_simulate_for_probe()`(真实按钮 pressed 信号链)、:68-71 守卫环(`while main.debug_overlay.is_running()` 逐帧等——SIM_GUARD_FRAMES=1800 在 :16)、:72-73 预算内未完成判红、:74-83 输出四串断言(关卡完成率/关卡完成剩余 HP 均〔口径换代 V7〕/败北节点/N=10)。面板语义面 `debug_overlay.gd`:`set_sim_running` :81-83 → `_sync_sim_button` :107-110(模拟与两层调试两钮禁用+「模拟中…」文案——浮窗运行态贯穿整批的可见面)、`is_running` :77-79。 +- 调用面 `game/presentation/main_debug_flow.gd`:`_on_tune_simulate` :72-77——守卫(活跃结算/冒险早退)后 `root._tune_sim.run(n)` **不 await 调用**(返回的协程态丢弃——新形态必须保持此调用形态可用、零改动);`_start_debug_levels` :84 守卫读 `root._tune_sim.is_running()`(两层调试入口互查守卫)。 +- 静态矛盾推演(设计文档 §2.3——T0 实跑定谳)::67-68 每局执行 → wd8 首局完成即把两个运行态置假 → `drv.seen` 实测 `[true, false]` 必红;探针按下返回后首局已在同一同步段内跑完、运行态已假 → 守卫环立即退出 → 输出停在进度串 → 四串断言全红。推断这两行原在循环之后(「循环体提函数」搬运时被卷入提取体——设计文档 §2.3),wd8 写于 d8 批且当时必绿可反证;两面均不在近期批次的关联档选测覆盖内(套件名与涉案文件无字面关联、probe_gate --full 非恒跑三保险),潜伏红因此未暴露。 +- 键账:`tools/gdstyle_baseline.txt:15` = 该文件唯一在册键(await-in-loop 裸键,基线 16 键之一)。S13 扫描面=产品码三目录(game/core+data+presentation——probe/sim/tests 不在面,`game/tests/static_scan.sh` S13 头注在案);S8 文件集=九文件显式清单(static_scan.sh :317——不含本件);S9/S10 扫描面同不含 game/tests/。 - 产品码 process_frame 订阅面(T0 实查项):本件 :50(循环内 await)+`settlement_fx_view.gd:94`(play_damage_number 内一次性 await 等文本度量——战斗结算呈现期,与模拟批不并存)。现库无「run 途中新增订阅者」场景。 - 报告渲染面 `game/presentation/hud_presenter.gd` :197-218 `tune_sim_report`(静态函数,零触碰计划):空数组 `_mean`/`_p90` 双守卫(:221-227/:230-235)、完成率分母 `maxi(1, n)`(:206)——n=0 退化路径渲染安全(本批对拍 n=0 案的前提,实查在案)。 -- 能力表现状:`game/tests/README.md` 能力表「调参测速批量模拟(逐帧步进与统计读数)」行已入表(:131——写卡段 v1.0 同批义务,真实入口=批量模拟入口,代表用例=对拍套件);断言宿主 wd8 套件锚既有行「站桩敌人呈现与结算演出落点」(:130)。 +- 能力表现状:`game/tests/README.md` 能力表零 tune_sim/调试浮窗行;锚到本批断言宿主套件的唯一既有行=「站桩敌人呈现与结算演出落点」(代表用例 `standee_fx_sweep_test.gd:_init`——wd8 断言所在函数)。 ## 4. 设计契约(违反即返工的红线条款,标 🔴 触碰=L3 停机呈报) @@ -40,30 +40,30 @@ ③ 终止条件与终止时读数不变——终止=计数走满 n;close_session/运行态复位/报告注入在同帧执行; ④ 输出逐字不变——报告=未触碰的 `HudPresenter.tune_sim_report`;stats 是 (种子, atk_pct, hp_pct) 的纯函数、帧时点不进测量值;JSONL 由未触碰的自动策略逐行写、close 时点同帧。 违反即返工;等价性对拍失败=L3 停机呈报。 -- 🔴 **行为改动零处(收尾语义零变化)**:收尾置假已在 run() 尾每批一次执行(:53-54,215da315 归还后的现行形态)——本批 `_run_one_level`(:58-68)零触碰、运行态复位语义零变化;收尾四语句(:51-54)迁入 `_finish_batch()` 为同帧同相位语句迁移(判据③承载——非行为改动,对照表 V3 行)。禁任何行为漂移——`_on_tune_simulate` 不加运行态早退守卫(设计文档待裁点 3 备选否决)、发布态零触碰、探针①–④⑥–⑩ 段零触碰、`HudPresenter`/`PlaytestLogger`/`sim/level_autoplay.gd`/`debug_overlay.gd` 零触碰。 +- 🔴 **行为改动唯一=收尾置假还原**(已申报——对照表 V3 行):`_run_one_level` 尾部两行(:67-68)删除,运行态复位收进 `_finish_batch()` 收尾单次执行;禁任何其他行为漂移——`_on_tune_simulate` 不加运行态早退守卫(设计文档待裁点 3 备选否决)、发布态零触碰、探针①–④⑥–⑩ 段零触碰、`HudPresenter`/`PlaytestLogger`/`sim/level_autoplay.gd`/`debug_overlay.gd` 零触碰。 - 🔴 **脚本错误中断路径差异=声明缺口**(设计文档 §5 缺口 1,接受并文档化):GDScript 无错误陷阱 API、差异不可消除——旧形态步进体内脚本错误=协程死亡静默停摆(运行态永真、报告永不落);新形态=计数未推进、次帧重试同一步(确定性错误逐帧重试,探针 1800 帧守卫可抓;生产态=持续重试的可见坏,与旧「静默卡死」同级不同形)。实现报告文档化义务(T3),禁静默、禁造第三条路(错误陷阱/守卫兜底均已否决);测量批遇错本就整批作废,两形态都不产出可用读数。 -- 🔴 **S8 结算路径 await 零命中面不触碰**:本件不在 S8 文件集(十文件显式清单实查不含本件);本批零触碰 S8 文件集与一切闸件。 +- 🔴 **S8 结算路径 await 零命中面不触碰**:本件不在 S8 文件集(九文件显式清单实查不含本件);本批零触碰 S8 文件集与一切闸件。 - 🔴 **拆分边界纪律**(判据权威=code-conventions §1.1,卡内执行条款):本批零拆出单元——步进状态机收编本件(职责语句与不拆理由见第 8 节);①拆出单元须有可陈述职责语句与单一变化原因;②命不出名字、只能说「剩下的成员」的打包=凑计数袋,禁;③「过阈值」只作发现信号——本批键消除靠形态根除、不靠拆件凑数;④按纪律拆不出内聚单元→停卡呈报;⑤禁沿阈值轴拆。 - 🔴 **RNG 与种子零触碰**:SEED_BASE=4200 与逐局种子切分(SEED_BASE+i)零变化——stats 是种子纯函数是等价性论证的前提。 - 🔴 **注释纪律**:新增头注/注释零变更叙事(「由 await 改为信号」类句禁——沿革归 git);S11 注释禁词由术语表解析供给,新增注释用白话描述现行形态与信号契约。 ## 5. 范围边界 -做什么:第 3 节四件事——T0 现状复核(两面实跑应绿)、`tune_sim_controller.gd` 步进状态机改造(收尾语义零变化)、双实现对拍套件、键账对账与收口。 +做什么:第 3 节四件事——T0 现状定谳(实跑两断言面)、`tune_sim_controller.gd` 步进状态机改造+收尾还原、双实现对拍套件、键账对账与收口。 不做什么(明示去向): - 零豁免其余四键(main.gd 类变量 49、battle_orchestrator 类变量 38、board_cell_unit 30、standee_body 19)——各归组合根注册面批/战斗编排归约批/呈现层状态件批(同条目四卡,待办清单在册)。 - `tools/gdstyle_baseline.txt` 禁实现批中途刷新——批末由用户人工重生成(第 6 节)。 - `_on_tune_simulate` 运行态早退守卫——不收件(+1 行为面、system_test 前置断言随之需复核,代价大于收益,设计文档待裁点 3 否决)。 -- `design/` 整个目录、两份根流程清单——实现批禁写;该改的点列反哺点清单。`game/tests/README.md`:测量面专属能力行已随本卡 v1.0 写卡段同批入表(「调参测速批量模拟」行),实现批禁写红线不变。 +- `design/` 整个目录、两份根流程清单——实现批禁写;该改的点列反哺点清单。`game/tests/README.md`:测量面专属能力行已随本卡写卡段同批入表(「调参测速批量模拟」行——写卡同批义务,grilling 复核订正),实现批禁写红线不变。 - 探针其余段与套件其余断言族(standee_fx_sweep 的 wd2/wd4/wd5/wd12 面)零触碰;`architecture.md` 其余豁免条目(battle_orchestrator 第三条注销句现状化归战斗编排归约批反哺点)零触碰。 ## 6. 关联文档同步面 | 文档 | 关联点(本批实现会改变它的哪条陈述) | 同步义务 | |---|---|---| -| design/engineering/architecture/architecture.md | 「测试与质量域」S13 测量面豁免注销句(v1.25——「设计闭合、实现批建件」批后现状化;句保持——注销对象是 await 豁免、不依赖行为改动申报;句内「随批申报行为改动一处(收尾置假还原)」半句已失效〔前提被迭代 41 消化、申报已剥离〕,现状化时一并修正为收尾现行陈述) | 设计权威文档——实现批禁直改;逐条列进 `reports/iter44-反哺点-2026-10-30.md`(豁免注销句现状化,该清单 #6 已登记暂停点、随本卡 v1.1 恢复),用户裁决后归文档批落文 | +| design/engineering/architecture/architecture.md | 「测试与质量域」S13 测量面豁免注销句(v1.25——「设计闭合、实现批建件」批后现状化;随批申报行为改动句随之成为已实施事实) | 设计权威文档——实现批禁直改;逐条列进 `reports/iter-反哺点-<日期>.md`(豁免注销句现状化),用户裁决后归文档批落文 | | game/tests/README.md | 能力表——测量面专属能力行「调参测速批量模拟(逐帧步进与统计读数)」已随本卡写卡段同批入表(真实入口=批量模拟入口,代表用例=本卡对拍套件);manifest 计数行随套件增减浮动(自助声明,零义务) | 机械面文档——实现批禁写;入表已完成于写卡段(本卡 v1.0 同批),实现期零改动 | | tools/gdstyle_baseline.txt | tune_sim await-in-loop 裸键(:15,该文件唯一在册键)随键消失成死键 | 重生成为人工专用——批末由用户执行 `python tools/gdstyle_gate.py --refresh`(键数 16→15,降幅 <10% 不需 --force);禁实现批中途刷新 | | design/gdd/(全目录) | 无关联文档——测量工具私有机制改造,不改游戏规则与玩家可见行为(tune_sim 全链路发布态不可达,设计文档 §7.3) | 无修订预期;契约句被触碰=停卡呈报走反哺点通道 | @@ -72,18 +72,18 @@ | 文件 | 新增/复用 | 职责 | 行为改动 | |---|---|---|---| -| game/presentation/tune_sim_controller.gd | 复用(改造) | 批量模拟控制器——process_frame 信号常驻连接+步进状态机;收尾单点 `_finish_batch`(四语句同帧同相位迁入);全文件零循环 | 非行为改动(结构改形——收尾语义零变化,零漂移由对拍证;回归锚面=对照表 V3 行) | -| game/tests/unit/presentation/tune_sim_batch_parity_test.gd | 新增 | 双实现对拍配对套件——现行 await 形态夹具(内类,收尾在 run() 尾的现行形态)+五面对拍断言 | 非行为改动(测试面) | +| game/presentation/tune_sim_controller.gd | 复用(改造) | 批量模拟控制器——process_frame 信号常驻连接+步进状态机;收尾单点 `_finish_batch`;全文件零循环 | 行为改动一处(V3 收尾还原)+结构改形(V1——零漂移由对拍证) | +| game/tests/unit/presentation/tune_sim_batch_parity_test.gd | 新增 | 双实现对拍配对套件——遗留 await 形态夹具(内类)+五面对拍断言 | 非行为改动(测试面) | | game/tests/run_all.gd | 复用(改造) | manifest 增一行(新套件注册,expect 0) | 非行为改动(测试基建) | -结构图与第 8 节互指:新增件一件(对拍套件),其「为什么不能复用/落点唯一」理由见第 8 节;其余两行均为既有文件改造。行为改动列全为零——每行的验收落点见对照表 V1/V2/V3 行。 +结构图与第 8 节互指:新增件一件(对拍套件),其「为什么不能复用/落点唯一」理由见第 8 节;其余两行均为既有文件改造。 ## 8. 落位与可复用性预检 - **步进状态机不引入新文件(本件内改造)——为什么不拆新件**:单一职责已在件内——`tune_sim_controller` 的职责就是批量模拟控制,步进机制(每帧一局的节律与帧相位承载)是它的私有运行机制,随批生灭(run 期活成员,批末失效下次 run 重写),没有独立于批量模拟的存在意义;拆出「步进机」件命不出独立职责语句=凑计数袋(code-conventions §1.1 判据②④禁)。复用面核查:全仓 grep 无信号步进机同职责实现可复用;其余四形态(SceneTreeTimer 链/Timer 节点/递归协程/线程)已被设计文档 §3 逐一否决(相位不同或键不消)。 -- **对拍套件落 `game/tests/unit/presentation/` 的唯一性与命名**:落点唯一——与断言宿主 wd8 同目录同协议(extends SceneTree+Kit 收尾),单元层承载;命名 `tune_sim_batch_parity_test.gd`(领域命名、无 ID 前缀);grep 同名与同义标识符实查无第二处。**遗留 await 形态副本全仓只许此一份**——产品码禁留第二实现(旧形态只活在测试夹具内);tests/ 不在 S13/S8/S9/S10 扫描面(扫描面=产品码三目录与 S8 文件集,实查在案),夹具内循环 await 合法在册。 +- **对拍套件落 `game/tests/unit/presentation/` 的唯一性与命名**:落点唯一——与断言宿主 wd8 同目录同协议(extends SceneTree+Kit 收尾),单元层承载;命名 `tune_sim_batch_parity_test.gd`(领域命名、无 ID 前缀);grep 同名与同义标识符实查无第二处。**遗留 await 形态副本全仓只许此一份**——产品码禁留第二实现(旧形态只活在测试夹具内);tests/ 不在 S13/S8/S9/S10 扫描面(扫描面=产品码三目录与 B1 九文件清单,实查在案),夹具内循环 await 合法在册。 - **复用面**:夹具的假面板/假驱动复用 wd8 的形态先例(FakePanel/FakeDriver 同形——代码在各套件内自建,不跨套件共享 helper,禁散落);收尾复用 `test_kit.gd` 的 Kit.finish(零断言专用退出码协议)。 -- **分层与交互**:控制器保持 RefCounted、零 Node 入树;process_frame 连接经 `panel.get_tree()` 取树(与现状同一出口);对拍套件 `--script` 模式零 autoload 依赖(db 用字典夹具,同 wd8 :142 先例);零新增跨层出口、零数值表读取、零随机。 +- **分层与交互**:控制器保持 RefCounted、零 Node 入树;process_frame 连接经 `panel.get_tree()` 取树(与现状同一出口);对拍套件 `--script` 模式零 autoload 依赖(db 用字典夹具,同 wd8 :129 先例);零新增跨层出口、零数值表读取、零随机。 ## 9. 实现规范预算(gdstyle 默认阈值对账:公开方法 ≤20、类变量 ≤15、单函数分支 ≤8、返回 ≤6、参数 ≤5、局部 ≤10、函数 ≤50 行) @@ -93,7 +93,7 @@ |---|---|---| | quality/await-in-loop(裸键,本案标的) | **消除** | 全文件零 for/while;await 仅 `run()` 函数级一处(`await _batch_finished`)——结构性免疫 | | quality/allocation-in-loop | 不命中 | 文件零循环;`PlaytestLogger.new()` 在 run 顶层(现状即如此) | -| max-function-length ≤50 | 达标 | run 约 22 行、`_step_frame` 约 12 行、`_finish_batch` 约 6 行、`_run_one_level` 现行 11 行零触碰 | +| max-function-length ≤50 | 达标 | run 约 22 行、`_step_frame` 约 12 行、`_run_one_level` 删两行后约 10 行 | | max-branches ≤8 | 达标 | `_step_frame` 一个 if/else;run 一个 if/else | | max-local-variables ≤10 | 达标 | run 局部 3(logger/stats/autoplay——余者转成员);`_step_frame` 局部 1 | | max-returns ≤6 | 达标 | 全 void | @@ -108,15 +108,15 @@ ## 11. 任务卡 T0..T3(估时为区间;工作量限制只属迭代层——合计 1–1.5 工作日) -**T0 现状复核与读数重锚(0.25 工作日)** +**T0 现状定谳与读数重锚(0.25 工作日)** -- [ ] 实跑 wd8 单套件(开发期最窄确认,不作验收凭据):`godot --headless --path game --script res://tests/run_all.gd -- --only=standee_fx_sweep`——记录 :145-147 三断言实况,**应绿**(绿=前提已被迭代 41 测试债批消化,按无行为改动形态开工)。套件若因在册无关债先红于 wd8 断言,先核该债修复状态、不得借本批顺手修非申报面——读数以「wd8 断言可达性」为准呈报(L2 记录) -- [ ] 实跑 ui_tune_probe 单探针:`…console.exe --headless --path game res://probe/ui_tune_probe.tscn`——记录 ⑤ 守卫环与输出四串实况,**应在** -- [ ] 判读:两面实跑全绿=重裁前提成立开工;任一面红=现状与重裁前提失配(迭代 41 修复被回退或新潜伏红)——L3 停卡呈报(第 13 节) -- [ ] 按当轮施工基面重锚第 3 节全部 file:line 与成员计数;漂移处就地改卡并记录(本卡行号基线=2026-10-30 master;同迭代卡①改址过 main_debug_flow/ui_tune_probe——两件相关行实测未漂移,仍以 T0 重锚为准) +- [ ] 实跑 wd8 单套件(开发期最窄确认,不作验收凭据):`godot --headless --path game --script res://tests/run_all.gd -- --only=standee_fx_sweep`——记录 :132-134 三断言实况;红=潜伏红定谳成立。套件若因在册无关缺陷先红于 wd8 断言(待办清单「迭代 41 测试债修复」在册的 set_trans 一类),先核该债修复状态、不得借本批顺手修非申报面——读数以「wd8 断言可达性」为准呈报(L2 记录) +- [ ] 实跑 ui_tune_probe 单探针:`…console.exe --headless --path game res://probe/ui_tune_probe.tscn`——记录 ⑤ 守卫环与输出四串实况 +- [ ] 判读:两面按第 3 节静态推演红=前提成立开工;两面实跑全绿=潜伏红证伪=设计前提崩塌——L3 停卡呈报(第 13 节) +- [ ] 按当轮 master 重锚第 3 节全部 file:line 与成员计数;漂移处就地改卡并记录 - [ ] 实查产品码 process_frame 订阅面——预期两处(本件 :50+settlement_fx_view.gd:94);出现 run 途中新增订阅者场景=L2 记录+代码评审注记(设计文档 §5 缺口 2) -产出:现状复核读数(两面绿——前提消化链条的当轮证据,独立验收审计要核对);重锚后行号。 +产出:潜伏红定谳读数(改动前基线证据,独立验收审计要核对);重锚后行号。 **T1 步进状态机改造(0.25–0.5 工作日)** @@ -124,16 +124,16 @@ - [ ] 增 `signal _batch_finished` 与 6 个 run 期活成员(名字按设计文档 §4.1 逐字:`_sim_n: int`/`_sim_i: int`/`_sim_stats: Dictionary`/`_sim_autoplay`/`_sim_logger`/`_finished: bool`) - [ ] `run(n)` 改形:setup 段(:37-46 语句逐字保持);n>0 分支——`is_connected` 守卫 connect `panel.get_tree().process_frame`(恰好连接一次,迭代 39 先例)→ `_step_frame()` 首步同步执行(与 await 形态「第 0 局在调用栈内跑完」对齐)→ `await _batch_finished`(函数级单次 await——键消除落点);n=0 分支直调 `_finish_batch()`(循环零进同步收尾,与 await 形态一致不挂起) - [ ] 新增 `_step_frame() -> void`(0 参):`_sim_i < _sim_n` 分支——`_run_one_level(_sim_autoplay, _sim_stats, _sim_i, panel, _sim_logger)`+`panel.set_sim_progress("模拟中:%d/%d …" % [_sim_i + 1, _sim_n])`(串逐字保持现码 :49)+`_sim_i += 1`;else 分支——先 disconnect(防再入)→ `_finish_batch()` → `_batch_finished.emit()` -- [ ] 新增 `_finish_batch() -> void`(0 参):收尾四语句由 run() 尾 :51-54 迁入(`close_session()`→报告注入→`panel.set_sim_running(false)`→`_running = false`,同帧同相位——判据③;`_finished = true` 置位为新增首句,对两断言面不可观测——白名单 5) -- [ ] `_run_one_level` **零触碰**(:58-68 逐字保持——败局节点记账非置假;本批行为零改动的锚面) -- [ ] 孤立验证:`--only=standee_fx_sweep` 全绿(wd8 三断言零改动保持绿=收尾语义零变化实证)+ui_tune_probe 单探针绿(probe_gate --full 段承载) +- [ ] 新增 `_finish_batch() -> void`(0 参):`_finished = true` → `close_session()` → `panel.set_sim_running(false)` → `_running = false` → `panel.set_sim_report(HudPresenter.tune_sim_report(_sim_n, panel.atk_pct(), panel.hp_pct(), _sim_stats, _base_hp))`(四语句相对次序按此草图,对两断言面不可观测——白名单 5) +- [ ] `_run_one_level` 删尾部两行(:67-68),其余逐字保持(5 参签名不变) +- [ ] 孤立验证:`--only=standee_fx_sweep` 全绿(wd8 三断言零改动转绿=潜伏红消除实证)+ui_tune_probe 单探针绿(probe_gate --full 段承载) - [ ] 自检:新增头注/注释零变更叙事、S11 禁词零命中 -取用:T0 重锚行号。产出:`_batch_finished` 信号与 `_step_frame`/`_finish_batch` 签名(T2 对拍新形态即产品码本体);wd8 保持绿读数。 +取用:T0 重锚行号。产出:`_batch_finished` 信号与 `_step_frame`/`_finish_batch` 签名(T2 对拍新形态即产品码本体);wd8 转绿读数。 **T2 双实现对拍夹具(0.25–0.5 工作日)** -- [ ] 建 `game/tests/unit/presentation/tune_sim_batch_parity_test.gd`(extends SceneTree,Kit 收尾,全类型标注):内类 FakePanel(Node——wd8 同形 setter 面,另带进度串/报告记录数组)+FakeDriver(RefCounted——固定返回值两档〔victory/defeat,同 wd8 :53-55 取值〕,零随机;逐局记录帧索引与运行态读数,帧索引经宿主注入的计数 Callable 读取)+AwaitLoopRunner(RefCounted——现 `run()` await 形态逐字副本,含循环内 await 与收尾四语句在 run() 尾的现行形态(:47-54 语句逐字),含自身 `is_running()` 镜像;**与产品码新形态的申报差异仅一处**:批末把 stats 六键束存夹具成员供对拍读取——产品码 stats 经报告公开面观测,第 17 节直查受控例外申报) +- [ ] 建 `game/tests/unit/presentation/tune_sim_batch_parity_test.gd`(extends SceneTree,Kit 收尾,全类型标注):内类 FakePanel(Node——wd8 同形 setter 面,另带进度串/报告记录数组)+FakeDriver(RefCounted——固定返回值两档〔victory/defeat,同 wd8 :53-55 取值〕,零随机;逐局记录帧索引与运行态读数,帧索引经宿主注入的计数 Callable 读取)+AwaitLoopRunner(RefCounted——现 `run()` await 形态逐字副本,含自身 `is_running()` 镜像;**与产品码的申报差异仅两处**:尾部两行按还原语义置收尾、批末把 stats 六键束存夹具成员供对拍读取) - [ ] 对拍驱动:两形态各经 `run(n)` 真实入口驱动(新形态=真 TuneSimScript 直构,旧形态=夹具直构;同参假面板/假驱动各自实例);拓扑钉死——测试宿主先连 process_frame 帧计数、后启动两形态(两形态下「计数先于步进体」相对次序同构,序列可比);n=3 常规+n=0 退化两案 - [ ] 五面对拍断言(落点 `_init`,尾缀〔测速 V1〕..〔测速 V5〕):V1 帧索引序列——两形态逐局记录序列逐位相等(每步恰一帧+相位同点的执行证据);V2 运行态序列——驱动内 `is_running()` 读数序列逐位相等+收尾双运行态假相等;V3 进度串序列——面板记录的 set_sim_progress 串序列逐位相等;V4 stats 六键逐位——夹具暴露 stats 与测试内按假驱动固定返回值重算的六键期望(wins/dmg_all/hp_left_win/hp_boss/rounds_all/death_nodes)逐键相等;V5 报告全文逐字——两形态面板记录的 set_sim_report 串逐字相等,n=3 案含「关卡完成率/关卡完成剩余 HP 均/败北节点/N=」渲染面(n=0 案只断逐字相等——空 stats 渲染无剩余 HP 行,`_mean`/`_p90` 空守卫实查在案) - [ ] `run_all.gd` manifest 增行 `{"path": "res://tests/unit/presentation/tune_sim_batch_parity_test.gd", "expect": 0}` @@ -147,27 +147,27 @@ - [ ] 关联档复跑:`--only=standee_fx_sweep,tune_sim_batch_parity`+ui_tune_probe(probe_gate --full 段)全绿 - [ ] 批末代码域闸全绿:`tools/run_all_gates.sh --code`(改动含 game/ 下非 .md 行为载体——`--code` 档) - [ ] 错误路径声明缺口写入实现报告(第 4 节 🔴 条全文迁入——禁静默) -- [ ] 产出物落位卡:`reports/iter44-实现报告-2026-10-30.md`(各卡状态+实机目检总清单〔本批零目检项〕+遗留读数义务+实现决定记录)与 `reports/iter44-反哺点清单-2026-10-30.md`(第 6 节同步面逐条落点)写齐,方算收口 +- [ ] 产出物落位卡:`reports/iter-实现报告-<日期>.md`(各卡状态+实机目检总清单〔本批零目检项〕+遗留读数义务+实现决定记录)与 `reports/iter-反哺点清单-<日期>.md`(第 6 节同步面逐条落点)写齐,方算收口 -编队注记:本卡与零豁免其余三卡无产品码触面交集(本卡触面=tune_sim_controller.gd+game/tests/ 两件);run_all.gd manifest 行与并行批可能交叠——编队归编排段。本卡在 iter44 worktree 施工(卡①卡② 已收口冻结在分支),T0 重锚以 worktree 基面为准。 +编队注记:本卡与零豁免其余三卡无产品码触面交集(本卡触面=tune_sim_controller.gd+game/tests/ 两件);run_all.gd manifest 行与并行批可能交叠——编队归编排段(待办清单条目口径:编队待迭代 42 合回)。 ## 12. 验证闸——改动—验收对照表 -域口径:本批改动全为 `game/` 下非 .md 行为载体——批末跑代码域闸;里程碑步(T 段)出口跑一次全套闸,波内修复只做出红最小单元的最窄确认,新增测试先行孤立验证再并入全套(测试执行两入口铁规不破)。三件保险(冒烟/系统测试/静态扫描)恒跑;02 单元与集成按关联档选套件——**本卡显式申报套件:standee_fx_sweep_test(wd8——套件名与涉案文件无字面关联,diff 推导可能漏选,特此申报)+tune_sim_batch_parity_test(新增)**。测试方式两分法:本批全部调试接口自动化(假驱动/假面板直构+真实按钮信号链探针),零用户目检项(tune_sim 全链路发布态不可达——登记不阻塞条款零登记项)。**静态判据申报(不入对照表,test-conventions §8.4 出口)**:S13 await-in-loop 裸键消除无独立运行期提交面——判据=`python tools/gdstyle_gate.py` 与 `game/tests/static_scan.sh` 退出码(T3 执行),属静态判据+评审注记申报观测面局限;对拍夹具不在 S13/S8/S9/S10 扫描面的声明同此口径(扫描面=产品码三目录与 S8 文件集,static_scan.sh 实查在案)。**声明缺口(不入对照表——证明做不出出口)**:脚本错误中断路径差异不可消除,见第 4 节 🔴 条,实现报告文档化(T3)。**能力清单**:本批新增能力一行「调参测速批量模拟(逐帧步进与统计读数)」已随本卡 v1.0 写卡段同批入表(README :131)——对照表三行能力列锚该行;manifest 计数行随套件增减浮动。 +域口径:本批改动全为 `game/` 下非 .md 行为载体——批末跑代码域闸;里程碑步(T 段)出口跑一次全套闸,波内修复只做出红最小单元的最窄确认,新增测试先行孤立验证再并入全套(测试执行两入口铁规不破)。三件保险(冒烟/系统测试/静态扫描)恒跑;02 单元与集成按关联档选套件——**本卡显式申报套件:standee_fx_sweep_test(wd8——套件名与涉案文件无字面关联,diff 推导可能漏选,特此申报)+tune_sim_batch_parity_test(新增)**。测试方式两分法:本批全部调试接口自动化(假驱动/假面板直构+真实按钮信号链探针),零用户目检项(tune_sim 全链路发布态不可达——登记不阻塞条款零登记项)。**静态判据申报(不入对照表,test-conventions §8.4 出口)**:S13 await-in-loop 裸键消除无独立运行期提交面——判据=`python tools/gdstyle_gate.py` 与 `game/tests/static_scan.sh` 退出码(T3 执行),属静态判据+评审注记申报观测面局限;对拍夹具不在 S13/S8/S9/S10 扫描面的声明同此口径(扫描面=产品码三目录与 B1 九文件清单,static_scan.sh 实查在案)。**声明缺口(不入对照表——证明做不出出口)**:脚本错误中断路径差异不可消除,见第 4 节 🔴 条,实现报告文档化(T3)。**能力清单**:本批新增能力一行「调参测速批量模拟(逐帧步进与统计读数)」已随本卡写卡段同批入表(写卡同批义务,grilling 复核订正原「既有名锚定+后续入表」悬置形态)——对照表三行能力列改锚该行;manifest 计数行随套件增减浮动。 | 行 | 需求点 | 改动(文件:行/行为) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 能力行 | |---|---|---|---|---|---|---|---| -| V1 | 步进状态机改造(循环内 await 消除的落码面) | tune_sim_controller.gd :36-54 改形(run 函数级单次 `await _batch_finished`,全文件零循环;收尾四语句 :51-54 迁入 `_finish_batch`)+:58 起新增 `_step_frame`/`_finish_batch` | 复用 wd8 驱动形态 `await ctrl.run(2)`(:144——run 协程身份契约,零改动);新增对拍 V4/V5(stats 六键逐位+报告逐字,落点 tune_sim_batch_parity_test.gd:_init,尾缀〔测速 V4/V5〕) | 单元 | TuneSimScript 直构(字典夹具 db+假面板+假驱动,wd8 :142 同款)经 `run(n)` 真实入口;对拍两形态同入口 | run 可等待(await 挂起=整批完成放行)+stats 六键逐位+报告全文逐字 | 新增:调参测速批量模拟(逐帧步进与统计读数) | +| V1 | 步进状态机改造(循环内 await 消除的落码面) | tune_sim_controller.gd :36-52 改形(run 函数级单次 `await _batch_finished`,全文件零循环)+:56-68 拆出 `_step_frame`/`_finish_batch` | 复用 wd8 驱动形态 `await ctrl.run(2)`(:131——run 协程身份契约,零改动);新增对拍 V4/V5(stats 六键逐位+报告逐字,落点 tune_sim_batch_parity_test.gd:_init,尾缀〔测速 V4/V5〕) | 单元 | TuneSimScript 直构(字典夹具 db+假面板+假驱动,wd8 :129 同款)经 `run(n)` 真实入口;对拍两形态同入口 | run 可等待(await 挂起=整批完成放行)+stats 六键逐位+报告全文逐字 | 新增:调参测速批量模拟(逐帧步进与统计读数) | | V2 | 双实现对拍(帧相位等价的执行化证据) | tune_sim_batch_parity_test.gd 新建+run_all.gd manifest 增行 | 新增五面对拍断言〔测速 V1–V3〕:帧索引序列/运行态序列/进度串序列逐位相等(落点 tune_sim_batch_parity_test.gd:_init) | 单元 | 两形态各经 `run(n)` 真实入口(n=3 常规+n=0 退化);帧索引经测试宿主 process_frame 计数 | 两形态五面记录序列逐位相等(每步恰一帧+相位同点的当轮执行证据——判据本体) | 新增:调参测速批量模拟(逐帧步进与统计读数) | -| V3 | 运行态语义零变化(收尾迁移的回归锚面——非行为改动行) | 收尾四语句(:51-54)迁入 `_finish_batch` 同帧同相位——非行为改动判断依据:语句集与语义零变化、对两断言面不可观测,等价证据由对拍 V2/V5 承载;`_run_one_level` :58-68 零触碰 | 复用 wd8 三断言〔wd8〕(:145-147——`drv.seen == [true, true]`/收尾双运行态假/报告非空——零改动**保持绿**)+ui_tune_probe ⑤ 守卫环与输出四串(:63-83——零改动保持绿) | 单元+探针 | wd8 假驱动回调读 `is_running()`(`await ctrl.run(2)`);探针 `press_simulate_for_probe()` → 按钮 pressed 信号链 → `run(10)` → 守卫环逐帧复查 | 驱动逐局运行态读数序列 [true, true]+收尾双运行态假+守卫环 1800 帧预算内退出+`output_text()` 四关键串(浮窗运行态贯穿整批的可观测提交面——绿=保持绿,非转绿) | 新增:调参测速批量模拟(逐帧步进与统计读数) | +| V3 | 行为改动——收尾置假还原(已申报唯一行为改动) | tune_sim_controller.gd :67-68 删(每局置假)→ `_finish_batch` 收尾单次置假 | 复用 wd8 三断言〔wd8〕(`drv.seen == [true, true]`/收尾 `ctrl.is_running()` 假+`panel.running` 假/报告非空——零改动)+ui_tune_probe ⑤ 守卫环与输出四串(:63-83——零改动) | 单元+探针 | wd8 假驱动回调读 `is_running()`(`await ctrl.run(2)`);探针 `press_simulate_for_probe()` → 按钮 pressed 信号链 → `run(10)` → 守卫环逐帧复查 | 驱动逐局运行态读数序列 [true, true]+收尾双运行态假+守卫环 1800 帧预算内退出+`output_text()` 四关键串(浮窗运行态贯穿整批的可观测提交面) | 新增:调参测速批量模拟(逐帧步进与统计读数) | -反向核对:diff 中行为改动共零处;其余为非行为改动——V1 结构改形(零漂移判据=对拍五面全等+wd8/probe 零改动保持绿)、V2 测试面新增、V3 回归锚面行(非行为改动)、manifest 增行(测试基建)。孤儿断言:对拍五断言挂 V1/V2 两行,wd8/probe 既有断言挂 V3 行,无孤儿。 +反向核对:diff 中行为改动共一处(V3 行);其余为非行为改动——V1 结构改形(零漂移判据=对拍五面全等+wd8/probe 零改动转绿)、V2 测试面新增、manifest 增行(测试基建)。孤儿断言:对拍五断言挂 V1/V2 两行,无孤儿。 ## 13. 异常分级 - **L1 自救**:编译错、断言尾缀措辞、夹具记录数组名笔误——就地修复,重跑最窄单元确认。 - **L2 记录继续**:T0 套件因在册无关债先红于 wd8 断言(记录可达性事实,不借批顺手修);行号重锚漂移(就地改卡记录);产品码 process_frame 订阅面发现第三处(记录+评审注记——非 run 途中新增场景不红)。 -- **L3 停机写报告等用户**:①T0 两面任一红=现状与重裁前提失配(迭代 41 修复被回退或新潜伏红),停卡呈报;②等价性对拍失败——五面任一不等=测量语义漂移,设计前提崩塌;③机检预算互斥——改造后实测键超第 9 节预算或基线外新键=上游缺陷停卡退回写卡段(含禁为机检保留无效成员条款冲突);④S13 对账 await 键未消或冒新键;⑤判据绑不上可观测提交面(证明做不出=停卡呈报,禁自造注入破坏或回退取证)。 +- **L3 停机写报告等用户**:①T0 潜伏红证伪——两面实跑全绿=「每局置假系搬运卷入」前提崩塌,停卡退回写卡段重裁行为改动申报面;②等价性对拍失败——五面任一不等=测量语义漂移,设计前提崩塌;③机检预算互斥——改造后实测键超第 9 节预算或基线外新键=上游缺陷停卡退回写卡段(含禁为机检保留无效成员条款冲突);④S13 对账 await 键未消或冒新键;⑤判据绑不上可观测提交面(证明做不出=停卡呈报,禁自造注入破坏或回退取证)。 ## 14. 代拍板白名单(逐条列,超清单记实现报告「实现决定记录」) @@ -175,20 +175,20 @@ 2. 头注 `_batch_finished` 契约行措辞(信号语义与发射时点不变)。 3. `_finished` 成员零读者确认后删去(成员 11→10——键账只降不升,行为与断言面零变化)。 4. 夹具假面板/假驱动成员命名微调(记录语义不变;与 wd8 同形自建、不跨套件共享)。 -5. 收尾段语句相对次序——现行 run() 尾次序(close→报告→置假→_running)与设计草图次序(close→置假→_running→报告)对两个断言面均不可观测,两种皆可落码(实现期查 git 历史原序可进一步对齐);`_finished = true` 置位为新增首句(成员写,不可观测)。 +5. `_finish_batch` 收尾段四语句相对次序按第 11 节 T1 草图次序落码(对两个断言面不可观测——实现期查 git 历史原序可进一步对齐)。 6. 排版机械改写(空行、preload 顺序、注释换行)。 ## 15. 待裁点 -零待裁点,开工门=U0。(裁决记录:2026-10-29 用户裁四点——①信号步进状态机形态采纳〔SceneTreeTimer 链/Timer 节点/递归协程/线程四形态淘汰依据见设计文档 §3〕;②收尾置假还原作为已申报行为改动随批实施【已作废——前提被迭代 41 测试债批 215da315 消化,v1.1 剥离】;③双实现对拍入批;④脚本错误中断路径差异接受并文档化。2026-10-30 停卡重裁(用户裁,按停卡呈报建议案逐条落卡):剥离行为改动申报 V3、T0 义务改现状复核、夹具申报差异两处减一处;重裁沿用 2026-10-29 grilling 复核门结论(reports/复核-grilling-零豁免五卡-2026-10-29.md,修正全落),重裁差异面未新增设计裁决。) +零待裁点,开工门=U0。(裁决记录:2026-10-29 用户裁四点——①信号步进状态机形态采纳〔SceneTreeTimer 链/Timer 节点/递归协程/线程四形态淘汰依据见设计文档 §3〕;②收尾置假还原作为已申报行为改动随批实施;③双实现对拍入批;④脚本错误中断路径差异接受并文档化。) ## 16. 测试削弱申报 -无申报项——wd8 三断言与 ui_tune_probe ⑤ 零改动零削弱(保持绿);对拍五断言为新增非改判;夹具与产品码新形态的申报差异仅一处(夹具暴露 stats 供对拍读取)系新增件自身设计、设计文档 §5 在案,非既有断言削弱。禁删断言、禁放宽阈值、禁缩小扫描面、禁把真实入口换成下层直调;未申报的削弱由独立验收审计判红。 +无申报项——wd8 三断言与 ui_tune_probe ⑤ 零改动零削弱;对拍五断言为新增非改判;夹具与产品码的两处申报差异(收尾还原语义+stats 暴露成员)系新增件自身设计、设计文档 §5 在案,非既有断言削弱。禁删断言、禁放宽阈值、禁缩小扫描面、禁把真实入口换成下层直调;未申报的削弱由独立验收审计判红。 ## 17. 独立验收审计 -合并前由未参与实现的会话或子代理执行;报告落 `reports/iter44-验收审计-2026-10-30.md`,未过不得合并。审计至少做五件事:①逐行核对第 12 节对照表(测试存在、真的执行、尾缀可 grep、真实入口未绕过改动点);②抽查判据绑定面——帧索引序列是否真从 process_frame 发射点记录、对拍拓扑两形态是否同构可比、V3 行判据是否钉在运行态提交面(保持绿证据,非终态兜底);③核对现状复核证据在案(T0 两面实跑绿读数+停卡呈报三重证据引注〔215da315 提交信息〕——前提消化链条完整,缺 T0 复核读数=审计不过);④核对行为改动零处——diff 中任何行为改动(含收尾四语句语义漂移)=审计不过,收尾迁移的等价性由对拍 V2 运行态序列+V5 报告逐字承载;⑤检查测试削弱(对照第 16 节零申报)与内部字段直查受控例外(本批零直查——新形态 stats 经报告公开面观测)。 +合并前由未参与实现的会话或子代理执行;报告落 `reports/iter-验收审计-<日期>.md`,未过不得合并。审计至少做五件事:①逐行核对第 12 节对照表(测试存在、真的执行、尾缀可 grep、真实入口未绕过改动点);②抽查判据绑定面——帧索引序列是否真从 process_frame 发射点记录、对拍拓扑两形态是否同构可比、V3 行判据是否钉在运行态提交面而非终态兜底;③核对 T0 定谳证据在案(改动前 wd8/probe 红读数——潜伏红链条完整,缺 T0 读数=审计不过);④核对行为改动唯一性——diff 中除 V3 外无第二处行为改动;⑤检查测试削弱(对照第 16 节零申报)与内部字段直查受控例外(本批零直查——新形态 stats 经报告公开面观测)。 ## 18. MC/数值与 RNG 零触碰声明 diff --git a/tools/render_gate.sh b/tools/render_gate.sh index d6443f25..b39e209a 100644 --- a/tools/render_gate.sh +++ b/tools/render_gate.sh @@ -18,10 +18,6 @@ # ④ 基准档 1280 单档(Q4)。1920 档注记:16:9 全屏下 stretch=canvas_items/aspect=keep 等比 # 1.35×+左右黑边各 96px,构图与基准档一致(分辨率案甲)——分辨率维度不重复建闸。 # (1920 布局机检面现无承载,待补。) -# ⑤ 脚本错误扫描(三硬闸之二):shot/fx/board 三段 Godot 日志逐段过 tools/error_scan.py, -# 命中 SCRIPT ERROR/Parse Error 即红(标记与 run_smoke.py 同源);python 工具自身输出不扫。 -# 此类错误不改退出码、不计失败数,且会中断所在函数的剩余断言——只能由外部扫日志。 -# 新增日志段必须同步加扫描调用,漏加=该段静默错误漏网。 # 过程产物纪律(Q2):一切截图 PNG 落 mktemp 临时区,闸毕即弃——任何过程 PNG 禁入 git; # 例外=game/tests/golden/ 基线(测试资产入库,.gitignore 已核不挡)。 # 窗口化运行会短暂弹 Godot 窗口(本机可开窗口径);禁改 --headless 跑本闸 @@ -45,15 +41,6 @@ kill_orphans() { local p; for p in $(orphans); do taskkill //F //PID "$p" >/dev/null 2>&1; done } -scan_log() { # scan_log <段名> <日志>——脚本错误扫描;日志不可读(error_scan exit 2)同判红 - local name="$1" log="$2" - if ! python "$REPO/tools/error_scan.py" "$log" >"$TMP/err_scan.txt" 2>&1; then - echo "[render_gate] FAIL — $name 脚本错误扫描未通过(命中或日志不可读):" - sed -n '1,6p' "$TMP/err_scan.txt" | sed 's/^/ /' - _rc=1 - fi -} - # 冷检出刷导入(.godot 不入 git,fresh worktree 首跑一次性付) if [ ! -f "$GAME/.godot/uid_cache.bin" ]; then echo "[render_gate] 冷检出:editor --quit 刷导入(一次性,可到 1-2 分钟)" @@ -79,7 +66,6 @@ if [ "$_rc" -eq 0 ]; then _rc="${PIPESTATUS[0]}" [ "$_rc" -ne 0 ] && echo "[render_gate] FAIL — shot_checker A-D 断言存在红(S1 停机点:首跑存量 FAIL 全量呈报登记待办清单)" fi -scan_log "shot_probe" "$TMP/shot.log" echo "======== fx_shot_probe 三时刻取证(Q7) ========" FX="$TMP/fxshots" @@ -91,7 +77,6 @@ if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then elif [ "$st" -ne 0 ]; then echo "[render_gate] FAIL — fx_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1 fi -scan_log "fx_shot_probe" "$TMP/fx.log" fx_evidence() { # 取证双检:模式文件存在且非零字节(零字节/缺失任一即 FAIL——Q7) local pat="$1" min="$2" n=0 f for f in "$FX"/$pat; do @@ -116,7 +101,6 @@ if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then elif [ "$st" -ne 0 ]; then echo "[render_gate] FAIL — board_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1 fi -scan_log "board_shot_probe" "$TMP/bd.log" for b in board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png board_meta.json; do if [ ! -s "$BD/$b" ]; then echo "[render_gate] FAIL — 取证缺失/零字节: $b"; _rc=1; fi done diff --git a/tools/run_all_gates.sh b/tools/run_all_gates.sh index 86c5dbfc..05885698 100644 --- a/tools/run_all_gates.sh +++ b/tools/run_all_gates.sh @@ -11,11 +11,11 @@ # 回执即作废须重跑;跑闸时若代码面还带着未提交改动,回执会标 dirty_game,合回闸拒绝采信。 # # 用法:tools/run_all_gates.sh [--code|--docs|--quick] -# --code 只跑代码域闸(00 冒烟测试/01 系统测试/02 单元与集成测试/03 场景探针/ -# 05 渲染闸/09 静态扫描+10 熵增闸) +# --code 只跑代码域闸(00 冒烟测试/01 系统测试/02 单元与集成测试/03 探针 --click/ +# 04 探针 --full/05 渲染闸/09 静态扫描+10 熵增闸) # ——本批改动含 game/ 下非 .md 的行为载体(代码/测试脚本/探针/场景/数据表/素材)时用。 # 关联档选闸(2026-10-29 用户裁,推广自 2026-10-28 影响面选闸):00 冒烟/01 系统测试/ -# 09 静态扫描三件保险恒跑不降;03 场景探针与 05 渲染闸维持影响面口径(本批 game/ 改动 +# 09 静态扫描三件保险恒跑不降;03/04 探针与 05 渲染闸维持影响面口径(本批 game/ 改动 # 全在测量面 game/tests、game/sim 时记 SKIP);02 单元与集成改「选套件跑」——套件集= # 需求卡片申报 ∪ diff 推导并集(算法与套件族映射表见脚本中部),横切面(公共件/数据表/ # autoload/数值·存档·RNG·结算触面)回落全套件;回执 scope 字段记 full|related。 @@ -218,7 +218,7 @@ suite_terms_for() { # suite_terms_for <仓库相对路径> → stdout: 套件 # ── 关联档选闸(2026-10-29 用户裁,推广自 2026-10-28 影响面选闸)── # 三件保险恒跑不降:00 冒烟/01 系统测试/09 静态扫描(10 熵增闸照旧两域都跑)。 -# 03 场景探针与 05 渲染闸维持影响面口径:本批 diff 相对 master 分叉点的 game/ 改动若全部落在 +# 03/04 探针与 05 渲染闸维持影响面口径:本批 diff 相对 master 分叉点的 game/ 改动若全部落在 # 测量面(game/tests/**、game/sim/**——测试与模拟驱动件,不触场景树与渲染输出)则记 SKIP。 # 02 单元与集成改「选套件跑」:套件集=申报 ∪ 推导 取并集—— # 申报:在册需求卡片(本批 diff 触及的 reports/需求卡片*.md ∪ roadmap「进行中」迭代行登记的 @@ -241,7 +241,7 @@ if in_domain code; then _behavior="$(echo "$_gfiles" | grep -vE '^(game/tests/|game/sim/)' || true)" if [ -n "$_gfiles" ] && [ -z "$_behavior" ]; then NONBEHAVIOR=1 - echo "[gates] 影响面选闸:game/ 改动全在测量面(tests/sim)——03 场景探针/05 渲染闸 → SKIP(00/01/09 三件保险恒跑)" + echo "[gates] 影响面选闸:game/ 改动全在测量面(tests/sim)——03/04 探针/05 渲染闸 → SKIP(00/01/09 三件保险恒跑)" fi # ── 02 选套件推导(只对行为面代码文件做——测量面文件不进推导,其验证就是 02/09 自身)── _codefiles="$(echo "$_gfiles" | grep -viE '\.md$' | grep -vE '^(game/tests/|game/sim/)' || true)" @@ -412,12 +412,15 @@ gate "00 冒烟测试" "smoke_boot.log" "$PY" "$REPO/tools/run_smoke.py" gate "01 系统测试" "system_test.log" "$PY" "$REPO/tools/run_system_test.py" # ── 2. 单元与集成测试 ── gate_unit -# ── 3. 场景探针(走查+断言十件一次跑齐;iter27 漏跑断言段事故后固化为无条件整闸)── +# ── 3. 场景探针 · 行为走查段 ── +# ── 4. 场景探针 · 断言段(iter27 漏跑的就是这一段)── if [ $NONBEHAVIOR -eq 1 ]; then - NAMES+=("03 场景探针"); EXITS+=("SKIP"); LOGS+=("") - echo ""; echo "[gates] 03 场景探针 → SKIP(影响面选闸:测量面改动不触场景树与探针断言面)" + NAMES+=("03 探针 --click"); EXITS+=("SKIP"); LOGS+=("") + NAMES+=("04 探针 --full"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 03/04 探针 → SKIP(影响面选闸:测量面改动不触场景树与探针断言面)" else - gate "03 场景探针" "probe.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --click --full + gate "03 探针 --click" "probe_click.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --click + gate "04 探针 --full" "probe_full.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --full fi # ── 5. 渲染面(窗口化,依赖 PIL)── if [ $QUICK -eq 1 ] || [ $NONBEHAVIOR -eq 1 ]; then diff --git a/待办清单.md b/待办清单.md index f5df4bf2..0b1cb2f6 100644 --- a/待办清单.md +++ b/待办清单.md @@ -92,9 +92,9 @@ **状态**:待执行 -#### 闸基线 02/05/09 三键换代——存量红指纹已消除或现行不复现(2026-10-29 登记、2026-10-30 并入 05,来源=批次移交) +#### 闸基线 02/09 两键换代——存量红指纹已随迭代 41 测试债批归绿(2026-10-29 登记,来源=批次移交) -**实况**:证据=迭代 41 测试债批代码域闸实测(worktree 分支 `testdebt-20261029`,回执 commit afd1a41f、域=code、scope=full、零新增红),02 单元与集成与 09 静态扫描两闸判「基线红 → 现绿(改善)」;05 渲染闸在 2026-10-30 脚本错误扫描批主区真跑 `bash tools/render_gate.sh` exit=0 全绿(shot/fx/board 三段扫描零命中、残留 Godot=0、golden changed 0.00%),其红键在现行主区不复现。基线 `game/tests/.gates/baseline.json` 里这三键仍记着历史红指纹(02 fp f9865f1a5a44=9 套件失败;05 fp de89589e49ed;09 fp 1a9ad773489f=S9 24 处+S10 4 处+S11 1 处),留着不动的后果=日后同类红回归会被判「存量红·对基线零新增」静默放行。待做=三键按归绿实测重生成(写 exit=0/绿指纹),改的是 `game/` 下非 .md 文件=代码面,须随下一个代码批在 worktree 内一并落、禁主区直改;05 须在素材齐盘预检后跑出真值(冷检出空盘态下的渲染红不作数)。人工确认凭据=上述回执、reports/迭代41测试债批-实现报告-2026-10-29.md 第三节与本批提交说明。本条目无设计段(机械重锚),不走写卡与编排。 +**实况**:证据=迭代 41 测试债批代码域闸实测(worktree 分支 `testdebt-20261029`,回执 commit afd1a41f、域=code、scope=full、零新增红),02 单元与集成与 09 静态扫描两闸判「基线红 → 现绿(改善)」;基线 `game/tests/.gates/baseline.json` 里这两键仍记着已消除的存量红指纹(02 fp f9865f1a5a44=9 套件失败;09 fp 1a9ad773489f=S9 24 处+S10 4 处+S11 1 处),留着不动的后果=日后同类红回归会被判「存量红·对基线零新增」静默放行。待做=两键按归绿实测重生成(写 exit=0/绿指纹),改的是 `game/` 下非 .md 文件=代码面,须随下一个代码批在 worktree 内一并落、禁主区直改;人工确认凭据=上述回执与 reports/迭代41测试债批-实现报告-2026-10-29.md 第三节。本条目无设计段(机械重锚),不走写卡与编排。 **改动面**:游戏代码 @@ -110,12 +110,12 @@ #### 零豁免五键清偿批——S13 余量五键真修四卡(2026-10-29 登记,来源=会话提及) -**实况**:用户 2026-10-29 裁「不允许任何豁免」——S13 三条豁免注销(architecture.md v1.25 落文),五键真实修复至阈值内:main.gd 类变量 49→15(组合根注册面:ui_registry 一门面+三域容器,约 867 处探针/系统测试引用改址 `main.ui.<域>.<句柄>`,7 别名/转发成员消灭)/battle_orchestrator 类变量 38→15(RoundCycle+BattleFacts 两件,叠加迭代 42 轴丙 BoardInstaller;encounter 归 BattleFacts、attune 改判状态两成员随件)/board_cell_unit 类变量 30→10+standee_body 类变量 19→10(各拆两独立文件,gdstyle inner-class 展平计数口径)/tune_sim 循环内 await 消除(process_frame 信号步进状态机;**零行为改动**——v1.0 申报的收尾置假还原已被迭代 41 测试债批 215da315 先行完成并申报,2026-10-30 实现段按卡片预设停机点停卡、三重证据定谳前提已消化,按停卡呈报建议案重裁剥离行为改动申报:iter44 worktree 分支 reports/iter44-实现报告-2026-10-30.md §5)。设计闭合凭据=architecture.md v1.25+五份设计文档(reports/ 下五件 设计-组合根注册面-2026-10-29.md/设计-战斗编排类变量归约-2026-10-29.md/设计-单格绘制状态件-2026-10-29.md/设计-站桩状态件-2026-10-29.md/设计-测速循环去await-2026-10-29.md)。四卡已成文(均零待裁点开工门 U0;测速循环批卡 2026-10-30 停卡重裁 v1.1——前提消化后剥离行为改动申报、T0 改现状复核、夹具申报差异两处减一处,仍零待裁点): +**实况**:用户 2026-10-29 裁「不允许任何豁免」——S13 三条豁免注销(architecture.md v1.25 落文),五键真实修复至阈值内:main.gd 类变量 49→15(组合根注册面:ui_registry 一门面+三域容器,约 867 处探针/系统测试引用改址 `main.ui.<域>.<句柄>`,7 别名/转发成员消灭)/battle_orchestrator 类变量 38→15(RoundCycle+BattleFacts 两件,叠加迭代 42 轴丙 BoardInstaller;encounter 归 BattleFacts、attune 改判状态两成员随件)/board_cell_unit 类变量 30→10+standee_body 类变量 19→10(各拆两独立文件,gdstyle inner-class 展平计数口径)/tune_sim 循环内 await 消除(process_frame 信号步进状态机;含**申报行为改动**一处:运行态收尾由每局置假还原为整批收尾单次置假——消除与 wd8/ui_tune_probe 断言面的静态矛盾,T0 实证定谳)。设计闭合凭据=architecture.md v1.25+五份设计文档(reports/ 下五件 设计-组合根注册面-2026-10-29.md/设计-战斗编排类变量归约-2026-10-29.md/设计-单格绘制状态件-2026-10-29.md/设计-站桩状态件-2026-10-29.md/设计-测速循环去await-2026-10-29.md)。四卡已成文(均零待裁点开工门 U0): 关联文档清单: - 组合根注册面批 reports/需求卡片-组合根注册面批-2026-10-29.md - 呈现层状态件批 reports/需求卡片-呈现层状态件批-2026-10-29.md(单格+站桩两设计并一卡) -- 测速循环批 reports/需求卡片-测速循环批-2026-10-29.md(v1.1,2026-10-30 停卡重裁) +- 测速循环批 reports/需求卡片-测速循环批-2026-10-29.md - 战斗编排归约批 reports/需求卡片-战斗编排归约批-2026-10-29.md(接收方=在册条目「战斗域结构深拆批」宿主瘦身子项,不在本条目记账) 编队待迭代 42 合回(battle_orchestrator/board_view 触面碰撞约束),具体迭代归属归编排段;编入前置=各卡过 grilling 复核门。gdstyle inner-class 计数截断缺陷已在册(architecture v1.25)——基线登记值欠估,各批按真实口径施工;各批全部合回后一次人工 --refresh --force 收账(一并清迭代 42 归绿 6 键离账——批末实测已归绿而基线仍记 16 键:board_view 四键+level_runner `_enter_node`+battle_orchestrator 文件长;各留债键按重锚时实测重登记,冻结禁恶化口径不变)。2026-10-29 编排(用户裁编法):卡 A 组合根注册面批+卡 B 战斗编排归约批+卡 D 测速循环批编入**迭代 44**(≈7.875 个工作日,中值合计);卡 C 呈现层状态件批编入**迭代 45**(与补件批同批,卡 C 先于其 board_hover 半边施工)。grilling 复核门已过(reports/复核-grilling-零豁免五卡-2026-10-29.md,五卡有条件成立、修正全落,提交 9855bf0a)。 @@ -327,11 +327,9 @@ **实况**:扩板已落地(四档板型 10/12/13/14 在跑),2026-10-26 文档面已按单点维护归一:①格边长口径的唯一出处收敛到 [战斗屏规格](design/gdd/ux/battle.md)「棋盘格尺寸公式」——公式含格间距与垂直预留,可读下限 30px,1280 档算例 43/36/33/30 标注为随布局变化的非契约值;②[美术圣经](design/art/art-bible.md) §8.1 改为指路 battle.md、只锁资产侧两条比例,§8.2 甲类整数倍纪律已剔除砖面 tile(瓦片随格边长整格铺满,允许非整数倍);③[HUD框架](design/gdd/systems/表现与支撑/HUD框架.md) 只留装板可行性不等式,删去 36/45 定值档与旧算例;④demo 原型 44px 那一路随 `src/` 退役消失。剩余为代码面两处与口径不同步:`game/probe/layout_probe.gd:15` 的格子可读门槛 `MIN_CELL := 36.0` 仍取 12×12 的旧锚点值,比 battle.md 的可读下限 30px 严——现探针只进 10×10 的 L1 故仍全绿,一旦覆盖 13×13(33px)与 14×14(30px)即会误报红;`game/tests/unit/core/board_gen_reward_test.gd:53` 的格边长冒烟只跑 L1/L2,13×13 与 14×14 无覆盖。断言改动后按 `game/tests/README.md`「能力清单」核对受影响行(验证集清单已退役——test-conventions §8.3)。 -- 需求卡片:[需求卡片-棋盘格边长口径收口批-2026-10-30](reports/需求卡片-棋盘格边长口径收口批-2026-10-30.md)(2026-10-30 已写卡,零待裁点——算式单点提取经 `BoardView._cell_for`/探针门槛 36→30 对齐 battle.md 可读下限/冒烟扩 13×13 与 14×14;探针不扩到 L3,最大板可读面改由实板面积推定断言承担) - **改动面**:游戏代码 -**状态**:已写卡 +**状态**:待写卡 #### 立绘清晰度回退评估——是否改用高清图(2026-10-13 登记,来源=playnote) @@ -366,6 +364,15 @@ **状态**:待美术设计(备选) +#### 在途草稿整理批——3349 个失管草稿(2026-10-06 登记,来源=审计发现) + +**实况**:审计 P2-1——`artwork/ui-reuse-2026-09-23/` 独占 3043 个草稿(91%),vN 命名率 0%,冗余 41 文件,残留非素材文件(.url ×8 / .ogg ×6 / .ai ×2 / .swf ×1);根因=脚本链覆盖「生成→验证」但**缺「草稿整理」段**。整理涉及**批量文件移动/删除**,按用户惯例须先 dry-run 呈报清单、经确认再分批执行。执行窗口随实现批;开工=用户点名。 +- 相关文档:[art-bible](design/art/art-bible.md) §8.3 归档纪律(原则已写入)· 资产管线批(工具+文件操作) + +**改动面**:美术素材+工具代码 + +**状态**:待写卡 + #### 战斗区背景图替换——B 区舞台插画 + 临时光柱退役(2026-10-07 登记,来源=会话提及) **实况**:B 区舞台现行=程序化平色底(`UiKit.STAGE_BG`,`battle_stage_view.gd:47`)+ **顶光柱氛围层**(`:55-63` 四柱,x 基准 110/900/1055/1210@1280,60 段叠层矩形自顶向下渐宽窄渐隐〔顶 α26→0〕,层序=bg < 光柱 < 世界)——二者均系 §6.1 方向稿的**临时占位**,用户口径「光柱只是临时方案,换正式战斗背景后效果更好」。正式件已立资产契约:`stage/stage_bg.png`(源 1280×270,B 区全高含明细带底;三关换挡走 §4.3 `wall_ambient` tint 槽,不按板出三张),属审计 P1-2 查得的「B 区舞台背景插画 0 张」缺口。**光柱在背景到位后顺带退役或吸收进背景绘制**(不再作为独立程序层)。**与立绘探顶解耦**:大体档「允许向上溢出盒顶 N px」的判定(2026-10-07 用户裁甲案)**不依赖光柱是否存在**,换背景零返工。 @@ -377,29 +384,6 @@ **状态**:待美术设计 -#### 棋盘封印印记图形换代——现件仍是扫雷旗形,改法阵或锁链待设计(2026-10-30 登记,来源=会话提及) - -**实况**:用户口径=棋盘封印图标现在还是按旧扫雷的做法画成一个旗子,希望换成法阵那样的封印、或锁链;形未定,先做设计讨论再出图。现状证据: - -- 现件 `game/assets/art/board/seal_mark.png`(32×32 纯黑剪影)=经典扫雷旗形(旗面 + 旗杆)。2026-09-28 那次只定了色(旗朱→封印紫→封印黑 `#000000`),图形一直没动。 -- 消费面两处,换形同时受影响:棋盘侧 `game/presentation/views/tile_palette.gd:19`(`TILE_SEAL_MARK` 常量)→ `board_view.gd:616` 未开格封印槽绘制,盘上缺图时回落单字「封」(`board_view.gd:654-658`);战末弹层的「金蜡封徽记」复用同一张图(金底 52×52 面板 `battle_end_overlay.gd:65-69`,叠 32px 印记 `:84`)——蜡封徽记要不要跟着换是独立一裁。 -- 文档侧口径仍以旗为形:art-bible §4.1 封印黑条内含「旗杆米白保可读」这一旗形前提、§3.2 封印印记条只锁色与格内栈位;art-assets-checklist §4 封印特效行写「印记旗面走封印黑」「印记常显形态可与 §3.3 旗面像素化合并出」;ux/battle.md:128 呈现术语注记与 :270 悬停三态光标「封印(印记形)」。 -- 术语层已闭合,本条只欠呈现层:「旗」概念 2026-10-18 用户裁全库退役(动作=封印、印记=封印印记、板面单字=「封」),图形停在退役前的形态。 - -关联文档清单: -- 相关文档 [美术圣经](design/art/art-bible.md) v3.23——§3.2 封印印记条(:167)· §4.1 封印黑条(:202)· §1.2 格内优先级栈 · §1.4 格子样张 -- 相关文档 [美术资源依赖清单](design/art/art-assets-checklist.md)——§3.3 雷/封印印记/数字格面像素化(:118)· §4 封印特效行(:162) -- 相关文档 [美术素材状态清单](design/art/art-status-checklist.md)——P2「雷/封印印记/数字砖面全套」◐ · P3「封印特效」⬜ -- 相关文档 [战斗屏 UX](design/gdd/ux/battle.md)(Last Updated 2026-10-29)——:128 呈现术语注记 · :270 悬停预承诺三态光标 · :261 未开格封印交互行 -- 相关文档 [术语表](design/gdd/术语表.md) v1.25——「封印」条(:15)· 用语规范禁用字面条(:184) -- 相关目检项 [体验验证清单](体验验证清单.md) 第 4 条「封印印记贴图三关加载」(:317,现判据只验贴图是否加载、不验形状) - -**缺口**:还差一次封印印记图形的设计讨论(形未定,法阵/锁链两案并列,禁代裁),讨论须一并裁五点:①形=法阵(圆形阵纹)还是锁链(束缚感),或第三种方案;与危险格巢穴纹、奖励徽章的剪影语言(art-bible §3.1 直线几何体)互辨是否成立。②色=现封印黑 `#000000` 是 §4.1 不变槽成员,法阵若带发光或色相即与该条口径冲突,须重登记不变槽与对比度机检判据(现判据=未开格墙顶对纯黑 ≥4.4:1)。③尺寸与遮挡=32×32 内保持可读,格内优先级栈「数字 > 小人 > 封印印记」下法阵铺满整格是否侵入数字字形区(§3.2 验收句禁止)。④连带件=战末金蜡封徽记与悬停「封印(印记形)」光标是否同步换,还是各留一件。⑤与「封印特效=施印瞬间特效 + 印记常显形态」两件的关系=合并出一件还是分两件。裁决写入 art-bible §3.2/§4.1 与两份美术清单条款后,才可出样张与终稿(终稿文件名沿用代码消费名 `seal_mark.png`,除非裁决换名)。 - -**改动面**:文档+美术素材 - -**状态**:待美术设计 - ### 教学引导 > **节头注记(2026-10-04 用户拍板)**:教学引导定稿批整体改排 **Beta 阶段**——当前 Alpha 版不做教学相关工作,本节五条全部随之改排(2026-09-19 的 Alpha 排期拍板被本拍板取代)。同日五问讨论已形成初裁与设计提案,提案已归档 `archive/` 目录(磁盘留存、不入 git,Beta 批立项底稿,引用以其「归档状态与初裁记录」节为准)。初裁摘要:①教学形态=丙1 混合(首次冒险编排式首遇提示〔挂 first_script 锚、不锁操作〕+其余全程情景触发);②「因果教学顺序」问题**注销**——只教扫雷操作,结算读法不设教学(用户裁:结算只是表达、展现、演出,无策略内容;展示层每行带来源标注已自带归因;教学引导.md 旧前提〔设计目的句/P0 候选归因提示/验收第 2 条〕随 Beta 批文档定稿清除);③图例面板按循环段落分组、无结算段;④不做「再教一次」——改走独立「帮助」手册面(全部内容放一个文档);⑤教学文案内容权归设置与本地化(两文档负责表对齐随 Beta 批)。**未裁三疑点**(Beta 立项时先裁):增益叠层节奏条目去留/帮助入口位置(设置面为推荐)/图例面板与帮助手册的内容分工。 @@ -469,6 +453,17 @@ ### 文档与结构 +#### qa/ 实现报告收尾·孤儿遗留七项余二项(2026-10-14 登记,来源=批次移交) + +**实况**:qa 收尾批核查 iter19 系列出口报告遗留移交项时发现**不在待办清单在册**的七项(acceptance-package §三.2/card0 §5.3/followup §七.3/uibackfill §6.2-4/verify §6.1/3/7)。已结案的(tree_rules 受理闭环、X/Y 双源、S1 七项违例修复、提灯目检项、tests.yml 休眠等)已在各报告标注结案证据,不入本条。2026-10-24 处置:第②③④项随迭代 36 卡C 落地销号(营地 STONE 深档六处、迷雾三关派生行、开场后缀条件加算区),第⑥项按用户 2026-10-24 裁整条注销(TG_* 装饰带非文字,不另立探针)。余二项均为挂号备查/顺路件,无排期承诺(第①项 design/diagrams 越界 2026-10-28 实查核销——目录磁盘与 git 均已不存在,越界实体消失,白名单甲/乙两案失去对象): + +5. **字面量墨收测前置**(iter19 卡②遗留):bcard 属性描述/类型脚注/✓章/primary_button 字四处内联 Color8 无常量形态,收测需先在 ui_kit 立常量(呈现批顺路件,改 ui_kit 越当批面)。落点=呈现优化批(ui_kit 常量面) +7. **商店房截图纳屏**(iter19 卡①遗留):商店房截图纳屏留后续呈现批(考证结论在 shot_probe 头注)。本项原并列的「`数据配置.md:223` 历史行死指」半句已于 2026-10-21 实查核销——禁写变更记录规约生效后该文件的版本史行整段已不存在(现全文 212 行,文内零版本史行),被指对象随之消亡。落点=呈现批 + +**改动面**:游戏代码+工具代码 + +**状态**:待写卡(备选) + #### 主角棋盘小人像素管线重产——原「美术资产链路审计两项 P0」之 P0②(2026-10-09 登记,来源=审计发现) **实况**:主角棋盘小人未走像素管线(插值糊图),重产事项未在任何登记处在册;审计报告 HTML 件从未进版本库、现已删除,现状以本条正文为准。2026-10-13 用户令随「UI 美化三层诊断」登记:本条属第三层「观感提升」,Alpha 阶段需完成。原 P0①(文档面:两处引用已丢失的冻结样张路径)2026-10-28 按用户裁乙销号——art-bible §1.4 冻结记录与 art-status-checklist P0 样张行删路径、留「产物原件已丢失」,机检报告凭据(git 提交 7a847c7)保留。 @@ -479,6 +474,29 @@ **状态**:待美术设计 +#### 技术债观察登记——长函数余量与 lambda 连接防回归(2026-10-15 登记,来源=批次移交) + +**实况**:证据=迭代 27 需求卡片两项「登记待办清单观察」义务批内漏登、卡片核对时补登——①**长函数 17 个**中非结算段的装配/校验函数(`level_map_screen.gd` 选路装配 155 行/`camp_overlay_view.gd` 152 行/`debug_overlay.gd` 114 行/`treasure_item_table.gd` schema 校验群等;`main._build_ui` 已随场景拆分自然解体、`_settle`/`_resolve_hero_attack` 已随波二拆段=17 中 3 件已消解,余 14 件);②**lambda 连接 76 处**(presentation 6/smoke 62/tests 8——实读无泄漏,登记防回归)。长函数拆分无独立需求驱动不动;lambda 连接数若有审计工具化需求归类型闸后续。 + +**改动面**:游戏代码 + +**状态**:待写卡(备选) + +#### 测试域遗留(2026-10-16 登记,来源=批次移交) + +**实况**:证据=迭代 29 批内未收余量(交付报告与验收审计申报,报告已按吸收即死退役、git 历史可考)。迭代 29 把验收语义从「跑一遍没崩=通过」扭到「三层完备+判定可靠」,三硬闸(退出码/脚本错误扫描/断言数下限)已落地,L2/L3 从零建起承载。本批未收的余量集中登记在此,均不阻塞、不排期: + +| # | 事项 | 落点与要点 | +|---|---|---| +| 1 | **余下 3 个 core 模块仍零指名单测**:`board_data`/`reward_economy`/`skill_lottery` | 测试基建批。本批补了 minesweeper_rules/board_generator/config_db_resolver/config_db_cross_validator 四件;余三件按同一手法补(判据=能否在 `--script` 模式不加载 autoload 验完) | +| 2 | **render_gate 的 SCRIPT ERROR 扫描仍缺失** | 测试基建批。判定主体是 golden 容差与取证文件存在性,脚本错误不改退出码时无扫描面;本批只把矩阵状态如实记为「缺失」,收它的写法待裁 | +| 3 | **E 级卫生件**(test_kit static var 串台风险、4 套件自建断言出口、157 处 untyped 容器、76 处 lambda) | 工程债批。本批只统一了**新写**断言的出口,存量未动;test_kit 的 static var 是「并发跑批」的前置障碍(并发分层方案的前提) | +| 4 | **行覆盖率度量缺失** | 基建评估项。Godot 无内建、第三方属新基建;现行补洞靠「零指名清单 + 人工判定」,是否引入度量待裁 | + +**改动面**:游戏代码 + +**状态**:待写卡(备选) + #### 营地交互重做·五张需求卡片(2026-10-18 登记,来源=会话提及) **实况**:2026-10-17~18 需求讨论澄清会话产出结论文档,用户令按工作内容维度拆卡、卡片不预占迭代号。五卡已写(顺序=执行顺序):①存档制数据层改造批 ②主菜单三入口化批 ③选档界面新建批 ④营地场景图新建批 ⑤Esc 浮窗与营地顶栏批(各卡估时与内容见卡片自身)。三道复核:grilling 子 Agent 对抗审查(五卡各一)→v1.1;用户裁定「事项按推荐的来」→v1.2;用户两次订正(**卡④留在批内不踢出**;前置归用户自立批,Agent 不代跑不代产——曾越权起草的点选反馈设计稿 v0.1 已撤回删除)→卡④ v2.2;**卡④ 2026-10-21 用户令退回美术设计段(v2.3,非有效卡,美术设计闭合后重立卡)**。①②③⑤ 零待裁点可编工程波次:②③ 硬耦合同波(「进入存档」钮落点=选档界面),① 是 ③ 前置;⑤ 随 ④ 代码波同行。Alpha 出口裁决(2026-10-21 用户裁):五卡整体列入 Alpha——08「三 · 界面」已开条目,Alpha 出口依赖五卡完工与卡④三个用户侧前置销号。**工程波次一已落地(2026-10-22)**:卡①②③ 随迭代 33 实现批合回 master(合并提交 ebf40947/5c2a7bcf/508f190b,总收口 6e8f72b2),三卡需求卡片与批次产出物已按「吸收即死」退役(设计口径已吸收进 存档序列化.md/生涯.md/camp.md/08-Alpha版本标准.md v1.13/art-bible.md v3.18,反哺落文凭据=文档批 git 提交 1864f157+9f7b3e8a);卡④(营地场景图)退回美术设计段待重立卡、卡⑤(Esc 浮窗)随卡④ 代码波同行,两卡未编排——编入哪一迭代由用户经迭代编排段发令。关联文档: From d1c1d3822a8319e4080d21ff3740b52f97219fe4 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 30 Oct 2026 07:13:23 +0800 Subject: [PATCH 2/3] =?UTF-8?q?iter44-=E5=8D=A12=EF=BC=9A=E5=8D=A1?= =?UTF-8?q?=E2=91=A1(=E6=88=98=E6=96=97=E7=BC=96=E6=8E=92=E5=BD=92?= =?UTF-8?q?=E7=BA=A6=E6=89=B9):=20RoundCycle+BattleFacts=E4=B8=A4=E6=96=B0?= =?UTF-8?q?=E4=BB=B6=C2=B7=E5=AE=BF=E4=B8=BB38=E2=86=9215=C2=B7=E9=94=AE?= =?UTF-8?q?=E8=B4=A615/15/8=C2=B7=E8=A1=8C=E4=B8=BA=E9=9B=B6=E5=8F=98?= =?UTF-8?q?=E5=8C=96=C2=B7=E5=90=AB=E6=B6=88=E8=B4=B9=E6=BC=8F=E7=BD=91?= =?UTF-8?q?=E8=A1=A5=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/battle_facts.gd | 48 +++ game/core/battle_facts.gd.uid | 1 + game/core/battle_orchestrator.gd | 281 ++++++------------ game/core/level_runner.gd | 2 +- game/core/settlement/combat_segment.gd | 4 +- game/core/settlement/grant_segment.gd | 42 +-- game/core/settlement/round_cycle.gd | 106 +++++++ game/core/settlement/round_cycle.gd.uid | 1 + game/tests/static_scan.sh | 4 +- .../battle_settlement/roll_multiplier_test.gd | 22 ++ .../round_flow_skill_pool_test.gd | 9 +- .../battle_settlement/seal_polarity_test.gd | 12 +- .../battle_settlement/trigger_cond_test.gd | 6 +- game/tests/unit/core/adventure_feats_test.gd | 2 +- game/tests/unit/core/feat_vouchers_test.gd | 22 +- .../unit/core/meta_progression_forge_test.gd | 8 +- game/tests/unit/core/settle_segments_test.gd | 15 +- game/tests/unit/core/treasure_effects_test.gd | 21 +- 18 files changed, 346 insertions(+), 260 deletions(-) create mode 100644 game/core/battle_facts.gd create mode 100644 game/core/battle_facts.gd.uid create mode 100644 game/core/settlement/round_cycle.gd create mode 100644 game/core/settlement/round_cycle.gd.uid diff --git a/game/core/battle_facts.gd b/game/core/battle_facts.gd new file mode 100644 index 00000000..5a4d8b7e --- /dev/null +++ b/game/core/battle_facts.gd @@ -0,0 +1,48 @@ +## 战斗事实件:战斗事实的唯一记录面——本战受理事实(encounter:打什么、带什么进来)、 +## 同调装配事实(attunement)、轮 facts 缓冲(reveals/seals/path)、封印判定账(去重两集+完美三账)、 +## 装板溯源(seq/provenance)、条件事实计数(refresh_used、feat_first_dig_used、attune_safe_seen)。 +## 零注入引用、零信号、零协作对象调用——纯数据+谓词+写口;缓冲写入点在宿主 A2 处理器, +## 身份快照捕获留宿主(fact 件不持协作引用,防它变成第二编排面)。 +## 完美账写口单点=三 mark 方法(契约 2:perfect_hit_false 保持纯净,豁免只累加 perfect_forgiven); +## 其余成员公有直读写(段类/快照/宿主经 oc._facts.X——fx_host._fx_bonus_delta 跨件直读写同款形态)。 +## reset_round 只清 reveals/seals/path 三项、不碰 refresh_used(_battle_over 后快照读到的 +## refresh_used 必须保持末轮值)。 +## 成员冻结:15/15 贴顶——实现期禁增员;扩容阀=先把封印判定账五成员二次拆出(SealLedger)再落新账。 +class_name BattleFacts +extends RefCounted + +var encounter: Dictionary[String, Variant] = {} # 本战受理事实(begin_battle assign 后全生命期只读) +var reveals: Array[Dictionary] = [] # 轮 facts 缓冲(cell_revealed 批) +var seals: Array[Dictionary] = [] # 封印判定批缓冲(seal_judged 批) +var path := "" # 轮末路径事实(round_end 载荷 path_tag) +var refresh_used := 0 # 本轮刷新计数(复位点=出口分发,语句随迁) +var board_seq := 0 # 板序号(本战斗第几张板——快照归因用) +var board_from_midround := false # 装板 provenance:轮内刷新换来的图=刷新图(清图奖励不发) +var seal_consumed := {} # 封印兑付资格账(危险格实例为键;install 重置) +var seal_miss_seen := {} # 封印未中行去重(每安全格实例一次) +var perfect_hit_true := 0 # 整图完美封印计数(install 重置) +var perfect_hit_false := 0 # 完美误标计数(纯净禁回退改写——契约 2) +var perfect_forgiven := 0 # 完美保险豁免累计(单向累加自有账) +var feat_first_dig_used := false # 条件事实计数(魔矢每战首次) +var attunement: Array[Variant] = [] # 本战装配集快照(enc.attunement 受理携带;受理后只读) +var attune_safe_seen := 0 # 积点制件安全开格计数(装配集自动兑付进度) + + +# 轮复位(原宿主 _reset_round_facts 逐字——三调用点:begin_battle 首板 install 后/出口分发/终局序列) +func reset_round() -> void: + reveals.clear() + seals.clear() + path = "" + + +# 完美封印三账写口(授予段直写升级为账本单点——契约 2 执行点) +func mark_perfect_hit() -> void: + perfect_hit_true += 1 + + +func mark_perfect_miss() -> void: + perfect_hit_false += 1 + + +func mark_perfect_forgiven() -> void: + perfect_forgiven += 1 diff --git a/game/core/battle_facts.gd.uid b/game/core/battle_facts.gd.uid new file mode 100644 index 00000000..2ed94de1 --- /dev/null +++ b/game/core/battle_facts.gd.uid @@ -0,0 +1 @@ +uid://bo0eolh0vmvgb diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 138fe7e3..94b29869 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -3,7 +3,7 @@ ## 效果引擎施加面归 BattleFxHost、奖励授予管线归 RewardGrant(两拆件,经组件出口 fx_host()/ ## reward_grant() 取件,禁链式穿透其内部);本类保留宿主职责面=状态机与生命周期、轮 facts 缓冲、 ## A2 三事件订阅、五段驱动、attune 族、行发射基建(_emit_row/_assert_row_source/_fatal)、呈现投影、刷新面。 -## 零 await;随机只经 rng 封闭流(encounter/behavior_roll/armor_roll/gp_drop)——同种子同输入逐位复现。 +## 零 await;随机只经 rng 封闭流(_facts.encounter/behavior_roll/armor_roll/gp_drop)——同种子同输入逐位复现。 ## 呈现层读取唯一出口 = presentation_snapshot()/diagnostic_snapshot(),禁链式穿透内部协作对象。 ## 伤害只向 DamageController 开单(何时开单归编排、怎么算归控制器);刷新唯一通道 player_request_refresh,禁 RAP 满自动刷。 class_name BattleOrchestrator @@ -34,6 +34,8 @@ const SOURCE_EXEMPT_TYPES := ["decay", "verdict"] # fx_grant_gp=专长·GP 入账(钱包面而非棋盘面,行无格子可指) const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal", "fx_grant_gp"] +var _facts: BattleFacts # 战斗事实件(battle_facts.gd——事实族 15 成员单点) +var _rounds: RoundCycle # 轮循环驱动件(settlement/round_cycle.gd——五段持有/段序/重入闸/摇号) var db: ConfigDb var rng: RngServiceCore # E1 封装收口:协作对象 enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/diagnostic_snapshot() 成文出口。 @@ -45,60 +47,43 @@ var action_economy: ActionEconomy var _enemy_system: UnitContainer var _status_effects: StatusContainer var _reward_economy: RewardEconomy -var treasure_inventory: TreasureItems = null: - set(v): - treasure_inventory = v - if _reward_grant != null: - _reward_grant.treasure_inventory = v # 库存注入转发(注入时序在宿主构造之后——外部赋值路径零改址) var _damage: DamageController # 伤害控制器(单据唯一兑付管线) var _fx_host: BattleFxHost # 效果引擎宿主件(fx 族持有与兑付——拆件定稿轴件一) var _reward_grant: RewardGrant # 奖励授予件(授予管线与秘宝使用——拆件定稿轴件二) -var encounter: Dictionary[String, Variant] = {} var round_index := 0 var battle_result := "" # "" / "victory" / "defeat" / "fatal_error" -var _facts_reveals: Array[Dictionary] = [] -var _facts_seals: Array[Dictionary] = [] -var _facts_path := "" -var _settling := false # 结算五段 Segment(段边界=审计五段,段序骨架冻结于 # SettlementTable.segment_order 语义:授予→增益/减益→战斗→衰减→判定。本类做段序编排+ # 行发射/开单原语单点——段体逻辑归 game/core/settlement/ 五类) -var _grant_seg = null # GrantSegment(①授予段——含封印判与 counter/grant 助手单点) -var _aura_seg = null # AuraSegment(②增益/减益段) -var _combat_seg = null # CombatSegment(③战斗段) -var _decay_seg = null # DecaySegment(④衰减段) -var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归本类) -var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_cap_per_round) -var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用) -var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励) -var _seal_consumed := {} # 兑付资格以危险格实例为键(整图;换图重置) -# 同调装配运行态(begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动) -var attunement: Array[Variant] = [] # 装备中同调件 item_id 集 -var _mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(触发条件事实面——本场累计/上轮转存/当轮累计三读数合一账) -# 转存时点=_roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle -var _attune_safe_seen := 0 # 积点制件 battle 域安全开格计数(装配集 ap_charge_safe 行扫描;满 charge_n 自动兑付当轮 AP+1,无玩家发起入口) +# 同调装配运行态(begin_battle 经 enc 携带 attunement 键注入 facts;off=空集全部效果零扰动) +# 转存时点=roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle var nest_preview_count := -1 # 巢穴预告读数口(board_installed 命中落值;-1=未装配/未发射;呈现桥消费) # 特性运行态(relic 持有集经 enc.feats 注入:效果行为=数据行 triggers 经 # 效果引擎分发;魔矢每战首次计数 battle 域。空集=零扰动) -var _feat_first_dig_used := false -# 加算日记读面(begin_battle 经 enc["journal"] 派生——encounter.assign 后全生命期可读; +# 主角攻击基值派生读(enc 携带 hero_atk 键受理后只读——E4 收口语义不变:表值运行期不可变, +# 派生值恒等;「读面即引用、无第二账」同款先例) +func _hero_atk() -> int: + return int(_facts.encounter.get("hero_atk", int(db.combat.hero.atk))) + + +# 加算日记读面(begin_battle 经 enc["journal"] 派生——encounter 受理 assign 后全生命期可读; # null=调试/单场直构面,账面消费跳过。领域拆分批成员删除后的引用承接,读面即引用、无第二账) func _journal() -> GainJournal: - return encounter.get("journal", null) if encounter.get("journal", null) is GainJournal else null + return _facts.encounter.get("journal", null) if _facts.encounter.get("journal", null) is GainJournal else null # 同调配装件读面(begin_battle 经 enc["loadout"] 派生——充能单源化的 reset_cycle/consume_charge # 消费面;null=直构零装配,充能面跳过) func _attune_loadout() -> AttuneLoadout: - return encounter.get("loadout", null) + return _facts.encounter.get("loadout", null) # 授予件战斗域回调束(bind_battle 注入——键集封闭三键:充能消耗/真视计数/负向踩雷计数; -# 宿主侧各转 _attune_loadout/_journal/_mines_hits 单点,授予件对账本零强引用) +# 宿主侧各转 _attune_loadout/_journal/_rounds.mines_hits 单点,授予件对账本零强引用) func _battle_hooks() -> Dictionary[Variant, Variant]: return { "attune_consume": _hook_attune_consume, @@ -108,8 +93,8 @@ func _battle_hooks() -> Dictionary[Variant, Variant]: func _hook_attune_consume(item_id: String) -> void: - if _attune_loadout() != null and treasure_inventory != null: - _attune_loadout().consume_charge(item_id, treasure_inventory) + if _attune_loadout() != null and _reward_grant.treasure_inventory != null: + _attune_loadout().consume_charge(item_id, _reward_grant.treasure_inventory) func _hook_vision_add() -> void: @@ -120,8 +105,8 @@ func _hook_vision_add() -> void: # 负向 blast(雷)踩雷计数入账(触发条件事实面——正向 spark_aoe/清图奖励不计入; # 极性判定在授予件 _flat_blast 传点,本口只记账) func _book_negative_blast() -> void: - _mines_hits["round"] += 1 - _mines_hits["total"] += 1 + _rounds.mines_hits["round"] += 1 + _rounds.mines_hits["total"] += 1 # 效果引擎宿主侧事实单点(fx_host facts_cb 注入——round_index/battle_index/seals_count/nest_count/hp_ratio 五事实;charge_layers 归 fx_host 自答;未知事实回落 null) @@ -136,10 +121,10 @@ func _fx_host_fact(fact: String) -> Variant: "round_index": return round_index "battle_index": - return int(encounter.get("battle_index", 1)) # 关卡域战斗序(1 基——level_runner enc 携带;开局姿态窗条件面) + return int(_facts.encounter.get("battle_index", 1)) # 关卡域战斗序(1 基——level_runner enc 携带;开局姿态窗条件面) "seals_count": var hits_n := 0 - for fevt in _facts_seals: + for fevt in _facts.seals: if bool(fevt.get("hit", false)): hits_n += 1 return hits_n @@ -151,7 +136,7 @@ func _fx_host_fact(fact: String) -> Variant: # attune 积点读投影(RewardGrant attune_safe_seen_cb 注入面——use_treasure 拒绝文案读数;经读投影出户防组件回调宿主私有族) func _attune_safe_seen_read() -> int: - return _attune_safe_seen + return _facts.attune_safe_seen # 组件出口(拆件后取件面——呈现与测试经此取件,禁链式穿透协作对象内部) @@ -164,7 +149,7 @@ func reward_grant() -> RewardGrant: func _attune_has(item_id: String) -> bool: - return attunement.has(item_id) + return _facts.attunement.has(item_id) # 装配集只读出口(_attune_has 契约化出口,save v6 只读访问器先例。只读零副作用—— @@ -179,7 +164,7 @@ func attune_has(item_id: String) -> bool: # kind 分发查询先例=_attune_refresh_x_total bonus_refresh。零装配=0) func attune_param_sum(kind: String) -> int: var total := 0 - for iid_v in attunement: + for iid_v in _facts.attunement: var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v)) if (arow.get("effect_kinds", []) as Array).has(kind): total += int(arow.get("magnitude", 0)) @@ -189,7 +174,7 @@ func attune_param_sum(kind: String) -> int: # 装配集内 ap_charge_safe 行读取(取数源=装配集行扫描; # 首行语义=装配集内至多一件积点制〔试炼镐〕) func _attune_charge_safe_row() -> Dictionary: - for iid_v in attunement: + for iid_v in _facts.attunement: var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v)) if (arow.get("effect_kinds", []) as Array).has("ap_charge_safe"): return arow @@ -200,9 +185,9 @@ func _attune_charge_safe_row() -> Dictionary: # (used handle 不回收=本轮已耗充能作废,补新 handle=「回满」;差额注入防双份=level_runner # set_attune_charges 同构)。reset ∈ {battle, level}——adventure 周期归冒险开始种子面) func _reseed_attune_charges(reset: String) -> void: - if _attune_loadout() == null or treasure_inventory == null: + if _attune_loadout() == null or _reward_grant.treasure_inventory == null: return # 直构零装配/库存未就位——充能回满跳过(空装配同语义) - _attune_loadout().reset_cycle(reset, treasure_inventory) # 写时种行/补行(按周期筛——语义逐字承原循环体) + _attune_loadout().reset_cycle(reset, _reward_grant.treasure_inventory) # 写时种行/补行(按周期筛——语义逐字承原循环体) # 积点制件进度读数(自动兑付无玩家发起入口,读数口=C 区秘宝槽悬停浮窗进度行: @@ -213,18 +198,13 @@ func attune_convert_state() -> Dictionary: if row.is_empty() or String(row.get("form", "")) != "active_charged": return {"assembled": false, "points": 0, "need": 0, "display_name": "", "item_id": ""} var cn := int(row.get("charge_n", 0)) - return {"assembled": true, "points": _attune_safe_seen % cn if cn > 0 else 0, "need": cn, + return {"assembled": true, "points": _facts.attune_safe_seen % cn if cn > 0 else 0, "need": cn, "display_name": String(row.get("display_name", "")), "item_id": String(row.get("item_id", ""))} # 特性求值(效果行为=数据行 triggers 订阅,效果引擎 dispatch 分发; # 流派档位质变=FeatsEngine 纯函数求值 # (档位机制本身系引擎基建非内容效果),两运行态注伤害控制器:档一 rider+1 / 档二 rider 可暴击) -var _seal_miss_seen := {} # 封印未中行去重(每安全格实例一次) -var _perfect_hit_true := 0 # 整图完美封印计数(install 重置) -var _perfect_hit_false := 0 -var _perfect_forgiven := 0 # 完美保险豁免累计(单向累加自有账;契约 2 禁改写 _perfect_hit_false) -var _hero_atk := 0 # 主角攻击基值快照(encounter.hero_atk;E4 收口——表值运行期不可变) func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: @@ -234,6 +214,7 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: minesweeper = MinesweeperRules.new(db, action_economy) _enemy_system = UnitContainer.new(db) _status_effects = StatusContainer.new(db) + _facts = BattleFacts.new() # 战斗事实件(受理事实/轮缓冲/封印账/溯源/条件计数——成员迁移见设计 §4) _reward_economy = RewardEconomy.new(db, rng) _reward_economy.fatal_handler = _fatal # 协作件违约→本类 _fatal 封印(单点接线) _damage = DamageController.new(db, rng, _enemy_system, _status_effects) @@ -252,11 +233,8 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: minesweeper.round_end_board.connect(_on_round_end) action_economy.round_end_stopped.connect(_on_round_end) # 结算五段接线(授予/战斗两段持三引用=宿主+两协作件;信号连接纪律不涉段〔段零信号连接〕) - _grant_seg = GrantSegment.new(self, _fx_host, _reward_grant) - _aura_seg = AuraSegment.new(self) - _combat_seg = CombatSegment.new(self, _fx_host, _reward_grant) - _decay_seg = DecaySegment.new(self) - _verdict_seg = VerdictSegment.new(self) + # 结算五段接线随轮循环驱动件迁驻(round_cycle.gd——段件持有/段序/重入闸/摇号单点;构造序禁环不变) + _rounds = RoundCycle.new(self, _fx_host, _reward_grant) # ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → install+roll → 行动经济)---- @@ -269,7 +247,7 @@ func begin_battle(enc: Dictionary) -> bool: return false if not _install_and_reset(): return false - _reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险) + _facts.reset_round() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险) _begin_battle_fx_state(enc) _begin_battle_attune_params(enc) _begin_battle_preview_economy(enc) @@ -281,14 +259,14 @@ func begin_battle(enc: Dictionary) -> bool: if not action_economy.notify_round_start(): _fatal("行动经济 首轮 notify_round_start 被拒") return false - _roll_round_actions() # 开轮摇号(首轮同样摇;先于 round_started,挖掘期动画位可读) + _rounds.roll_round_actions() # 开轮摇号(首轮同样摇;先于 round_started,挖掘期动画位可读) round_started.emit(round_index) return true -# 受理段:encounter 装载+缓冲清零+护甲合流+重载闸+开域门+遭遇骰与敌人系统 spawn 受理门。 +# 受理段:_facts.encounter 装载+缓冲清零+护甲合流+重载闸+开域门+遭遇骰与敌人系统 spawn 受理门。 func _begin_battle_accept(enc: Dictionary) -> bool: - encounter.assign(enc) + _facts.encounter.assign(enc) _reward_grant.reset_battle() # 战斗域零 GP 持久(每场战斗缓冲从零起算;案C 闸复位) _damage.set_hero_ac_override(int(enc.get("hero_armor_class", -1))) # 主角护甲合流入参(伤害控制器 defender_ac 消费) db.set_reload_gate(true) @@ -299,9 +277,9 @@ func _begin_battle_accept(enc: Dictionary) -> bool: if dice.is_empty(): _fatal("敌人表 行集解析为空(%d,%d,%s)" % [int(enc.level_index), int(enc.node_depth), String(enc.get("node_type", "battle"))]) return false - # E4 收口:加成经 encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径) - _hero_atk = int(enc.get("hero_atk", int(db.combat.hero.atk))) - if not _enemy_system.spawn({"level_index": int(enc.level_index), "node_depth": int(enc.node_depth), "hero_hp": int(enc.hero_hp), "unit_count_overrides": dice[0], "hero_atk_override": _hero_atk, "hero_max_hp_override": int(enc.get("hero_max_hp", -1)), "enemy_atk_overrides": dice[1], "hero_attack_type": String(enc.get("attack_type", "")), "hero_armor_type": String(enc.get("armor_type", "")), "row_kind": String(enc.get("node_type", "battle"))}): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss) + # E4 收口:加成经 _facts.encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径; + # _hero_atk 派生读=受理后不变量,无快照成员) + if not _enemy_system.spawn({"level_index": int(enc.level_index), "node_depth": int(enc.node_depth), "hero_hp": int(enc.hero_hp), "unit_count_overrides": dice[0], "hero_atk_override": _hero_atk(), "hero_max_hp_override": int(enc.get("hero_max_hp", -1)), "enemy_atk_overrides": dice[1], "hero_attack_type": String(enc.get("attack_type", "")), "hero_armor_type": String(enc.get("armor_type", "")), "row_kind": String(enc.get("node_type", "battle"))}): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss) _fatal("敌人系统 spawn 被拒") return false return true @@ -309,7 +287,7 @@ func _begin_battle_accept(enc: Dictionary) -> bool: # 遭遇骰(难度曲线与Boss GDD Core Rule 8):行有 unit_count_max 时按 [unit_count, max] 均匀骰; # 行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值 -# encounter 封闭流恰一 scope(数量骰先、atk 骰后——区间宽度为零的行不掷=「定值不骰」既有口径); +# _facts.encounter 封闭流恰一 scope(数量骰先、atk 骰后——区间宽度为零的行不掷=「定值不骰」既有口径); # 敌人系统零随机纪律保持(收 override 即确定值)。 # 返回 [count_overrides, atk_overrides];行集为空 = 返回 [](fail 判据交受理段 _fatal)。 func _begin_battle_dice(enc: Dictionary) -> Array: @@ -352,12 +330,12 @@ func _begin_battle_dice_roll(rows: Array, estream: RandomNumberGenerator, roll_a # 引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零) # 扩签运行态就位(归 fx_host)+账本挂接+桶挂引用+揭示/AC 两回调+rider 档位恒零+订阅重建。 func _begin_battle_fx_state(enc: Dictionary) -> void: - # 同调装配运行态注入(enc.attunement 携带;off=空集零扰动。淬毒 N 注入 + # 同调装配运行态注入(enc._facts.attunement 携带;off=空集零扰动。淬毒 N 注入 # 伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install) - attunement = (enc.get("attunement", []) as Array).duplicate() - _attune_safe_seen = 0 + _facts.attunement = (enc.get("attunement", []) as Array).duplicate() + _facts.attune_safe_seen = 0 _fx_host.reset_battle_state(int(enc.get("hero_armor_class", -1))) - _mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款) + _rounds.mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款) var adv_bucket: Variant = enc.get("adv_bucket", null) _fx_host.mount_buckets(adv_bucket if adv_bucket is Dictionary else {}, enc.get("level_counters")) # 冒险桶宿主字典(直构兜底自有空字典);关卡桶 LevelRunner 每关 new 下传、非 Dictionary 即不挂 _reward_grant.bind_battle(_battle_hooks()) # 授予件战斗域回调束(充能消耗/真视计数/踩雷计数) @@ -368,13 +346,13 @@ func _begin_battle_fx_state(enc: Dictionary) -> void: # 不设档位机制」):rider_tier 两注入恒零,散件单体效果=唯一形态 _damage.inject["rider_tier_bonus"] = 0 _damage.inject["rider_tier_crit"] = false - _feat_first_dig_used = false + _facts.feat_first_dig_used = false # 树 rule 键消费(Fork 激活侧效果=效果引擎合成 # 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。其余 rule 键 # =stat 面投影不入战斗(T8 表内容 provisional 口径披露)) var tree_rules: Array[Variant] = enc.get("tree_rules", []) as Array # 效果引擎订阅重建(装载序=feats 表序→treasure 表序→树规则,写死保种子复放——细则见 battle_fx_host.rebuild_subscriptions) - _fx_host.rebuild_subscriptions(feat_ids, attunement, tree_rules) + _fx_host.rebuild_subscriptions(feat_ids, _facts.attunement, tree_rules) # 秘宝常时参数注入(装配集 kind 分发:远程镐挖距放宽/信标网络封印可达豁免; @@ -415,19 +393,19 @@ func _begin_battle_preview_economy(enc: Dictionary) -> void: # 板序推进+board_changed 发射+秘宝叠加层退役——时序逐字;复位职责留宿主(成员单点),不随件。 func _install_and_reset(during_round := false) -> bool: var installer := BoardInstaller.new({"db": db, "rng": rng, "minesweeper": minesweeper, - "reward_economy": _reward_economy, "encounter": encounter, "fatal_cb": _fatal}) + "reward_economy": _reward_economy, "encounter": _facts.encounter, "fatal_cb": _fatal}) if not installer.install(during_round): return false - _seal_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8) - _seal_miss_seen.clear() - _perfect_hit_true = 0 # 整图完美计数随 install 重置 - _perfect_hit_false = 0 - _perfect_forgiven = 0 # 豁免累计随 install 重置(domain=battle 每战清零) - _board_from_midround_refresh = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false - _board_seq += 1 + _facts.seal_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8) + _facts.seal_miss_seen.clear() + _facts.perfect_hit_true = 0 # 整图完美计数随 install 重置 + _facts.perfect_hit_false = 0 + _facts.perfect_forgiven = 0 # 豁免累计随 install 重置(domain=battle 每战清零) + _facts.board_from_midround = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false + _facts.board_seq += 1 board_changed.emit() - if treasure_inventory != null: - treasure_inventory.retire_overlays() # 叠加层随图消亡(换图/刷新图装板点;已花道具不返还) + if _reward_grant.treasure_inventory != null: + _reward_grant.treasure_inventory.retire_overlays() # 叠加层随图消亡(换图/刷新图装板点;已花道具不返还) return true @@ -448,9 +426,9 @@ func _on_cell_revealed(evt: Dictionary) -> void: if String(evt.get("kind", "")) == "safe": var cs_row: Dictionary[Variant, Variant] = _attune_charge_safe_row() if not cs_row.is_empty(): - _attune_safe_seen += 1 + _facts.attune_safe_seen += 1 var cn_b: int = int(cs_row.get("charge_n", 999)) - if cn_b > 0 and _attune_safe_seen % cn_b == 0: + if cn_b > 0 and _facts.attune_safe_seen % cn_b == 0: action_economy.grant_ap(int(cs_row.get("magnitude", 1))) if bool(evt.get("resource", false)): # 身份快照随缓冲捕获(批次隔离——轮内刷新后 mapping 已换批,授予必须用缓冲期身份, @@ -459,47 +437,20 @@ func _on_cell_revealed(evt: Dictionary) -> void: if snap != null: evt = evt.duplicate() evt["reward_snapshot"] = snap - _facts_reveals.append(evt) + _facts.reveals.append(evt) func _on_seal_judged(evt: Dictionary) -> void: - _facts_seals.append(evt) + _facts.seals.append(evt) func _on_round_end(_payload: Dictionary) -> void: - _facts_path = String(_payload.get("path_tag", "")) - _maybe_settle() + _facts.path = String(_payload.get("path_tag", "")) + _rounds.settle_if_due() -func _maybe_settle() -> void: - if _settling or battle_result != "": - return - if action_economy.phase != action_economy.PHASE_WAITING: - return - _settle() -# ---- 每轮结算序列(五段——段序骨架冻结于 SettlementTable.segment_order)---- - -func _settle() -> void: - _settling = true - var ri := round_index - # 段序编排(五段——段体归 game/core/settlement/,本类只做段序+行发射/开单原语单点) - _grant_seg.run(ri) # ① 授予段(案C 闸随段装载) - _aura_seg.run(ri) # ② 增益/减益段(续存实例组合行) - _combat_seg.run(ri) # ③ 战斗段(主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕) - _decay_seg.run(ri) # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减) - var verdict: String = _verdict_seg.run(ri) # ⑤ 判定段(防御性优先级 hero 先判) - if verdict != "continue": - return # 终局:_settling 不回置——_battle_over 已接管 - _settling = false - - -# ---- 授予段 helpers(极性路由)---- - -# 开格产出乘区(贪婪矿灯/矿脉直觉/赌徒的血签事件收集器注入): -# positive=power ×(1+静态 mult%);negative=power ×(1+cost%)。 -# 全零收集器=原快照原样返回(零扰动)。 func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int) -> Dictionary: if positive and pos_m <= 0: return snap @@ -556,57 +507,7 @@ func set_treasure_unlocked(ids: Dictionary) -> void: # 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。 # 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。 # 摇定后、兑现前死亡 → 本拍技能作废。 -func _roll_round_actions() -> void: - var pity_m: int = int(db.combat.skill.pity_threshold) - # facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts, - # 容器内直读主角行) - var facts := { - "round_index": round_index, - "last_round_end": "none" if action_economy.last_round_end().is_empty() else String(action_economy.last_round_end().get("path_tag", "none")), - "mines_hit_last": int(_mines_hits["last"]), - "mines_hit_total": int(_mines_hits["total"]), - } - for u in _enemy_system.query_alive("enemy"): - # Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行 - # 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性 - # 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机 - # 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏) - for sid in _enemy_system.check_trigger_unlocks(u, facts): - var srow_u: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(sid) - _emit_row({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"}, - round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))], - 0, {"skill_id": sid}) - # enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射, - # 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值) - var pct_v := 0 - for pe in (u.get("pool", []) as Array): - var pe_d: Dictionary[Variant, Variant] = pe - if String(pe_d.get("skill_id", "")) == sid and (pe_d.get("trigger", {}) as Dictionary).has("value"): - pct_v = int((pe_d.get("trigger", {}) as Dictionary).get("value", 0)) - break - # target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记, - # 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value) - var eht_hits := _fx_host.fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v, - "is_boss": String(encounter.get("node_type", "")) == "boss"}) - for h in eht_hits: - if String(h.get("kind", "")) == "target_attack_bonus": - u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0)) - var entries: Array[Variant] = _enemy_system.rollable_skills(u, facts) - if _enemy_system.basic_streak(u) >= pity_m: - var specials: Array[Variant] = _enemy_system.special_skills(u, facts) - if not specials.is_empty(): - entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合 - var stream: RandomNumberGenerator = rng.open_stream("behavior_roll") - _enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream)) - _enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记) - _mines_hits["last"] = _mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新) - _mines_hits["round"] = 0 - -# ---- 战斗段/算术读数(段体与掷点/折扣/暴击/克制/池乘区/下限/交付管线归 damage_controller.gd -# 与 settlement/combat_segment.gd;本类只保留薄委托——_defender_ac 供结算/呈现快照消费)---- - -# 防守方护甲等级读数——实现归 伤害控制器 defender_ac(本委托供结算快照/呈现快照) func _defender_ac(unit: Dictionary) -> int: return _damage.defender_ac(unit) @@ -643,12 +544,12 @@ func _emit_settlement_snapshot(verdict: String) -> void: buffs.append({"id": eff.effect_id, "side": "enemy", "layers": eff.layers, "left": eff.remaining_settlements}) var mines_hit := 0 var safe_n := 0 - for evt in _facts_reveals: + for evt in _facts.reveals: if evt.kind == "mine": mines_hit += 1 elif evt.kind == "safe": safe_n += 1 - var board_stats := {"seq": _board_seq} + var board_stats := {"seq": _facts.board_seq} if minesweeper.current_board() != null: board_stats["w"] = minesweeper.current_board().width board_stats["h"] = minesweeper.current_board().height @@ -656,18 +557,18 @@ func _emit_settlement_snapshot(verdict: String) -> void: board_stats["seals"] = minesweeper.seal_count() settlement_snapshot.emit({ "kind": "settlement", - "level": int(encounter.get("level_index", 0)), - "depth": int(encounter.get("node_depth", 0)), + "level": int(_facts.encounter.get("level_index", 0)), + "depth": int(_facts.encounter.get("node_depth", 0)), "round": round_index, "verdict": verdict, "unit_count": enemies.size(), "spawn_count": _enemy_system.all_enemies().size(), # 本战斗实例化总数(含死亡残位)——unit_count=0 时区分「首轮全灭」vs「空场」 "hero": {"hp": int(_enemy_system.hero_unit().get("hp", 0)), "max_hp": int(_enemy_system.hero_unit().get("max_hp", 0)), "atk": int(_enemy_system.hero_unit().get("atk", 0)), "armor_class": _defender_ac(_enemy_system.hero_unit())}, - "economy": {"phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "refresh_used": _refresh_used_this_round}, + "economy": {"phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "refresh_used": _facts.refresh_used}, "board": board_stats, "enemies": enemies, "buffs": buffs, - "round_facts": {"path": _facts_path, "safe": safe_n, "mines": mines_hit, "seal_events": _facts_seals.size(), }, + "round_facts": {"path": _facts.path, "safe": safe_n, "mines": mines_hit, "seal_events": _facts.seals.size(), }, }) @@ -675,7 +576,7 @@ func _emit_settlement_snapshot(verdict: String) -> void: func _exit_distribution() -> void: var mode := "same_board" - if _facts_path == "cleared": + if _facts.path == "cleared": mode = "new_board" # cleared 轮恒走 new_board(契约义务);非清图轮恒同图(等待期刷新通道已移除) if mode == "new_board": _reward_economy.retire_mapping() @@ -689,16 +590,16 @@ func _exit_distribution() -> void: round_index += 1 _fx_host.emit_round_start() action_economy.notify_round_start() - _reset_round_facts() - _refresh_used_this_round = 0 - _roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道) + _facts.reset_round() + _facts.refresh_used = 0 + _rounds.roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道) round_started.emit(round_index) # 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道 func can_refresh() -> bool: return battle_result == "" \ - and _refresh_used_this_round < int(db.run.action_economy.refresh_cap_per_round) \ + and _facts.refresh_used < int(db.run.action_economy.refresh_cap_per_round) \ and action_economy.phase == action_economy.PHASE_ACTIVE \ and action_economy.rap >= int(db.run.action_economy.rap_refresh_cost) @@ -711,7 +612,7 @@ func player_request_refresh() -> bool: return false if not action_economy.request_refresh(): return false - _refresh_used_this_round += 1 + _facts.refresh_used += 1 _reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提) minesweeper.retire_board(true) # 旧板退役(轮内通道) return _install_and_reset(true) # 当场换图——失败即编排违例 fail-fast @@ -737,11 +638,11 @@ func _battle_over(result: String) -> void: _reward_economy.retire_mapping() _enemy_system.despawn_all() _status_effects.notify_battle_end() - if treasure_inventory != null: - treasure_inventory.retire_overlays() # 叠加层随战斗结束清空(库存不动——关卡域跨战斗) + if _reward_grant.treasure_inventory != null: + _reward_grant.treasure_inventory.retire_overlays() # 叠加层随战斗结束清空(库存不动——关卡域跨战斗) action_economy.notify_combat_end() # → Idle,RAP 清零 db.set_reload_gate(false) - _reset_round_facts() + _facts.reset_round() battle_finished.emit({"result": result, "hero_hp": hero_hp_left, "gp_dropped": gp_dropped}) @@ -749,7 +650,7 @@ func _battle_over(result: String) -> void: # gp_dropped = gp_drop 封闭流掷定(tier 行的 roll_min..roll_max 均匀整数——每战斗末恰一 scope) # + level_gp_bonus(level_index) + escrow。掷定行缺位 = 数据面缺行(fail 报错按 0 兑付——不哑掉)。 func _roll_gp_drop() -> int: - var tier := "boss" if String(encounter.get("node_type", "")) == "boss" else "normal" + var tier := "boss" if String(_facts.encounter.get("node_type", "")) == "boss" else "normal" var drops: Dictionary[Variant, Variant] = db.run.shop.gp_drop_row(tier) var roll := 0 if drops.is_empty(): @@ -757,11 +658,11 @@ func _roll_gp_drop() -> int: else: var stream: RandomNumberGenerator = rng.open_stream("gp_drop") roll = stream.randi_range(int(drops.get("roll_min", 0)), int(drops.get("roll_max", 0))) - var drop := roll + db.run.shop.level_gp_bonus_at(int(encounter.get("level_index", 0))) + var drop := roll + db.run.shop.level_gp_bonus_at(int(_facts.encounter.get("level_index", 0))) # 财富之道·聚(战斗掉落 GP 乘算·入账前:只乘战斗掉落两成分〔掷定+关卡加成〕, # 店内 escrow 不乘〔范围=仅战斗掉落渠道〕;本函数返回值=结算框「GP 增量」行 # 显示源,落在入账前=显示即实得;乘数 provisional 150%) - if (encounter.get("tree_rules", []) as Array).has("economy_hoard"): + if (_facts.encounter.get("tree_rules", []) as Array).has("economy_hoard"): drop = int(round(float(drop) * 1.5)) return drop + _reward_grant.escrow @@ -812,7 +713,7 @@ func presentation_snapshot() -> Dictionary: "phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "can_refresh": can_refresh(), - "refresh_used": _refresh_used_this_round, # 受理计数入呈现成文面(HUD 余量单源读此处) + "refresh_used": _facts.refresh_used, # 受理计数入呈现成文面(HUD 余量单源读此处) "hero": hero, "enemies": enemies, "hero_effects": _status_effects.query_active("hero").duplicate(true), # 值拷贝出户(快照消费方直改不渗入状态容器) @@ -839,12 +740,12 @@ func diagnostic_snapshot() -> Dictionary: return { "battle_result": battle_result, "round": round_index, - "settling": _settling, - "refresh_used_round": _refresh_used_this_round, - "board_seq": _board_seq, - "facts_path": _facts_path, - "facts_reveals": _facts_reveals.size(), - "facts_seals": _facts_seals.size(), + "settling": _rounds.is_settling(), + "refresh_used_round": _facts.refresh_used, + "board_seq": _facts.board_seq, + "facts_path": _facts.path, + "facts_reveals": _facts.reveals.size(), + "facts_seals": _facts.seals.size(), "action_economy": {"phase": String(action_economy.phase), "ap": action_economy.ap, "rap": action_economy.rap}, "minesweeper_board_installed": minesweeper.current_board() != null, "minesweeper_revealed": minesweeper.revealed_cells().size() if minesweeper.current_board() != null else 0, @@ -855,12 +756,6 @@ func diagnostic_snapshot() -> Dictionary: } -func _reset_round_facts() -> void: - _facts_reveals.clear() - _facts_seals.clear() - _facts_path = "" - - # ---- 行协议(Core Rule:公共字段 + 行类型特有字段)---- func _emit_row(head: Dictionary, ri: int, display: String, value: int, extra: Dictionary) -> void: @@ -911,6 +806,6 @@ func _fatal(msg: String) -> void: ## 完美封印谓词(两账相减:有效误标=误标总数−豁免数) -## 契约 2:_perfect_hit_false 保持纯净,豁免只累加 _perfect_forgiven,禁回退式改写与拦截式吞事件。 +## 契约 2:_facts.perfect_hit_false 保持纯净,豁免只累加 _facts.perfect_forgiven,禁回退式改写与拦截式吞事件。 func perfect_sweep_earned() -> bool: - return _perfect_hit_false - _perfect_forgiven <= 0 and _perfect_hit_true >= 1 + return _facts.perfect_hit_false - _facts.perfect_forgiven <= 0 and _facts.perfect_hit_true >= 1 diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 05d110d5..df01c3f4 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -517,7 +517,7 @@ func _start_battle(node: Dictionary) -> bool: _settlement = BattleOrchestrator.new(db, rng) if treasure_inventory == null: treasure_inventory = TreasureItems.new(db) # 惰性建——关卡域单实例(跨战斗延续) - _settlement.treasure_inventory = treasure_inventory # 库存注入(授予路由+use_treasure 编排消费面) + _settlement.reward_grant().treasure_inventory = self.treasure_inventory # 库存注入(经授予件出口直注——注入时序在宿主构造之后不变) if _settlement != null: _settlement.set_treasure_unlocked(_treasure_unlocked) # 解锁门控下传(装板摇号前) # core↔core 接线只许方法 Callable(静态规则 S6——lambda 捕获 self 会延长 diff --git a/game/core/settlement/combat_segment.gd b/game/core/settlement/combat_segment.gd index db8bae28..1e9a6ac9 100644 --- a/game/core/settlement/combat_segment.gd +++ b/game/core/settlement/combat_segment.gd @@ -26,7 +26,7 @@ func run(ri: int) -> void: # 同一次 _settle 内,facts 已定。 var round_safe := 0 var round_mine := false - for evt in oc._facts_reveals: + for evt in oc._facts.reveals: if evt.kind == "safe": round_safe += 1 elif evt.kind == "mine": @@ -53,7 +53,7 @@ func _hero_strike(ri: int) -> void: var hero_target = oc._enemy_system.default_target() if hero_alive and hero_target != null: var ht := oc._damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击", - {"channel": "combat", "base_value": oc._hero_atk, "target_rule": DamageController.TARGET_FRONT_ALIVE, + {"channel": "combat", "base_value": oc._hero_atk(), "target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": ri, "origin_cell": Vector2i(-1, -1), "attack_type": String(oc._enemy_system.hero_unit().get("attack_type", "normal"))}) oc._damage.submit(ht) diff --git a/game/core/settlement/grant_segment.gd b/game/core/settlement/grant_segment.gd index 2de0123c..1c396e51 100644 --- a/game/core/settlement/grant_segment.gd +++ b/game/core/settlement/grant_segment.gd @@ -34,7 +34,7 @@ func run(ri: int) -> void: # 开格/踩雷兑付批(轮 facts_reveals 资源事件序:safe 正向乘区兑付 / mine_hit 减半负向兑付) func _reveal_batch(ri: int) -> void: - for evt in oc._facts_reveals: + for evt in oc._facts.reveals: if not bool(evt.get("resource", false)): continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②) var snap = evt.get("reward_snapshot") @@ -61,7 +61,7 @@ func _reveal_safe(evt: Dictionary, snap: Dictionary, ri: int) -> void: if is_new_type: fx_host._fx_new_resource_types[ctype] = true var dr_hits := fx_host.fx_emit("dig_reveal", {"cell": evt.cell, "round": ri, - "is_first_dig_of_battle": not oc._feat_first_dig_used, + "is_first_dig_of_battle": not oc._facts.feat_first_dig_used, "terrain_kind": oc._harmful_variants(evt.get("variant_ids", []) as Array), "is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0 or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1, @@ -91,8 +91,8 @@ func _reveal_safe_apply(dr_hits: Array, evt: Dictionary, ri: int) -> void: if String(h.get("kind", "")) == "grant_material": _apply_material_hit(h, evt.cell, ri) var spell_hits: Array[Variant] = oc._hits_of(dr_hits, "spawn_spell") - if not spell_hits.is_empty() and not oc._feat_first_dig_used: - oc._feat_first_dig_used = true + if not spell_hits.is_empty() and not oc._facts.feat_first_dig_used: + oc._facts.feat_first_dig_used = true var spell_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell", "奥术矢", {"channel": "board", "base_value": int((spell_hits[0] as Dictionary).get("value", 0)), "target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": ri, "origin_cell": evt.cell, "attack_type": "magic"}) @@ -160,7 +160,7 @@ func _reveal_mine_apply(mh_hits: Array, snap: Dictionary, cell: Vector2i, ri: in # 危险格恒无变异——布置段排除,双层叠罚不存在;封印格直挖照吃变异伤害——地形后果同 mine_hit 口径, # 非奖励不作废;吞噬流资源兑付〔荆棘编织/地脉补偿〕中伤害形态行同样经案C闸暂存,非伤害形态照常当场) func _terrain_batch(ri: int) -> void: - for evt in oc._facts_reveals: + for evt in oc._facts.reveals: var vids: Array[Variant] = evt.get("variant_ids", []) for vid in vids: var vrow: Dictionary[Variant, Variant] = oc.db.combat.variation.row_by_id(String(vid)) @@ -203,7 +203,7 @@ func _terrain_damage_reduce(dt_hits: Array, dt_amount: int) -> int: # 台账披露待追认〕;完美布防=本轮封印批全中(hit>0 且 miss==0)时封印伤 ×mult——全对推理奖励)。 # 伤害单据案C 暂存战斗段末兑付。 func _seal_batch(ri: int) -> void: - for evt in oc._facts_seals: + for evt in oc._facts.seals: _judge_seal(evt, ri) _seal_strike_batch(ri) @@ -213,7 +213,7 @@ func _seal_batch(ri: int) -> void: func _seal_strike_batch(ri: int) -> void: var fs_hits := 0 var fs_misses := 0 - for fevt in oc._facts_seals: + for fevt in oc._facts.seals: if bool(fevt.get("hit", false)): fs_hits += 1 else: @@ -223,7 +223,7 @@ func _seal_strike_batch(ri: int) -> void: var strike := _seal_strike_value(fs_fx, fs_hits, fs_misses) if strike.is_empty(): return - for fevt in oc._facts_seals: + for fevt in oc._facts.seals: if not bool(fevt.get("hit", false)): continue var st_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_seal_strike", "战封印军规", @@ -272,8 +272,8 @@ func _round_end_apply(ri: int) -> void: var rap_pct: int = oc.attune_param_sum("rap_transfer_mult") oc.action_economy.finish_round_stop(fx_host.engine.has_effect_on("round_end", "grant_ap", "transfer"), rap_pct if rap_pct > 0 else 100) - if oc._facts_path == "cleared": - reward_grant.grant_clear_reward(ri, oc._board_from_midround_refresh, oc.perfect_sweep_earned()) + if oc._facts.path == "cleared": + reward_grant.grant_clear_reward(ri, oc._facts.board_from_midround, oc.perfect_sweep_earned()) fx_host.flush_pending_reveals(ri) # 揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入) oc._damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界) @@ -305,10 +305,10 @@ func _judge_seal(evt: Dictionary, ri: int) -> void: # 殉爆票据 → 施加面 → 雷区图谱额外兑付 → 基线正向兑付) func _judge_seal_hit(evt: Dictionary, ri: int) -> void: var cell: Vector2i = evt.cell - if oc._seal_consumed.has(cell): + if oc._facts.seal_consumed.has(cell): return # 去重跳过;零明细行 - oc._seal_consumed[cell] = true - oc._perfect_hit_true += 1 + oc._facts.seal_consumed[cell] = true + oc._facts.mark_perfect_hit() var fh_hits := fx_host.fx_emit("seal_hit", {"cell": cell, "round": ri, "terrain_kind": oc._harmful_adjacent(cell)}) _seal_burst_tickets(fh_hits, cell, ri) @@ -367,8 +367,8 @@ func _seal_preview_area(cell: Vector2i, ri: int) -> void: var n_snap = oc._reward_economy.query_full_snapshot(n) if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))): types.append(String((n_snap as Dictionary).get("cell_type", ""))) - if oc.treasure_inventory != null and not types.is_empty(): - oc.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell)) + if reward_grant.treasure_inventory != null and not types.is_empty(): + reward_grant.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell)) oc._emit_row({"type": "extended", "source_class": "fx_warning_beacon", "origin_cell": cell}, ri, "示警烟火·周围资源类型", 0, {"effect_id": "fx_preview_area", "types": types}) @@ -376,10 +376,10 @@ func _seal_preview_area(cell: Vector2i, ri: int) -> void: # grant_ap mode:queue 下一轮兑现 / forgive_seal_miss 完美保险豁免 → 归因行 → 负向兑付) func _judge_seal_miss(evt: Dictionary, ri: int) -> void: var cell: Vector2i = evt.cell - oc._perfect_hit_false += 1 - if oc._seal_miss_seen.has(cell): + oc._facts.mark_perfect_miss() + if oc._facts.seal_miss_seen.has(cell): return # 每格实例一次去重 - oc._seal_miss_seen[cell] = true + oc._facts.seal_miss_seen[cell] = true # 危险直觉·守挂点(seal_missed 发射:契约 12 触发主语=「当回合发生的标错 # 动作」——去重闸=新旧标错判别器〔后续轮重判旧封印被闸挡回零触发〕;双条件②「结算时封印仍在 # 场」由发射面既有过滤承载〔被挖开的封印已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮 @@ -391,11 +391,11 @@ func _judge_seal_miss(evt: Dictionary, ri: int) -> void: elif String(h.get("kind", "")) == "forgive_seal_miss": # 完美保险(订阅式豁免:本战首次标错计一次豁免,只累加自有账 # _perfect_forgiven,绝不回写 _perfect_hit_false=契约 2 计数纯净;发射时机与消费点零改动=契约 3) - oc._perfect_forgiven += 1 + oc._facts.mark_perfect_forgiven() _apply_counter_hit(h) # 豁免同时记账(insurance_used battle 域——下次标错 when lt 1 不再命中) oc._emit_row({"type": "extended", "source_class": "seal_miss", "origin_cell": cell}, ri, "完美保险·标错豁免", 0, - {"effect_id": "insurance_used", "forgiven": oc._perfect_forgiven}) - var hl := "perfect_broken" if oc._perfect_hit_false - oc._perfect_forgiven == 1 else "" + {"effect_id": "insurance_used", "forgiven": oc._facts.perfect_forgiven}) + var hl := "perfect_broken" if oc._facts.perfect_hit_false - oc._facts.perfect_forgiven == 1 else "" oc._emit_row({"type": "seal_miss", "source_class": "seal_miss", "origin_cell": cell, "highlight": hl}, ri, "封印未中", 0, {}) var snap = oc._reward_economy.grant(cell) if not (snap is Dictionary): diff --git a/game/core/settlement/round_cycle.gd b/game/core/settlement/round_cycle.gd new file mode 100644 index 00000000..370e6c77 --- /dev/null +++ b/game/core/settlement/round_cycle.gd @@ -0,0 +1,106 @@ +## 轮循环驱动件:每轮结算周期的唯一驱动——结算重入门闸(settling)、五段段序执行 +## (段序骨架冻结于 SettlementTable.segment_order)、开轮摇号(共识点 B:技能选择前置到轮次开始, +## 摇定即本拍结算的选技)、踩雷计数账(转存时点=摇号轮末)。 +## 单一变化原因:轮结算周期的推进结构(段序、重入闸、摇号时序)。 +## 耦合边界:oc=宿主引用(五段段类既定同款直调形态)——经 oc 取 facts/协作件/行发射/出口分发; +## 五段构造参数与持有形态不变(grant/combat 三引用、aura/decay/verdict 单引用);判定段零改动 +## (_exit_distribution 留宿主);随机只经 oc.rng 的 behavior_roll 封闭流(流名、开流时点、取号序逐字); +## 零信号连接、零 await;随编排器每战新建同灭。 +class_name RoundCycle +extends RefCounted + +var oc: BattleOrchestrator # 宿主引用(段类同款直调——facts/协作件/行发射/出口分发取数单点) +var mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(本场累计/上轮转存/当轮累计三读数合一账) +var _grant_seg = null # GrantSegment(①授予段——含封印判与 counter/grant 助手单点) +var _aura_seg = null # AuraSegment(②增益/减益段) +var _combat_seg = null # CombatSegment(③战斗段) +var _decay_seg = null # DecaySegment(④衰减段) +var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归宿主) +var _settling := false + + +func _init(host: BattleOrchestrator, fx_host_ref: BattleFxHost, reward_grant_ref: RewardGrant) -> void: + oc = host + # 结算五段接线(授予/战斗两段持三引用=宿主+两协作件;信号连接纪律不涉段〔段零信号连接〕——五行自宿主 _init 逐字迁入) + _grant_seg = GrantSegment.new(oc, fx_host_ref, reward_grant_ref) + _aura_seg = AuraSegment.new(oc) + _combat_seg = CombatSegment.new(oc, fx_host_ref, reward_grant_ref) + _decay_seg = DecaySegment.new(oc) + _verdict_seg = VerdictSegment.new(oc) + + +# 结算重入门闸+段序体(原宿主 _maybe_settle 三闸与 _settle 合体——闸序 settling→battle_result→phase +# 逐字;终局早退不回置 _settling,_battle_over 接管) +func settle_if_due() -> void: + if _settling or oc.battle_result != "": + return + if oc.action_economy.phase != oc.action_economy.PHASE_WAITING: + return + _settling = true + var ri := oc.round_index + # 段序编排(五段——段体归 game/core/settlement/,宿主只做行发射/开单原语单点) + _grant_seg.run(ri) # ① 授予段(案C 闸随段装载) + _aura_seg.run(ri) # ② 增益/减益段(续存实例组合行) + _combat_seg.run(ri) # ③ 战斗段(主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕) + _decay_seg.run(ri) # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减) + var verdict: String = _verdict_seg.run(ri) # ⑤ 判定段(防御性优先级 hero 先判) + if verdict != "continue": + return # 终局:_settling 不回置——_battle_over 已接管 + _settling = false + + +func is_settling() -> bool: + return _settling + + +# ---- 开轮摇号(共识点 B:技能选择前置到轮次开始,数值计算留结算段——原宿主函数逐字迁入)---- +# 每实例:保底判定(连续执行普攻 ≥ M → 必出特殊技、特殊技内按权重、不受冷却限制)→ 排除冷却中技能 +# (中签率 = 权重 ÷ 可摇权重合计——份额归一)→ 按行序逐敌从统一流顺序取号摇定本拍动作。 +# 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。 +# 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。 +# 摇定后、兑现前死亡 → 本拍技能作废。 +func roll_round_actions() -> void: + var pity_m: int = int(oc.db.combat.skill.pity_threshold) + # facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts, + # 容器内直读主角行) + var facts := { + "round_index": oc.round_index, + "last_round_end": "none" if oc.action_economy.last_round_end().is_empty() else String(oc.action_economy.last_round_end().get("path_tag", "none")), + "mines_hit_last": int(mines_hits["last"]), + "mines_hit_total": int(mines_hits["total"]), + } + for u in oc._enemy_system.query_alive("enemy"): + # Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行 + # 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性 + # 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机 + # 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏) + for sid in oc._enemy_system.check_trigger_unlocks(u, facts): + var srow_u: Dictionary[Variant, Variant] = oc.db.combat.skill.row_by_id(sid) + oc._emit_row({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"}, + oc.round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))], + 0, {"skill_id": sid}) + # enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射, + # 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值) + var pct_v := 0 + for pe in (u.get("pool", []) as Array): + var pe_d: Dictionary[Variant, Variant] = pe + if String(pe_d.get("skill_id", "")) == sid and (pe_d.get("trigger", {}) as Dictionary).has("value"): + pct_v = int((pe_d.get("trigger", {}) as Dictionary).get("value", 0)) + break + # target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记, + # 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value) + var eht_hits := oc._fx_host.fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v, + "is_boss": String(oc._facts.encounter.get("node_type", "")) == "boss"}) + for h in eht_hits: + if String(h.get("kind", "")) == "target_attack_bonus": + u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0)) + var entries: Array[Variant] = oc._enemy_system.rollable_skills(u, facts) + if oc._enemy_system.basic_streak(u) >= pity_m: + var specials: Array[Variant] = oc._enemy_system.special_skills(u, facts) + if not specials.is_empty(): + entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合 + var stream: RandomNumberGenerator = oc.rng.open_stream("behavior_roll") + oc._enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream)) + oc._enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记) + mines_hits["last"] = mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新) + mines_hits["round"] = 0 diff --git a/game/core/settlement/round_cycle.gd.uid b/game/core/settlement/round_cycle.gd.uid new file mode 100644 index 00000000..f7d726e5 --- /dev/null +++ b/game/core/settlement/round_cycle.gd.uid @@ -0,0 +1 @@ +uid://ce07smbypdcyj diff --git a/game/tests/static_scan.sh b/game/tests/static_scan.sh index df9951d6..c76494db 100644 --- a/game/tests/static_scan.sh +++ b/game/tests/static_scan.sh @@ -314,7 +314,7 @@ fi # get_tree());排除注释行(行首 #/##)与字符串字面量(双引号包裹段内的 await 非代码), # 口径仿 S1/S2 的 grep 排除手法。 # 自检:清单内文件缺位(路径写错/被改名)即判红——防扫面为空静默绿。 -S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd" +S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd" S8_MISS=0 for f in $S8_FILES; do if [ ! -f "$f" ]; then @@ -356,7 +356,7 @@ fi # 自检:扫描面为空即判红(require_scan_face);B1 文件集与三族自家文件缺位即判红 # (文件被改名=豁免名单失效,宁可红不可静默放行)。 if require_scan_face "S7" "$DIR_CORE"; then -S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd" +S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd" S7_MISS=0 for f in $S7_B1_SET game/core/unit_container.gd game/core/status_container.gd game/core/reward_economy.gd; do if [ ! -f "$f" ]; then diff --git a/game/tests/unit/battle_settlement/roll_multiplier_test.gd b/game/tests/unit/battle_settlement/roll_multiplier_test.gd index ac3c8980..ac4365af 100644 --- a/game/tests/unit/battle_settlement/roll_multiplier_test.gd +++ b/game/tests/unit/battle_settlement/roll_multiplier_test.gd @@ -34,9 +34,31 @@ func _init() -> void: _smoke_pool_wiring() _smoke_damage_types() _smoke_attack_chain_fresh_pool() + _smoke_hero_atk_derived() quit(Kit.finish("roll_multiplier_test")) +# ---- 主角攻击基值派生读(注册面批同代编排归约 V4——_hero_atk 快照成员退役改派生读: +# enc 携 hero_atk 键受理后经派生读供 spawn 与攻击单 base_value 两消费点;缺省回退表值—— +# 受理后不变量(encounter 受理后只读)。攻击单 base_value 贯通由 _smoke_damage_types 的 +# hero_atk_val=100 全链路行值承载(同夹具既有覆盖——派生读在攻击链上工作即行值随 override 走)---- +func _smoke_hero_atk_derived() -> void: + var rng_a = load("res://core/rng_service.gd").new() + rng_a.set_master_seed(4242) + var s_a = load("res://core/battle_orchestrator.gd").new(_db, rng_a) + Kit.check_quiet(s_a.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 100}), + "起战受理(override 100)〔编排归约 V4〕") + Kit.check_quiet(int(s_a._enemy_system.hero_unit().get("atk", -1)) == 100, + "主角攻击单基值==enc 携带 override(_hero_atk 派生读贯通 spawn 消费点)〔编排归约 V4〕") + var rng_b = load("res://core/rng_service.gd").new() + rng_b.set_master_seed(4242) + var s_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b) + Kit.check_quiet(s_b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}), + "起战受理(缺省)〔编排归约 V4〕") + Kit.check_quiet(int(s_b._enemy_system.hero_unit().get("atk", -1)) == int(_db.combat.hero.atk), + "缺省主角攻击单基值==hero 表值(派生读回退面)〔编排归约 V4〕") + + # ---- 敌方攻击类型轴(表列/单位快照/单据载荷/矩阵命中证词)---- # 克制矩阵命中证词 = 同种子三甲型配对实测(不是查表存在性证词):同种子下 armor_roll 流消费逐位一致、 # 阈值(hero armor_class)不变,三甲型唯一差异 = 克制点数 cdelta(normal_vs_light 0 / heavy −1 / ward +1) diff --git a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd index 2b972ee1..677a9f25 100644 --- a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd +++ b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd @@ -83,9 +83,10 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary: var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(42) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) - var out := {"stream": [], "events": [], "rounds": 0, "result": "", "terminated": false} + var out := {"stream": [], "events": [], "rounds": 0, "result": "", "terminated": false, "econ_refresh_used": -1} settlement.step_row.connect(func(row): out.stream.append(row)) settlement.battle_finished.connect(func(payload): out.result = payload.get("result", "")) + settlement.settlement_snapshot.connect(func(snap): out.econ_refresh_used = int(snap.get("economy", {}).get("refresh_used", -1))) # 末次发射=终局轮快照 settlement.minesweeper.cell_revealed.connect(func(evt): out.events.append(evt)) settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt)) settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag})) @@ -108,6 +109,10 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary: # P0-3 回归断言:notify_combat_end 曾被并进行尾注释从不执行—— # 战斗收束后相位必须回 Idle(RAP 清零),任何「相位卡 Waiting」回归在此被抓 Kit.check("战斗收束后行动经济相位回 Idle(notify_combat_end 生效,实测=%s)" % String(action_economy.phase), action_economy.phase == "Idle") + # 复位边界(注册面批同代编排归约 V2——BattleFacts.reset_round 不碰 refresh_used 的可观测提交面): + # 终局序列含 facts.reset_round(),其后呈现快照的 refresh_used 必须等于末轮结算快照的 economy 值 + Kit.check("L%d·D%d 终局后呈现快照 refresh_used==末轮结算快照 economy 值(复位不碰 refresh_used)〔编排归约 V2〕" % [L, D], + int(settlement.presentation_snapshot().get("refresh_used", -1)) == out.econ_refresh_used) return out @@ -1019,7 +1024,7 @@ func _smoke_counter_buckets() -> void: if not oA.minesweeper.current_board().is_mine(ca): safe_a = ca break - oA._grant_seg._judge_seal({"cell": safe_a, "hit": false}, 1) + oA._rounds._grant_seg._judge_seal({"cell": safe_a, "hit": false}, 1) Kit.check("V6 第1战标错落 battle 桶 insurance_used=1(_apply_counter_hit 静态写口经 ctx)〔无状态化 V6〕", EffectEngine.counter(oA.fx_host().counters_ctx, "insurance_used") == 1) # 卡④ wr2①——完美保险标错行=单一扩展行型(原 insurance_used 实名行型收敛) var v6_ins := {} diff --git a/game/tests/unit/battle_settlement/seal_polarity_test.gd b/game/tests/unit/battle_settlement/seal_polarity_test.gd index e10b1241..862711b1 100644 --- a/game/tests/unit/battle_settlement/seal_polarity_test.gd +++ b/game/tests/unit/battle_settlement/seal_polarity_test.gd @@ -374,10 +374,10 @@ func _smoke_clear_reward_zero_paths() -> void: Kit.check("高光守卫构造战斗启动", s_b.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"})) for u in s_b._enemy_system.all_enemies(): s_b._enemy_system.apply_damage(u, int(u.hp)) - s_b._perfect_hit_true = 1 # 测试通道:构造完美谓词成立(真值在正常流由封印判定累加) - s_b._perfect_hit_false = 0 + s_b._facts.perfect_hit_true = 1 # 测试通道:构造完美谓词成立(真值在正常流由封印判定累加) + s_b._facts.perfect_hit_false = 0 rows_b.clear() - s_b.reward_grant().grant_clear_reward(1, s_b._board_from_midround_refresh, s_b.perfect_sweep_earned()) + s_b.reward_grant().grant_clear_reward(1, s_b._facts.board_from_midround, s_b.perfect_sweep_earned()) Kit.check("② grant_clear_reward 守卫路径重走首份 → 未生效行仍在案", _has_source_row(rows_b, "reward_ineffective", "clear_reward", "clear_blast")) Kit.check("② 首份零兑付 → 高光行不发(不记名义值 2)", _rows_of_type(rows_b, "highlight").is_empty()) # ③ 第二份被拒:唯一存活敌 HP ≤ 首份 aoe 量 → 首份击杀 → 第二份无存活敌(zero + 未生效行) @@ -389,10 +389,10 @@ func _smoke_clear_reward_zero_paths() -> void: Kit.check("第二份被拒构造战斗启动", s2.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"})) var boss = s2._enemy_system.all_enemies()[0] boss.hp = 1 # 测试通道:压低至首份 aoe 必杀(弱形式——正常流 Boss 血量不可控) - s2._perfect_hit_true = 1 - s2._perfect_hit_false = 0 + s2._facts.perfect_hit_true = 1 + s2._facts.perfect_hit_false = 0 rows2.clear() - s2.reward_grant().grant_clear_reward(1, s2._board_from_midround_refresh, s2.perfect_sweep_earned()) + s2.reward_grant().grant_clear_reward(1, s2._facts.board_from_midround, s2.perfect_sweep_earned()) Kit.check("③ 首份 aoe 真实兑付(击杀唯一存活敌)", _has_source_row(rows2, "damage", "clear_reward")) Kit.check("③ 第二份被拒产出 reward_ineffective 行(零兑付)", _has_source_row(rows2, "reward_ineffective", "clear_reward", "clear_blast")) var hl2 := _rows_of_type(rows2, "highlight") diff --git a/game/tests/unit/battle_settlement/trigger_cond_test.gd b/game/tests/unit/battle_settlement/trigger_cond_test.gd index cd7c0709..026b88d6 100644 --- a/game/tests/unit/battle_settlement/trigger_cond_test.gd +++ b/game/tests/unit/battle_settlement/trigger_cond_test.gd @@ -116,19 +116,19 @@ func _init() -> void: "feats": [], "tree_rules": [], "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle", "adv_bucket": {}}), "V6 前置:战斗受理") # facts 组包经真实摇号路径消费(mines 全零时条件技 sneak_ambush 不入集合——facts 四键在位的可观察面) - Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["last"]) == 0 and int(orch._mines_hits["round"]) == 0, + Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 0 and int(orch._rounds.mines_hits["last"]) == 0 and int(orch._rounds.mines_hits["round"]) == 0, "V6 开战清零:三计数全零起〔触发器批 V6〕") # 正向 blast(spark_aoe)不递增——负向计数守卫(flat 行应用面自编排器迁至奖励授予件 # ——迭代40 卡① 拆件;计数经授予件 battle_hooks.negative_blast 回宿主 _mines_hits) var att_v6 := {"source_class": "reward", "cell": Vector2i(-1, -1), "ri": 1} orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "positive", "power": 2, "display_name": "爆裂", "reward_id": "spark_aoe"}, att_v6) - Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["round"]) == 0, + Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 0 and int(orch._rounds.mines_hits["round"]) == 0, "V6 正向 blast(spark_aoe)不递增(负向守卫——清图/爆裂格零误计)〔触发器批 V6〕") # 负向 blast(雷)递增 orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "negative", "power": 2, "display_name": "警觉", "reward_id": "enemy_alert"}, att_v6) - Kit.check_quiet(int(orch._mines_hits["total"]) == 1 and int(orch._mines_hits["round"]) == 1, + Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 1 and int(orch._rounds.mines_hits["round"]) == 1, "V6 负向 blast(雷)递增恰一次〔触发器批 V6〕") # ---- V7 池构成纪律(现行全表零违例+构造全条件池报错——跨表校验真实入口)---- diff --git a/game/tests/unit/core/adventure_feats_test.gd b/game/tests/unit/core/adventure_feats_test.gd index 320ecbad..28e5093f 100644 --- a/game/tests/unit/core/adventure_feats_test.gd +++ b/game/tests/unit/core/adventure_feats_test.gd @@ -850,7 +850,7 @@ func _drive_to_pref(autoplay, prefer: String) -> Dictionary: r.pick_next_node(int(hit.get("id", -1))) out["node"] = hit if prefer == "battle": - out["enc"] = r.current_battle().encounter + out["enc"] = r.current_battle()._facts.encounter else: var cands: Array = (adr.stop().node_curio_candidates() as Dictionary).get("candidates", []) as Array adr.stop().present_node_curio(cands) diff --git a/game/tests/unit/core/feat_vouchers_test.gd b/game/tests/unit/core/feat_vouchers_test.gd index ee376c43..33631f57 100644 --- a/game/tests/unit/core/feat_vouchers_test.gd +++ b/game/tests/unit/core/feat_vouchers_test.gd @@ -174,10 +174,10 @@ func _init() -> void: Kit.check_quiet(safe_cells.size() == 2, "保险 rig:两枚安全格定位(去重闸按格实例——限次样本须异格)〔卡② R2〕") var rows_orch: Array[Dictionary] = [] orch.step_row.connect(func(row): rows_orch.append(row)) # step_row 公开信号面(insurance_used 载荷/seal_miss highlight=豁免账与误标账观测面) - Kit.check_quiet(int(orch._perfect_hit_false) == 0 and int(orch._perfect_forgiven) == 0, + Kit.check_quiet(int(orch._facts.perfect_hit_false) == 0 and int(orch._facts.perfect_forgiven) == 0, "保险前置:误标账与豁免账双零起〔卡② R2〕") - orch._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1) - Kit.check_quiet(int(orch._perfect_hit_false) == 1, + orch._rounds._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1) + Kit.check_quiet(int(orch._facts.perfect_hit_false) == 1, "计数纯净:首次标错 _perfect_hit_false 落 1 未被回退改写(契约 2)〔卡② R2〕") var ins_row := {} for r in rows_orch: @@ -187,7 +187,7 @@ func _init() -> void: "豁免一次:首次标错 _perfect_forgiven=1(forgive_seal_miss 派发)〔卡② R2〕") Kit.check_quiet(int(orch.action_economy.pending_ap_delta()) == 1, "事件不吞:同一标错的 seal_missed 仍被危险直觉·守收到(queue AP+1)〔卡② R2〕") - orch._grant_seg._judge_seal({"cell": safe_cells[1], "hit": false}, 2) + orch._rounds._grant_seg._judge_seal({"cell": safe_cells[1], "hit": false}, 2) var ins_count := 0 var miss_hl_last := "" for r in rows_orch: @@ -199,11 +199,11 @@ func _init() -> void: "限次:第二次标错不获豁免(forgiven 恒 1——when counter lt 1)〔卡② R2〕") Kit.check_quiet(not orch.perfect_sweep_earned(), "判定相减:误标 2 − 豁免 1 = 有效 1 → 完美不成立〔卡② R2 真值表〕") - orch._perfect_hit_false = 1 - orch._perfect_hit_true = 1 + orch._facts.perfect_hit_false = 1 + orch._facts.perfect_hit_true = 1 Kit.check_quiet(orch.perfect_sweep_earned(), "判定相减:误标1−豁免1=有效0且标中≥1→完美成立(保险每战限次一次迁移保绿)〔无状态化 V7〕") - orch._perfect_hit_true = 0 + orch._facts.perfect_hit_true = 0 Kit.check_quiet(not orch.perfect_sweep_earned(), "判定守卫:标中数 0 时零误标也不成立(整图 ≥1 标中条件零松动)〔卡② R2 真值表〕") var rig_rng2 = RngScript.new() @@ -218,7 +218,7 @@ func _init() -> void: var rows_orch2: Array[Dictionary] = [] orch2.step_row.connect(func(row): rows_orch2.append(row)) # step_row 公开信号面(insurance_used 载荷=豁免账观测面) var sc2 = safe_cells[0] - orch2._grant_seg._judge_seal({"cell": sc2, "hit": false}, 1) + orch2._rounds._grant_seg._judge_seal({"cell": sc2, "hit": false}, 1) var ins_row2 := {} for r in rows_orch2: if String(r.get("effect_id", "")) == "insurance_used": @@ -245,7 +245,7 @@ func _init() -> void: if not o10.minesweeper.current_board().is_mine(c10): sc10 = c10 break - o10._grant_seg._judge_seal({"cell": sc10, "hit": false}, 1) + o10._rounds._grant_seg._judge_seal({"cell": sc10, "hit": false}, 1) var sm10: Dictionary = {} for r10 in rows10: if String(r10.get("type", "")) == "seal_miss": @@ -272,14 +272,14 @@ func _init() -> void: if not o11.minesweeper.current_board().is_mine(c11): sc11 = c11 break - o11._grant_seg._judge_seal({"cell": sc11, "hit": false}, 1) + o11._rounds._grant_seg._judge_seal({"cell": sc11, "hit": false}, 1) var sm11: Dictionary = {} for r11 in rows11: if String(r11.get("type", "")) == "seal_miss": sm11 = r11 Kit.check_quiet(String(sm11.get("highlight", "")) == "", "A11 豁免:首次标错获豁免 → seal_miss 行 highlight==空串〔卡① N7〕") - o11._perfect_hit_true = 1 + o11._facts.perfect_hit_true = 1 Kit.check_quiet(o11.perfect_sweep_earned(), "A11 真值:误标 1 − 豁免 1 = 有效 0 且标中≥1 → 完美成立(与 highlight==空 一致)〔卡① N7〕") diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index 0b3360c8..508396dd 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -705,16 +705,16 @@ func _smoke_tree_rules() -> void: Kit.check("守侧 rig:首轮后 queue 干净(开局 posture queue 已兑现)", int(orch.action_economy.pending_ap_delta()) == 0) # 场景①:当回合新封印、结算判标错且封印仍在场 → queue +1(本轮 AP 零变化=queue 承载、下轮 notify_round_start 兑现) - orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 1) + orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 1) Kit.check("守①:当回合标错且封印在场 → 下轮 AP+1(queue 排队——本轮 AP 逐位零变化)", int(orch.action_economy.pending_ap_delta()) == 1) # 场景③④:之前回合标错重判(封印在场状态)与同格重复——去重闸挡回,最多返还一次 - orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 2) - orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 3) + orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 2) + orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 3) Kit.check("守③④:旧封印重判零触发+同格最多一次(_seal_miss_seen 去重闸=「当回合标错动作」判别器)", int(orch.action_economy.pending_ap_delta()) == 1) # 场景②负样本:封印命中(hit=true 正向兑付路径)零触发 - orch._grant_seg._judge_seal({"cell": safe, "hit": true}, 4) + orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": true}, 4) Kit.check("守②负样本:标中/被挖封印零触发(标错事件只在 miss 分支发射——发射面过滤承载)", int(orch.action_economy.pending_ap_delta()) == 1) # 场景⑤:末轮判出→无下一轮=queue 不兑现(编排器每战新建——队列实例随战亡,合法作废态) diff --git a/game/tests/unit/core/settle_segments_test.gd b/game/tests/unit/core/settle_segments_test.gd index e7f23209..1714b14e 100644 --- a/game/tests/unit/core/settle_segments_test.gd +++ b/game/tests/unit/core/settle_segments_test.gd @@ -23,12 +23,15 @@ func _init() -> void: Kit.check_quiet(load("res://core/settlement/decay_segment.gd").new(oc) != null, "V4 DecaySegment 直接 new()〔迭代27 T2〕") Kit.check_quiet(load("res://core/settlement/verdict_segment.gd").new(oc) != null, "V4 VerdictSegment 直接 new()〔迭代27 T2〕") - # V4-2 编排器 _init 接线(段持宿主引用;段序编排面在位) - Kit.check_quiet(oc._grant_seg != null and oc._grant_seg.oc == oc, "V4 编排器接线·授予段(宿主回指)〔迭代27 T2〕") - Kit.check_quiet(oc._aura_seg != null and oc._aura_seg.oc == oc, "V4 编排器接线·增益/减益段〔迭代27 T2〕") - Kit.check_quiet(oc._combat_seg != null and oc._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕") - Kit.check_quiet(oc._decay_seg != null and oc._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕") - Kit.check_quiet(oc._verdict_seg != null and oc._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕") + # V4-2 编排器 _init 接线(段持宿主引用;段序编排面在位)——注册面批同代归约 V1: + # 段件宿主直持改经 _rounds(轮循环驱动件)承载,接线断言改指 _rounds 段族 + Kit.check_quiet(oc._rounds != null and oc._rounds.oc == oc, "V1 编排器接线·轮循环驱动件在座且宿主回指〔编排归约 V1〕") + Kit.check_quiet(oc._facts != null, "V1 编排器接线·战斗事实件在座〔编排归约 V1〕") + Kit.check_quiet(oc._rounds._grant_seg != null and oc._rounds._grant_seg.oc == oc, "V4 编排器接线·授予段(宿主回指)〔迭代27 T2〕") + Kit.check_quiet(oc._rounds._aura_seg != null and oc._rounds._aura_seg.oc == oc, "V4 编排器接线·增益/减益段〔迭代27 T2〕") + Kit.check_quiet(oc._rounds._combat_seg != null and oc._rounds._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕") + Kit.check_quiet(oc._rounds._decay_seg != null and oc._rounds._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕") + Kit.check_quiet(oc._rounds._verdict_seg != null and oc._rounds._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕") # counter_add 写口真实路径(geologist 订阅行承载:公共命令链 begin_battle→move→dig→stop # 经授予段 counter_add 单点落 counters_ctx battle 桶——受理载荷+公开读数口 fx_host().counters_ctx) var rng_c = load("res://core/rng_service.gd").new() diff --git a/game/tests/unit/core/treasure_effects_test.gd b/game/tests/unit/core/treasure_effects_test.gd index 4f232f6a..fdceacb8 100644 --- a/game/tests/unit/core/treasure_effects_test.gd +++ b/game/tests/unit/core/treasure_effects_test.gd @@ -162,13 +162,13 @@ func _init() -> void: "线香:装配起战(battle 级充能 1)") var inv8 = load("res://core/treasure_items.gd").new(db) var rec8: Dictionary = inv8.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0) - ob8.treasure_inventory = inv8 + ob8.reward_grant().treasure_inventory = inv8 var h8: int = int(rec8.get("handle", -1)) # handle 取 add_item 返回(snapshot 投影无 handle 键) var tgt8: Vector2i = _first_closed_safe(ob8) Kit.check_quiet(tgt8 != Vector2i(-9, -9), "线香:夹具存在未开安全格") var v8: Dictionary = ob8.reward_grant().use_treasure(h8, tgt8) Kit.check_quiet(bool(v8.get("ok", false)), "线香正常:单格真视受理(cell 目标解析)") - Kit.check_quiet(bool(ob8.treasure_inventory.query_overlays().get("mine_reveal", []).has(tgt8)) + Kit.check_quiet(bool(ob8.reward_grant().treasure_inventory.query_overlays().get("mine_reveal", []).has(tgt8)) and ob8.minesweeper.is_revealed(tgt8) == false, "线香断言:目标格入真视叠加层且未被开格(真视≠开格——信息红线)") Kit.check_quiet(inv8.snapshot_inventory().is_empty() or bool(inv8.snapshot_inventory()[0].get("used", false)), @@ -240,7 +240,7 @@ func _init() -> void: "天眼:装配起战(level 级充能 1)") var inv11 = load("res://core/treasure_items.gd").new(db) var rec11: Dictionary = inv11.add_item("sky_eye", "attunement", Vector2i(-1, -1), 0) - ob11.treasure_inventory = inv11 + ob11.reward_grant().treasure_inventory = inv11 var h11: int = int(rec11.get("handle", -1)) var tgt11: Vector2i = _first_closed_safe(ob11) var v11: Dictionary = ob11.reward_grant().use_treasure(h11, tgt11) @@ -254,7 +254,7 @@ func _init() -> void: if ob11.minesweeper.is_revealed(c): opened_safe = false Kit.check_quiet(mines_shown == 0 or opened_safe, "天眼反例①:危险格仅显示不被开格(棋盘事实不可变)") - var overlay11: Array = ob11.treasure_inventory.query_overlays().get("mine_reveal", []) + var overlay11: Array = ob11.reward_grant().treasure_inventory.query_overlays().get("mine_reveal", []) var overlay_ok := true for c_v in (v11.get("resolved", []) as Array): if not overlay11.has(c_v): @@ -289,7 +289,7 @@ func _init() -> void: "计数链:装配起战(GainJournal 携带)") var inv13 = load("res://core/treasure_items.gd").new(db) var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0) - ob13.treasure_inventory = inv13 + ob13.reward_grant().treasure_inventory = inv13 var h13: int = int(rec13.get("handle", -1)) var tgt13: Vector2i = _first_closed_safe(ob13) if tgt13 != Vector2i(-9, -9): @@ -306,8 +306,13 @@ func _init() -> void: "装配不可变:双件装配冒险启动") Kit.check_quiet(adr14.runner().pick_next_node(int(adr14.runner().next_candidate_ids()[0])), "装配不可变:进首战") var ob14 = adr14.runner().current_battle() - Kit.check_quiet(ob14 != null and (ob14.attunement as Array) == ["morning_order", "wide_scout_band"], + Kit.check_quiet(ob14 != null and (ob14._facts.attunement as Array) == ["morning_order", "wide_scout_band"], "装配不可变:战斗运行态装配集与冒险装配集逐位同源(运行零写口——快照对比)") + # 注册面批同代编排归约 V5:库存注入改址(level_runner 注入改经 reward_grant() 出口直注)后 + # 引用同一性——战斗域经出口持有的库存与关卡域同一实例;use_treasure 受理链经新地址走通 + # 由本套件 ob8/ob11/ob13 三段既有受理链承载(注入面同批改址后全绿即证据)。 + Kit.check_quiet(ob14 != null and ob14.reward_grant().treasure_inventory == adr14.runner().treasure_inventory, + "库存注入改址:战斗域经 reward_grant 出口持有的库存与关卡域同一引用〔编排归约 V5〕") # ⑮ 扩展行收敛(迭代 38 卡④ wr2④⑤——套件首次 feats 注入,V6 形态自 round_flow_skill_pool_test 移植: # enc 带 feats 键+ledger,专长触发经 _judge_seal 真实封印判定管线) @@ -332,7 +337,7 @@ func _init() -> void: Kit.check_quiet(mines15.size() == 3, "封印匠:板上存在三枚危险格(断言前提)") if mines15.size() == 3: for mv in mines15: - ob15._grant_seg._judge_seal({"cell": mv, "hit": true}, 1) + ob15._rounds._grant_seg._judge_seal({"cell": mv, "hit": true}, 1) var smith_row := {} for r in rows15: if String(r.get("effect_id", "")) == "mine_neutralized": @@ -354,7 +359,7 @@ func _init() -> void: cell15b = cv break if cell15b != null: - ob15b._grant_seg._judge_seal({"cell": cell15b, "hit": true}, 1) + ob15b._rounds._grant_seg._judge_seal({"cell": cell15b, "hit": true}, 1) var beacon_row := {} for r in rows15b: if String(r.get("effect_id", "")) == "fx_preview_area": From 4e04d2f1ba17ac6af7b74a5acdb6b08b9e5c3f23 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 30 Oct 2026 07:13:23 +0800 Subject: [PATCH 3/3] =?UTF-8?q?iter44-=E5=8D=A13=EF=BC=9A=E5=8D=A1?= =?UTF-8?q?=E2=91=A2(=E6=B5=8B=E9=80=9F=E5=BE=AA=E7=8E=AF=E6=89=B9v1.1):?= =?UTF-8?q?=20=E4=BF=A1=E5=8F=B7=E6=AD=A5=E8=BF=9B=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=9C=BA=E6=B6=88await-in-loop=E8=A3=B8=E9=94=AE=C2=B7?= =?UTF-8?q?=E5=8F=8C=E5=AE=9E=E7=8E=B0=E5=AF=B9=E6=8B=8D=C2=B7=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA=E6=94=B9=E5=8A=A8=E9=9B=B6=E5=A4=84=C2=B7=E5=90=AB?= =?UTF-8?q?=E5=B9=B6master=E4=B8=8E=E4=B8=89=E5=8D=A1=E4=BA=A7=E5=87=BA?= =?UTF-8?q?=E7=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/art/art-assets-checklist.md | 2 +- design/art/art-prompts.md | 6 +- design/art/remote-gen-prompts.md | 2 +- .../engineering/architecture/architecture.md | 4 +- design/engineering/process-conventions.md | 2 +- design/engineering/test-conventions.md | 2 +- game/presentation/tune_sim_controller.gd | 50 +++- game/probe/ui_input_probe.gd | 4 +- game/tests/.gates/baseline.json | 10 +- game/tests/README.md | 4 +- game/tests/run_all.gd | 1 + .../unit/core/meta_progression_forge_test.gd | 6 +- game/tests/unit/core/run_time_fatal_test.gd | 2 +- .../tune_sim_batch_parity_test.gd | 245 ++++++++++++++++++ reports/iter44-反哺点-2026-10-30.md | 26 ++ reports/iter44-实现报告-2026-10-30.md | 118 +++++++++ reports/iter44-验收审计-2026-10-30.md | 99 +++++++ reports/需求卡片-测速循环批-2026-10-29.md | 94 +++---- tools/render_gate.sh | 16 ++ tools/run_all_gates.sh | 21 +- 待办清单.md | 78 +++--- 21 files changed, 655 insertions(+), 137 deletions(-) create mode 100644 game/tests/unit/presentation/tune_sim_batch_parity_test.gd create mode 100644 reports/iter44-反哺点-2026-10-30.md create mode 100644 reports/iter44-实现报告-2026-10-30.md create mode 100644 reports/iter44-验收审计-2026-10-30.md diff --git a/design/art/art-assets-checklist.md b/design/art/art-assets-checklist.md index b6a3e98f..2aac8c7f 100644 --- a/design/art/art-assets-checklist.md +++ b/design/art/art-assets-checklist.md @@ -131,7 +131,7 @@ ### 3.5 营地场景图 - **要什么**:营地场景图 9 张——`scene_camp_bg`(背景)+ `scene_camp_gate / forge / chest / notebook / dummy / board / campfire`(七个可点物件)+ `scene_camp_tent`(承载件)。钥石顶栏图标沿用现役 `icon_keystone`。 -- **来源/规格**:**✦ 插画级**(作者自跑远程生图管线);烛光黄 `#F6C667` + 木色 `#8A5A34`(art-bible §6.2);2× 源档出图、最近邻降采样到 1280×800;后处理=抠底/降采样/色板量化/描边统一。**每张出图须登记模型名+分辨率+seed+对话/日期**。 +- **来源/规格**:**✦ 插画级**(作者自跑远程生图管线);烛光黄 `#F6C667` + 木色 `#8A5A34`(art-bible §6.2);2× 源档出图、最近邻降采样到 1280×800;后处理=抠底/降采样/色板量化/描边统一。**出图登记照 [remote-gen-prompts](remote-gen-prompts.md) 头注轻量口径执行:用途+生成时间必注,其余参数有则记。** - **排期**:营地交互重做**美术自立批**(不占迭代位;UI 贴图重构批已搁置)。终稿落 `game/assets/art/scene/`,消费常量 `UiKit.TEX_CAMP_*`。 - **现状**:全占位。 diff --git a/design/art/art-prompts.md b/design/art/art-prompts.md index 76348109..ca2d2c3e 100644 --- a/design/art/art-prompts.md +++ b/design/art/art-prompts.md @@ -1,4 +1,4 @@ -# 生图提示词卡(角色级资产)v2.10 +# 生图提示词卡(角色级资产)v2.11 > **性质**:美术清单生图通道(✦ 用户自跑,现走线上 API)的执行侧文件——每资产一段**自包含完整提示词**:正向 = 通用底座+主体+动作已拼妥、负向 = 通用底座+增补已并妥,整段复制直接贴进工作流即可,**不需要读其他文件、不需要拼装**。人物规格转写自 [character-design.md](character-design.md)(人物设计 v1.0,自 art-bible §5 迁出,圣经 §5 为存根),风格/色彩条款转写自 [art-bible](art-bible.md)(§4.1 主色板 / §4.2 语义色规则);冲突时人物规格以 character-design 为准、色彩以 art-bible 为准。 > **边界**:本文件只管**角色级**资产的生图通道。图标/格面/样张走脚本管线(⚙ 代理出),规格在 art-bible §4.6 与 [art-assets-checklist](art-assets-checklist.md),不需要提示词。**场景级资产(营地场景图的背景层与八个物件层)另立 [camp-scene-prompts](camp-scene-prompts.md),本文件不覆盖**;两卡的角色/场景分工以「谁的资产谁出卡」为界,同一段提示词不两处维护。战斗舞台的场景级两件与护甲类型图标三枚的**出图工单**登记在本文件 §5(camp-scene-prompts 只管营地场景图,战斗屏这两类无处可落);§5 只把各权威节的定值收进逐件工单,不立新值。意图动画三档是动画资产,等一屏样张锁方案后另立规格卡,本文件不覆盖。 @@ -289,7 +289,7 @@ Seamless tileable pixel art texture of dry dirt ground, top-down, for a dungeon > **范围注**:本文件原定位角色级(见文件头边界句),§4bis 已有「地形类同库登记」的先例,本节循同一先例补登记战斗屏两类——战斗舞台的场景级两件(① ②)与护甲类型图标三枚(③),两组在营地卡与清单里都无处落工单;角色级立绘重产(④)是本文件的本职。四组的规格定值清单 §4 已有登记,本节补的是「逐件照着就能出件」那一层。 > **定值纪律(§8 头注 定值单点纪律)**:本节**指路不立值**——色值 = 圣经 §4.1(含场景环境子板)、尺寸与缩放 = §8.2、明度带 = §2、甲档 = character-design §5.3、命名 = §8.3 + asset-path-map。本节新增零定值;凡圣经未定的值,一律进本节 §5.5 缺口,不在本节填空。 > **验收标注纪律**:逐条按三档标 `←机检: <断言>` / `←目检: <判据>` / `←方向性`,照圣经头注 2026-10-06 用户拍板的写法。 -> **流程**:出图前后一律走 §0 五步;出图登记义务(模型名 + 分辨率 + seed + 对话/日期)照 remote-gen-prompts 头注,随候选图存 `artwork/candidates/<资产名>/`,缺登记的候选不入正稿遴选。 +> **流程**:出图前后一律走 §0 五步;出图登记义务照 remote-gen-prompts 头注轻量口径(用途+生成时间必注,其余参数有则记),随候选图存 `artwork/candidates/<资产名>/`;登记不作正稿遴选前置。 > **产出状态**:组二地台件与组三护甲三枚已按各节 ⑤「程序化产出即终稿」产出入包(`scene_stage_plaza.png` 48×12、`ui_armor_light/ward/heavy.png` 24×24,路径见各组 ① 字段;登记=战斗一屏样张批,合并提交 3b66d1a6);组一背景插画与组四立绘重产仍待出图。 ### 5.1 组一 · B 区舞台背景插画(1 件) @@ -445,7 +445,7 @@ Do not add any of the following: realistic, photorealistic or 3D rendering; pain 1. 出件顺序 = 先主角、再三 Boss(Alpha 范围 = 主角 + 三 Boss 重产,其余敌种当前可读则保留到 Beta)。 2. **源密度判据 = 三判据②**:显示 96–188px 下脸、铲、旗、物种可认——照这条反推源档即可,逐键定档已完成(§8.2),本批不必再猜档位。 3. 手绘起稿顺序 = 剪影(§5.1 archetype)→ 体块主色 → 暗部与受光 → 描边 → 落影底盘。 -4. 逐件 2–3 版候选;登记义务项 = 手绘者 / 日期 / 源档尺寸(手绘件无模型名与 seed,按此三项替换 §0 的登记项),随候选图存 `artwork/candidates/<资产名>/`。 +4. 逐件 2–3 版候选;登记照 remote-gen-prompts 头注轻量口径(用途+生成时间必注,手绘作者有则记),随候选图存 `artwork/candidates/<资产名>/`。 5. 候选评审 = 并排横评,**隐去文件名与来源后按三判据逐条判读**(§0 五步第 1 步「便于横评」的手绘侧落法),不靠记忆与印象拍板。 6. 出件 = 透明 PNG(Alpha 直出,禁先出白底图再抠);入包前过 §8.3 色板检查器(报新增色 / 近色告警 / 超 48 上限拦截)。 diff --git a/design/art/remote-gen-prompts.md b/design/art/remote-gen-prompts.md index 29ed0880..394440d0 100644 --- a/design/art/remote-gen-prompts.md +++ b/design/art/remote-gen-prompts.md @@ -2,7 +2,7 @@ > **渠道定位**:生图渠道 = **远程对话式生图为主,本地 ComfyUI 为备选**;本文件是**对话操作单**(怎么开对话/贴什么参考图/怎么续写),提示词正文词库归 [art-prompts](art-prompts.md)——分工:词库改词库、操作单改流程,同一段正文不双处维护。 > **冲突优先级**:画法与色彩以 [art-bible](art-bible.md) 为准,人物规格以 [character-design](character-design.md) 为准;本单为执行侧操作单,不另立规格。 -> **出图登记义务(复现性)**:每张产出登记 **模型名 + 分辨率 + seed(可取则取)+ 对话/日期**,随候选原图同存 `artwork/candidates/<资产名>/`;缺登记的候选不入正稿遴选。 +> **出图登记义务(出处备查)**:每张产出随图注明 **用途**(备选/参考/其他什么用)与 **生成时间**(YYYY-MM-DD HH:MM 格式),随候选原图同存 `artwork/candidates/<资产名>/`;模型名、seed、分辨率等参数有则随手记、无则不填,分辨率这类文件属性里自带的值不必抄。登记不作正稿遴选前置——缺注明不落选,遴选时问清用途即可。 > 开新对话出图:同一角色 2–3 版候选各开新对话(同对话续写=修图);同角色换姿势在同对话续写。分辨率开大档;候选原图存 `artwork/candidates/<资产名>/`。 ## 1. 主角战斗立绘 diff --git a/design/engineering/architecture/architecture.md b/design/engineering/architecture/architecture.md index a169a01a..fd60bb59 100644 --- a/design/engineering/architecture/architecture.md +++ b/design/engineering/architecture/architecture.md @@ -238,7 +238,7 @@ | **run_system_test**(系统测试运行器) | `tools/run_system_test.py` | 系统测试总入口:headless 跑真实玩法路径断言面(宿主=`res://tests/system_test.gd`)+ 三闸判定(口径同 run_smoke,摘要行兼容两种宿主格式) | 进程退出码(0=PASS / 1=FAIL) | 三闸齐备(同 run_smoke:退出码/摘要失败数/脚本错误扫描;wall-clock 超时在位) | | **run_all**(单元与集成测试) | `game/tests/run_all.gd` | 64 套统一入口(61 单元+3 集成,2026-10-28 实数):套件 manifest 逐条「套件 → 期望退出码」,致命类 expect=1 独立子进程实跑;`negative` 字段标负面套件(仅豁免错误扫描一闸);选跑态 `--only=<词[,词…]>`(直跑形态对套件路径子串匹配只跑命中套件,零命中=用法错 quit(2),仅供开发期最窄确认——不产闸回执、不作验收凭据,批末全量闸不经选跑分支) | 逐套件结果 + 汇总退出码(任一套件失败 quit(1)) | 套件级退出码协议在位;三硬闸齐备(入口内扫子套件输出 + 编排层扫 `unit.log`;`Kit.finish` 零断言返回专用码 2)。子进程输出须经系统命令解释器重定向落盘再读——`OS.execute` 的输出管道在本机恒空 | | **probe_gate**(场景探针) | `tools/probe_gate.sh` | headless 场景探针段编排:默认段 ui_input_probe / ui_struct_probe / layout_probe / ui_contrast_probe / **screen_flow_probe**(屏壳切换),`--click` 加跑 ui_click_probe,`--full` 加跑 ui_probe / ui_tune_probe / t17_defer_probe / fx_probe;孤儿 Godot 预清 + 批末残留清点(必须 0) | 段级 FAIL 输出 + 汇总退出码 | 每段 timeout 守卫在位(挂死 124/137 = FAIL,不静默);判定统一走 `verdict` 函数——退出码为唯一权威判据,PASS 串降级为附加判据,逐段跑 `tools/error_scan.py` | -| **render_gate**(视觉 golden) | `tools/render_gate.sh` | 窗口化渲染闸:①shot_probe 七屏截图 → shot_checker A-D 语义断言;②fx_shot_probe / board_shot_probe 取证(超时/零字节/文件缺失任一 = FAIL);③棋盘 golden 逐像素容差比对(差 ≤16 / changed ≤2%,provisional)+ 菜单 golden;④批末孤儿残留清零 | 段级 FAIL 输出 + 汇总退出码 | 查退出码在位(每段 timeout 守卫 + 退出码/取证文件双检);**SCRIPT ERROR 扫描缺失**;断言数下限不适用(判定主体 = golden 容差与取证文件存在性,非断言计数) | +| **render_gate**(视觉 golden) | `tools/render_gate.sh` | 窗口化渲染闸:①shot_probe 七屏截图 → shot_checker A-D 语义断言;②fx_shot_probe / board_shot_probe 取证(超时/零字节/文件缺失任一 = FAIL);③棋盘 golden 逐像素容差比对(差 ≤16 / changed ≤2%,provisional)+ 菜单 golden;④批末孤儿残留清零;⑤三段探针日志逐段脚本错误扫描 | 段级 FAIL 输出 + 汇总退出码 | 查退出码在位(每段 timeout 守卫 + 退出码/取证文件双检);SCRIPT ERROR 扫描在位(逐段 `tools/error_scan.py`,标记与 run_smoke 同源);断言数下限不适用(判定主体 = golden 容差与取证文件存在性,非断言计数) | | **static_scan**(静态扫描域) | `game/tests/static_scan.sh` | 静态扫描域 S1–S10 可执行载体(grep 规则 + `tools/audit_type_hints.py`〔S9〕+ `tools/audit_file_headers.py`〔S10〕) | fail 聚合退出码(任一硬失败规则命中即 exit 1) | 查退出码在位;SCRIPT ERROR 扫描与断言数下限不适用(逐规则命中即红,无运行时错误面与断言计数);S3 现为 warning 级、不参与 fail 判定(见下表) | ### 改动域划分(2026-10-17 用户裁「代码批只跑代码闸、文档批只跑文档闸」) @@ -267,7 +267,7 @@ | 硬闸 | run_smoke | run_system_test | run_all | probe_gate | render_gate | static_scan | |---|---|---|---|---|---|---| | **退出码优先** | 已落实(闸①) | 已落实(闸①,口径同 run_smoke) | 已落实(套件 manifest expect 协议) | 已落实(`verdict` 函数逐段判退出码,PASS 串不作判据) | 已落实(段级退出码 + 取证双检) | 已落实(fail 聚合退出) | -| **SCRIPT ERROR 扫描** | 已落实(闸③,外部扫描——GDScript 无脚本错误陷阱 API,闸设在外部包装) | 已落实(闸③,标记清单与 run_smoke 同源) | 已落实(入口内扫子套件输出;子进程的 stdout/stderr 须先落盘——`OS.execute` 管道在本机恒空) | 已落实(逐探针段 `tools/error_scan.py`,标记与 run_smoke 同源) | **缺失**(本批未收——判定主体是 golden 容差与取证存在性;登记待办清单) | 不适用(静态域无运行时错误面) | +| **SCRIPT ERROR 扫描** | 已落实(闸③,外部扫描——GDScript 无脚本错误陷阱 API,闸设在外部包装) | 已落实(闸③,标记清单与 run_smoke 同源) | 已落实(入口内扫子套件输出;子进程的 stdout/stderr 须先落盘——`OS.execute` 管道在本机恒空) | 已落实(逐探针段 `tools/error_scan.py`,标记与 run_smoke 同源) | 已落实(三段探针日志逐段 `tools/error_scan.py`,标记与 run_smoke 同源) | 不适用(静态域无运行时错误面) | | **断言数下限** | 无独立下限(闸②摘要行必须在案,防「跑一半仍 PASS」;脚本错误吞断言路径由闸③兜住) | 同 run_smoke(过渡期宿主摘要行必须在案) | 已落实(`Kit.finish` 零断言返回专用退出码 2——与「断言失败=1」区分,防致命类套件的 expect 协议放过) | 已落实(退出码 0 但无该探针签名标记亦判红——防静默退出) | 不适用(golden 容差 + 取证存在性判定) | 不适用(逐规则命中判定;另有扫描面为空即判红的零命中自检) | 背景事实(run_smoke 头注在案):GDScript 运行时脚本错误不改进程退出码、不计入冒烟失败数、且静默中断所在函数剩余断言——2026-09-19 状态记录 83 实证「309 项 0 失败」实为断言未跑全。这是 SCRIPT ERROR 扫描必须存在于判定层的原因。 diff --git a/design/engineering/process-conventions.md b/design/engineering/process-conventions.md index 727fc342..04273dd3 100644 --- a/design/engineering/process-conventions.md +++ b/design/engineering/process-conventions.md @@ -48,7 +48,7 @@ 7. **验收改动先行声明**——验收变化(基线换代/探针增改/旧断言改判)预先写进需求卡片,禁事后补 **完成** -8. **批末按域验收才合并(worktree 批;直改车道无批末动作,验收在 commit 点由 git 钩子执行,见 §1 第 3 条)**——在任务 worktree 内跑 `tools/run_all_gates.sh`,**按本批改动域选档**:按文件性质判——代码面=`game/` 下非 `.md` 的行为载体(代码、测试脚本、探针、场景、数据表、素材)→ `--code`;文档件=`game/` 内 `.md`(README、生成清单)与 `game/` 外一切路径(`design/`、根规则文件、`tools/` 工具脚本、`.zcode/` 配置等)→ `--docs`;两类同批都有 → 全量档不带参数。代码域按**关联档选测**(2026-10-29 用户裁):02 单元与集成只跑「需求卡片申报 ∪ diff 推导」并集的套件——申报=在册需求卡片对照表「验收测试」列提到的测试文件名,推导=闸脚本内套件族映射表按改动文件归族;改动命中公共件/数据表/autoload/数值·存档·RNG·结算触面或映射不到 → 横切回落全部套件;00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降;03/04 探针与 05 渲染闸维持影响面口径(本批 game/ 改动全在测量面 game/tests、game/sim 时记 SKIP)。所选域全绿即可合回 master,全量回归由用户自发发起(合入后无自动兜底)。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。该脚本是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。跑完写回执 `game/tests/.gates/receipt.json`(含 `domain`、`scope`(full|related,02 跑子集=related)与 `dirty_game` 字段),合回时由 worktree-guard 按 diff 文件性质推域强制校验(六况拦截:缺回执/quick 模式/回执未覆盖分支尖=回执 commit 之后代码面又有改动、或跑闸时代码面带着未提交改动(`dirty_game`)/域不覆盖本次改动/含新增红/master HEAD 非回执 commit 祖先=合并将产出未测内容;guard 只验域覆盖与全绿,不复核选套件清单)。回执与分支尖的对齐判据是**代码面**不是 commit 号:回执之后只追加文档件(`game/` 内 `.md`、`reports/`、`design/` 等)提交,回执照样有效,不构成复跑理由。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占合回全量——其质量义务=落批时人工确认闸可跑通且判据口径未变,闸真崩溃时合回闸 fail-closed(跑不出回执即拦),不以合回全量为兜底 +8. **批末按域验收才合并(worktree 批;直改车道无批末动作,验收在 commit 点由 git 钩子执行,见 §1 第 3 条)**——在任务 worktree 内跑 `tools/run_all_gates.sh`,**按本批改动域选档**:按文件性质判——代码面=`game/` 下非 `.md` 的行为载体(代码、测试脚本、探针、场景、数据表、素材)→ `--code`;文档件=`game/` 内 `.md`(README、生成清单)与 `game/` 外一切路径(`design/`、根规则文件、`tools/` 工具脚本、`.zcode/` 配置等)→ `--docs`;两类同批都有 → 全量档不带参数。代码域按**关联档选测**(2026-10-29 用户裁):02 单元与集成只跑「需求卡片申报 ∪ diff 推导」并集的套件——申报=在册需求卡片对照表「验收测试」列提到的测试文件名,推导=闸脚本内套件族映射表按改动文件归族;改动命中公共件/数据表/autoload/数值·存档·RNG·结算触面或映射不到 → 横切回落全部套件;00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降;03 场景探针与 05 渲染闸维持影响面口径(本批 game/ 改动全在测量面 game/tests、game/sim 时记 SKIP)。所选域全绿即可合回 master,全量回归由用户自发发起(合入后无自动兜底)。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。该脚本是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。跑完写回执 `game/tests/.gates/receipt.json`(含 `domain`、`scope`(full|related,02 跑子集=related)与 `dirty_game` 字段),合回时由 worktree-guard 按 diff 文件性质推域强制校验(六况拦截:缺回执/quick 模式/回执未覆盖分支尖=回执 commit 之后代码面又有改动、或跑闸时代码面带着未提交改动(`dirty_game`)/域不覆盖本次改动/含新增红/master HEAD 非回执 commit 祖先=合并将产出未测内容;guard 只验域覆盖与全绿,不复核选套件清单)。回执与分支尖的对齐判据是**代码面**不是 commit 号:回执之后只追加文档件(`game/` 内 `.md`、`reports/`、`design/` 等)提交,回执照样有效,不构成复跑理由。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占合回全量——其质量义务=落批时人工确认闸可跑通且判据口径未变,闸真崩溃时合回闸 fail-closed(跑不出回执即拦),不以合回全量为兜底 9. **合并后主区零复跑**(2026-10-24 用户裁:合回重跑验的是与回执同一份字节,零信息)——合回落地后 code 批只做两件秒级事:`--editor --quit` 重刷类名缓存(在册坑:.godot 缓存不入 git,与合并后代码失配会假红假绿)+冒烟启动确认可开机(合计约 1 分钟);docs 批零动作。全量验证**每批跑一次**=worktree 批末按域闸,顺序是「先并入 master、再跑闸」(§1 第 2 条);只有两种情形允许把这次跑作废重来——① 闸红且修法动了 `game/`,② 闸已开跑后 master 又被推进(合并将产出未测内容,由合回闸祖先判定拦下,§3.9 第六况)。「跑都跑了再跑一遍保险」「闸后顺手改点 `game/` 再补一跑」都不构成复跑理由;master 上永不跑全量 10. **批次完成后的登记义务**——合回 master 后按 doc-conventions §12.1 推进待办清单条目状态(做完整条删除),涉及迭代编排的在 roadmap 迭代总表与顶部「当前位」节换代——执行入口=项目技能 batch-closure(批后收尾与中间文档退役),跨会话教训写记忆库。批次过程与结论由 git 提交信息承载,禁止再写会话状态文件(见 doc-conventions 第 10 节)。`design/` 与根规则文件(`AGENTS.md`/`roadmap.md`/`待办清单.md`/`体验验证清单.md`)实现批禁写,见 §12 11. **文档同步闸(实现批禁写文档)**——实现批**禁写 `design/` 整个目录与根规则文件**,**另禁写 `game/tests/README.md`(能力清单,归写卡段维护,test-conventions §8.9)**,要往外交代的东西一律落 `reports/`:实现报告 `reports/iter-实现报告-<日期>.md`(各卡状态+实机目检总清单+遗留读数义务+实现决定记录——超白名单/超卡片明列范围事项逐条记「决了什么/为什么/代价/撤销方式」,全部按卡执行时只写一行「零拍板」)、GDD 反哺点清单 `reports/iter-反哺点-<日期>.md`(每条写清哪个文件哪一节、改成什么、依据是什么)。设计权威文档(GDD/art-bible)**任何批都不直改**:用户在会话中逐点裁决后,由工程/文档设计改动批(直改类)写入 `design/`(执行入口=doc-design 技能),并把反哺条目从 `reports/` 清掉(吸收即死;退役执行=batch-closure 技能)。机械面文档(roadmap 行/术语表/README 工具注)同样归工程/文档设计改动批在合并前更新。反哺点未落地期间相关 GDD 节视为待反哺,读前先查 `reports/` 与待办清单;需求卡片必列「关联文档同步面」按两层标注 diff --git a/design/engineering/test-conventions.md b/design/engineering/test-conventions.md index c89d61a8..f82fde02 100644 --- a/design/engineering/test-conventions.md +++ b/design/engineering/test-conventions.md @@ -67,7 +67,7 @@ - 「跑一遍没崩=通过」无效:测试绿只证明**当前代码没犯这个错**,不证明**犯了这个错会被抓住**;断言存在不等于断言执行。 - **脚本错误会静默中断所在函数的剩余断言,而套件仍报全绿**(机制事实):GDScript 无脚本错误陷阱 API,错误不改进程退出码、不计失败数。三处实证:①2026-09-19「309 项 0 失败」实为断言未跑全(状态记录 83);②敌人表行集纪律 10 条断言因定型数组赋值错误从未执行(迭代 29 修复后两条正向见红=夹具本缺锚行);③`Callable` 形参类型写错致解析函数返回 null、后续断言整段蒸发。 - 由此产生两条义务:**写闸件**按 §6 配三硬闸;**写验收函数**必须自证断言全数执行——真实入口断言在当轮执行并通过(§8.4)为主证,零命中自检(扫描面为空即判红)为辅证。 -- **覆盖率不是目标**:行覆盖率度量缺失(Godot 无内建,第三方属新基建,是否引入待办清单在册待裁);现行补洞=零指名单测清单+人工判定,判定可靠性靠上列自证手段,不靠覆盖率数字。 +- **覆盖率不是目标**:不引入行覆盖率度量(Godot 无内建,第三方属新基建);现行补洞=能力清单覆盖域表+人工判定,判定可靠性靠上列自证手段,不靠覆盖率数字。 ## 8. 改动级验收闭合(2026-10-21 立) diff --git a/game/presentation/tune_sim_controller.gd b/game/presentation/tune_sim_controller.gd index db2c5601..8a6695b3 100644 --- a/game/presentation/tune_sim_controller.gd +++ b/game/presentation/tune_sim_controller.gd @@ -2,16 +2,28 @@ ## 调整板批量模拟控制器:逐次驱动关卡自动策略(与 headless 扫描同口径),进度/报告经面板 setter 注入。 ## 零核心写——表基线复位由组合根在移交前完成,加成施加只走自动策略内部 runner.set_hero_override 单路径。 ## presentation 不 preload smoke:驱动实例由调用方注入(须供与 level_autoplay 同款 run_level 签名),缺省 null=运行时 load 兜底。 +## 逐帧步进:每局恰占一帧——第 0 局在调用点同步段执行,第 i 局(i≥1)在第 i 帧信号发射点执行,收尾恒在第 n 帧发射点; +## n=0 退化路径为调用点同步收尾零帧。 +## _batch_finished 信号契约:整批收尾放行——发射时点=报告注入与运行态复位之后;消费方=run() 自身收尾放行, +## 外部 await run(n) 经此挂起至整批完成(协程身份契约)。 extends RefCounted const SEED_BASE := 4200 # 模拟种子基(与历史实现同源——同种子可回放) const PlaytestLogger := preload("res://presentation/playtest_logger.gd") # 试玩日志(同层依赖——运行时 load 收敛为 preload 常量) +signal _batch_finished + var db var panel # 调试浮窗 setter 面:atk_pct/hp_pct/set_sim_running/set_sim_progress/set_sim_report var _base_hp := 0 # 报告中的"基础 HP"口径注记值 var _driver = null # 关卡自动策略驱动(调用方注入;null = 旧行为运行时 load 兜底) var _running := false # 运行态(run 全程 true——组合根互查守卫用,与面板置灰解耦) +var _sim_n := 0 # 本批局数(run 期活成员——批末失效、下次 run 重写) +var _sim_i := 0 # 已执行局数(步进计数) +var _sim_stats: Dictionary[Variant, Variant] = {} # stats 六键束(run 期承载) +var _sim_autoplay = null # 自动策略驱动实例(run 期承载) +var _sim_logger = null # 共享 JSONL 日志会话(run 期承载) +var _finished := false # 批完成旗标(_finish_batch 置位——收尾段同步承载) func _init(db_ref, panel_ref, base_hp: int, driver = null) -> void: @@ -44,12 +56,38 @@ func run(n: int) -> void: panel.set_sim_progress("模拟中:0/%d …" % n) var stats := {"wins": 0, "dmg_all": [], "hp_left_win": [], "hp_boss": [], "rounds_all": [], "death_nodes": {}} var autoplay = _autoplay() - for i in range(n): - _run_one_level(autoplay, stats, i, panel, logger) - panel.set_sim_progress("模拟中:%d/%d …" % [i + 1, n]) - await panel.get_tree().process_frame - logger.close_session() - panel.set_sim_report(HudPresenter.tune_sim_report(n, panel.atk_pct(), panel.hp_pct(), stats, _base_hp)) + _sim_n = n + _sim_i = 0 + _sim_stats = stats + _sim_autoplay = autoplay + _sim_logger = logger + _finished = false + if n > 0: + if not panel.get_tree().process_frame.is_connected(_step_frame): + panel.get_tree().process_frame.connect(_step_frame) # 恰好连接一次(迭代 39 先例同款守卫) + _step_frame() # 首步同步执行(第 0 局在调用栈内跑完——与 await 形态对齐) + await _batch_finished + else: + _finish_batch() # n=0:循环零进同步收尾,不挂起(与 await 形态一致零帧) + + +# 逐帧步进回调:i void: + if _sim_i < _sim_n: + _run_one_level(_sim_autoplay, _sim_stats, _sim_i, panel, _sim_logger) + panel.set_sim_progress("模拟中:%d/%d …" % [_sim_i + 1, _sim_n]) + _sim_i += 1 + else: + panel.get_tree().process_frame.disconnect(_step_frame) # 先断连防再入 + _finish_batch() + _batch_finished.emit() + + +# 整批收尾单点(收尾四语句同帧同相位:close_session→报告→运行态复位——次序与 run() 尾现行形态逐句对齐) +func _finish_batch() -> void: + _finished = true + _sim_logger.close_session() + panel.set_sim_report(HudPresenter.tune_sim_report(_sim_n, panel.atk_pct(), panel.hp_pct(), _sim_stats, _base_hp)) panel.set_sim_running(false) _running = false diff --git a/game/probe/ui_input_probe.gd b/game/probe/ui_input_probe.gd index 71b55f77..fddfb61a 100644 --- a/game/probe/ui_input_probe.gd +++ b/game/probe/ui_input_probe.gd @@ -479,13 +479,13 @@ func _ready() -> void: var p_lantern: Dictionary = main.build_adventure_params(main.save_manager, main.meta) if not (p_lantern.get("attunement", []) as Array).has("wide_scout_band"): _problems.append("case⑮ 装配未进开局参数面(attunement=%s 应含 wide_scout_band)" % str(p_lantern.get("attunement", []))) - main.run_scope.settlement.attunement = ["wide_scout_band"] + main.run_scope.settlement._facts.attunement = ["wide_scout_band"] main._sync_treasure_glow() if int(bv.fog_params_probe()["lantern"]) != 4: _problems.append("case⑮ 装配后视野未扩(lantern 应=4——呈现桥未生效)") if int(bv.cells_probe()[ring_cell].fog_state) != 0: _problems.append("case⑮ 装配后 dist==R+1 格仍被遮(应转可见)") - main.run_scope.settlement.attunement = [] + main.run_scope.settlement._facts.attunement = [] main._sync_treasure_glow() if int(bv.fog_params_probe()["lantern"]) != 0: _problems.append("case⑮ 卸装后视野未复原(lantern 应=0——契约复原语义显式重算)") diff --git a/game/tests/.gates/baseline.json b/game/tests/.gates/baseline.json index fc904fe3..95e64f41 100644 --- a/game/tests/.gates/baseline.json +++ b/game/tests/.gates/baseline.json @@ -13,13 +13,9 @@ "exit": "1", "fp": "f9865f1a5a44" }, - "03 探针 --click": { - "exit": "1", - "fp": "8a3cae0d0446" - }, - "04 探针 --full": { - "exit": "1", - "fp": "591b27250b36" + "03 场景探针": { + "exit": "0", + "fp": "green" }, "05 渲染闸": { "exit": "1", diff --git a/game/tests/README.md b/game/tests/README.md index 574f7ee5..0fc9073e 100644 --- a/game/tests/README.md +++ b/game/tests/README.md @@ -33,7 +33,7 @@ tests/ | **单元测试** | 领域类的原子行为对不对(公式/状态机/纯逻辑)——从对外方法进、断言钉对外可观测面(写法判据见 test-conventions §8.2) | `tests/unit/` | `run_all.gd` manifest(闸 02) | | **集成测试** | 跨系统的闭环对不对 | `tests/integration/` | 同上(与单元测试同一入口、同一协议) | | **系统测试** | 启动真实工程走真实玩法路径 | 过渡期 `game/probe/smoke_all.gd`(断言迁移批后=`tests/system_test.gd`) | `run_system_test.py`(闸 01) | -| **UI 探针+渲染 golden** | 跑起来之后 UI 与场景切换对不对;画面基线 | `game/probe/*.tscn` 探针 | `probe_gate.sh`(闸 03/04)、`render_gate.sh`(闸 05) | +| **UI 探针+渲染 golden** | 跑起来之后 UI 与场景切换对不对;画面基线 | `game/probe/*.tscn` 探针 | `probe_gate.sh`(闸 03)、`render_gate.sh`(闸 05) | > **integration/ 已建承载**(迭代 29 T3——此前只有章程句无实体,空壳曾于迭代 19 卡① T4 删过一次)。 > 首个集成用例 = `save_rng_roundtrip_test.gd`(存档⇄RNG 快照往返)。章程登记的四类集成用例里, @@ -194,7 +194,7 @@ tests/ | `tools/run_system_test.py`(系统测试) | 真实玩法路径的行为断言面(推理→战斗→结算→进度推进) | 退出码+脚本错误扫描 | 玩法链路改动的调试期确认 | | `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(65 套件 manifest) | 退出码+三硬闸(见下);每套件子进程带截止时间守护——超时先 taskkill /T 整树回收再判该套件 FAIL、批继续(默认 120s,环境变量 `RUN_ALL_SUITE_TIMEOUT_SEC` 可覆盖);连续 3 个套件超时=红树整批挂死,第 3 个即中止整轮、汇总已见失败与超时清单后非 0 退出(阈值=可调常量,不连续的超时照旧逐个回收) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;此形态仅供开发期最窄确认,不产闸回执、不作验收凭据);闸机关联档选跑——同一过滤机制加 `-- --only-source=gates`,由 `tools/run_all_gates.sh` 在 02 闸传入(回执 scope=related,是验收跑批本体) | | `tools/probe_gate.sh`(场景探针,`--click`/`--full` 两模式) | UI 可达性、可见性、状态——真实实例化场景+真实输入路径 | 探针自检+退出码 | 新增 UI 区域的配套探针直跑与断言复核 | -| `tools/render_gate.sh`(渲染 golden) | 画面基线——截图与基线图逐像素容差比对 | golden 比对脚本(依赖 PIL) | 主题/素材/排版类改动;窗口化运行,需开显示器 | +| `tools/render_gate.sh`(渲染 golden) | 画面基线——截图与基线图逐像素容差比对 | golden 比对脚本(依赖 PIL)+退出码/取证双检/三段探针日志脚本错误扫描(`error_scan.py`) | 主题/素材/排版类改动;窗口化运行,需开显示器 | | `game/tests/static_scan.sh`(S 域) | 项目纪律+第三方质量规则:RNG 单点/core 纯度/类型标注/文件头/注释禁词/旧符号残留;S13=gdstyle 质量规则(重复 load、循环内 await、循环内分配、不可达代码等真缺陷类;基线键=文件|规则|函数名(类/文件级=文件|规则,无数字条目带消息),数字漂移不假红,同键指标恶化超基线登记值即红) | 退出码(逐规则 FAIL 行) | 改产品码前后的快速自检;新增纪律先补扫描规则再修码 | | 测量件(`game/sim/`,逐件见下「游离脚本台账」) | 期望/分布读数——只给读数,不判对错 | 人工读 stdout 或配套工具脚本 | 调参与归因;MC 类一律用户发起(档位表见 `sim-data/README.md`「MC 采集档位表」) | diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 93e05bd1..f0df8b67 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -186,6 +186,7 @@ func _suites() -> Array: {"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值) {"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # AC⑤/T9——排空零分叉/零丢行/延迟双态 {"path": "res://tests/unit/presentation/standee_fx_sweep_test.gd", "expect": 0}, # 呈现域缺陷扫掠(d2 蓄力档/d4 庆祝句柄/d5+d8 互斥/d12 站桩按 id 解析) + {"path": "res://tests/unit/presentation/tune_sim_batch_parity_test.gd", "expect": 0}, # 测速循环批双实现对拍(帧索引/运行态/进度串/stats/报告五面——信号步进 vs await 基线) {"path": "res://tests/unit/presentation/standee_present_values_test.gd", "expect": 0}, # 战斗一屏样张批 V5–V9(血条危险红/名字行素底不压地台/徽标 FS_HINT/护甲 PNG 绑定/素材入包) {"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 奖励节点 UI 直验(盲区补漏) {"path": "res://tests/unit/presentation/level_map_info_fog_test.gd", "expect": 0}, # 信息迷雾——呈现显隐/Boss 豁免/详情零显示/大图拟合 diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index 508396dd..0a3f5373 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -339,7 +339,7 @@ func _meta_settle_and_workshop() -> void: adr_res.resume_adventure(bp_min, p_resume)) Kit.check("继续上局受理面:tree_rules 随参数面进 runner._tree_rules(消费面 set_tree_rules 下传现成)", adr_res.runner() != null and adr_res.runner().pick_next_node(int(adr_res.runner().next_candidate_ids()[0])) - and ((adr_res.runner().current_battle().encounter.get("tree_rules", []) as Array) == rules_e)) + and ((adr_res.runner().current_battle()._facts.encounter.get("tree_rules", []) as Array) == rules_e)) _meta_smoke_cleanup() @@ -746,7 +746,7 @@ func _smoke_tree_rules() -> void: "feats": [], "tree_rules": [], "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"})) - orch3.encounter["tree_rules"] = [] + orch3._facts.encounter["tree_rules"] = [] var gp_base: int = orch3._roll_gp_drop() var orch4 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005)) Kit.check("聚 rig:增益编排启动(tree_rules=economy_hoard)", @@ -755,7 +755,7 @@ func _smoke_tree_rules() -> void: "feats": [], "tree_rules": ["economy_hoard"], "battle_index": 1, "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"})) - orch4.encounter["tree_rules"] = ["economy_hoard"] + orch4._facts.encounter["tree_rules"] = ["economy_hoard"] var gp_hoard: int = orch4._roll_gp_drop() Kit.check("聚:战斗掉落乘算 ×1.5(provisional——同种子配对 %d→%d,入账前=结算框显示实得)", gp_hoard == int(round(float(gp_base) * 1.5)) and gp_hoard >= gp_base) diff --git a/game/tests/unit/core/run_time_fatal_test.gd b/game/tests/unit/core/run_time_fatal_test.gd index b25e1910..cd80a919 100644 --- a/game/tests/unit/core/run_time_fatal_test.gd +++ b/game/tests/unit/core/run_time_fatal_test.gd @@ -72,7 +72,7 @@ func _seal_propagation(db) -> bool: rng_a.set_master_seed(5) var oc_bg = OrchestratorScript.new(db, rng_a) var bi_bg = BoardInstallerScript.new({"db": db, "rng": rng_a, "minesweeper": oc_bg.minesweeper, - "reward_economy": oc_bg._reward_economy, "encounter": oc_bg.encounter, "fatal_cb": oc_bg._fatal}) + "reward_economy": oc_bg._reward_economy, "encounter": oc_bg._facts.encounter, "fatal_cb": oc_bg._fatal}) bi_bg.generator._fatal("测试违约①棋盘生成") if oc_bg.battle_result != "fatal_error": push_error("[run_time_fatal_test/wr5] 棋盘生成违约未传导封印(battle_result=%s)" % str(oc_bg.battle_result)) diff --git a/game/tests/unit/presentation/tune_sim_batch_parity_test.gd b/game/tests/unit/presentation/tune_sim_batch_parity_test.gd new file mode 100644 index 00000000..b44b9c6f --- /dev/null +++ b/game/tests/unit/presentation/tune_sim_batch_parity_test.gd @@ -0,0 +1,245 @@ +# 双实现对拍配对套件(注册面批同代测速循环批 v1.1——帧相位等价的执行化证据): +# 产品码(process_frame 信号步进状态机)vs 夹具(现行 await 形态逐字副本——循环内 await+收尾四语句在 run() 尾)。 +# 五面对拍(帧索引序列/运行态序列/进度串序列/stats 六键/报告全文)逐位相等= +# 每步恰一帧+相位同点+输出逐字的当轮真实执行证据(测量语义逐点等价四判据的落码面)。 +# 拓扑钉死:测试宿主先连 process_frame 帧计数、后启动两形态——两形态下「计数先于步进体」相对次序同构,序列可比。 +# n=3 常规案+n=0 退化案(同步收尾零帧——_mean/_p90 空守卫渲染面)。 +# 运行:godot --headless --path game --script res://tests/unit/presentation/tune_sim_batch_parity_test.gd(expect 0) +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const TuneSimScript := preload("res://presentation/tune_sim_controller.gd") + + +# 假调试面板(wd8 同形 setter 面+进度/运行态/报告三记录数组供序列对拍) +class FakePanel extends Node: + var running := false + var progress := "" + var report := "" + var atk := 0 + var hp := 0 + var progress_log: Array[String] = [] + var report_log: Array[String] = [] + var running_log: Array[bool] = [] + + func atk_pct() -> int: + return atk + + func hp_pct() -> int: + return hp + + func set_sim_running(r: bool) -> void: + running = r + running_log.append(r) + + func set_sim_progress(t: String) -> void: + progress = t + progress_log.append(t) + + func set_sim_report(t: String) -> void: + report = t + report_log.append(t) + + +# 假自动策略(固定返回值两档 victory/defeat 交替——wd8 :53-55 同款取值,零随机; +# 逐局记录帧索引与运行态读数——帧索引经宿主注入的计数 Callable 读取) +class FakeDriver extends RefCounted: + var ctrl + var frame_counter: Callable # 宿主帧计数(0 参 Callable——拓扑钉死:计数先于步进体) + var seen: Array[bool] = [] + var frames: Array[int] = [] + var calls := 0 + + func run_level(_db: Variant, _seed: int, _level: int, _atk: int, _hp: int, _other: bool, _logger: Variant) -> Dictionary: + calls += 1 + seen.append(ctrl.is_running()) + frames.append(int(frame_counter.call())) + if calls % 2 == 1: + return {"hero_dmg": 12.0, "rounds": 4, "result": "victory", "hp_left": 3.0, "hp_at_boss": 5.0} + return {"hero_dmg": 6.0, "rounds": 2, "result": "defeat", + "defeat_node_index": 3, "defeat_node_type": "battle", "hp_left": 0.0, "hp_at_boss": 0.0} + + +# 假配置库(TuneSimController 只读 db.run.action_economy.ap_per_round——wd8 同款最小域对象图) +class FakeActionEconomy: + var ap_per_round := 2 + + +class FakeRun: + var action_economy := FakeActionEconomy.new() + + +class FakeDB: + var run := FakeRun.new() + + +# 现行 await 形态逐字副本(tune_sim_controller.gd run(n) 改形前形态——循环内 await+收尾四语句在 run() 尾; +# 全仓唯一在测夹具——产品码禁留第二实现,tests/ 不在 S13/S8/S9/S10 扫描面、夹具内循环 await 合法在册)。 +# 与产品码新形态的申报差异仅一处:批末把 stats 六键束存 out_stats 成员供对拍读取 +# (产品码 stats 经报告公开面观测——内部字段直查受控例外申报在卡 §17)。 +class AwaitLoopRunner extends RefCounted: + const SEED_BASE := 4200 + const ParityLogger := preload("res://presentation/playtest_logger.gd") + var db + var panel + var _base_hp := 0 + var _driver = null + var _running := false + var out_stats: Dictionary = {} + + func _init(db_ref, panel_ref, base_hp: int, driver = null) -> void: + db = db_ref + panel = panel_ref + _base_hp = base_hp + _driver = driver + + func is_running() -> bool: + return _running + + func run(n: int) -> void: + _running = true + var logger = ParityLogger.new() + logger.ap_per_round = int(db.run.action_economy.ap_per_round) + logger.start_session(SEED_BASE) + panel.set_sim_running(true) + panel.set_sim_progress("模拟中:0/%d …" % n) + var stats := {"wins": 0, "dmg_all": [], "hp_left_win": [], "hp_boss": [], "rounds_all": [], "death_nodes": {}} + var autoplay = _driver + for i in range(n): + _run_one_level(autoplay, stats, i, panel, logger) + panel.set_sim_progress("模拟中:%d/%d …" % [i + 1, n]) + await panel.get_tree().process_frame + logger.close_session() + panel.set_sim_report(HudPresenter.tune_sim_report(n, panel.atk_pct(), panel.hp_pct(), stats, _base_hp)) + panel.set_sim_running(false) + _running = false + out_stats = stats + + func _run_one_level(autoplay, stats: Dictionary, i: int, panel, logger) -> void: # type-hint: ignore(驱动/面板/日志件鸭子型注入——同产品码构造缝) + var st = autoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger) + (stats["dmg_all"] as Array).append(st.hero_dmg) + (stats["rounds_all"] as Array).append(st.rounds) + if st.result == "victory": + stats["wins"] = int(stats["wins"]) + 1 + (stats["hp_left_win"] as Array).append(st.hp_left) + (stats["hp_boss"] as Array).append(st.hp_at_boss) + elif st.result == "defeat": + var key := "%d.%s" % [st.defeat_node_index, st.defeat_node_type] + stats["death_nodes"][key] = int(stats["death_nodes"].get(key, 0)) + 1 + + +func _init() -> void: + var frame_idx := {"i": -1} # 帧计数(宿主最先连——拓扑钉死「计数先于步进体」) + process_frame.connect(func() -> void: frame_idx["i"] += 1) + await process_frame # 树初始化就绪帧(SceneTree._init 时点 root 未挂树——面板 get_tree() 首帧后才有效;wd8 同款先例:panel 段前有 await 过帧) + var r3 := await _run_pair(3, frame_idx) + _check_n3(r3) + var r0 := await _run_pair(0, frame_idx) + _check_n0(r0) + quit(Kit.finish("tune_sim_batch_parity")) + + +# 对拍驱动:两形态各经 run(n) 真实入口(同参假面板/假驱动各自实例);先启动两形态、后分别 await—— +# 两形态在调用点同一同步段内先后挂起(并行于同一帧时钟),驱动协程返回 int 承接 void run 的可等待面 +func _run_pair(n: int, frame_idx: Dictionary) -> Dictionary: + # 两形态串行驱动(各经 run(n) 真实入口 await 调用——GDScript 协程动态调用必须 await, + # 无并行启动形态):帧序列各自记录、断言按相对首帧偏移归一后逐位比对——拓扑不变式 + # 「计数先连、启动后于计数、计数先于步进体」在两形态下相对次序同构,等价证据结构不变。 + var panel_a := FakePanel.new() + root.add_child(panel_a) + var drv_a := FakeDriver.new() + var a: Variant = TuneSimScript.new(FakeDB.new(), panel_a, 100, drv_a) + drv_a.ctrl = a + drv_a.frame_counter = func() -> int: return frame_idx["i"] + await a.run(n) + var panel_b := FakePanel.new() + root.add_child(panel_b) + var drv_b := FakeDriver.new() + var b: Variant = AwaitLoopRunner.new(FakeDB.new(), panel_b, 100, drv_b) + drv_b.ctrl = b + drv_b.frame_counter = func() -> int: return frame_idx["i"] + await b.run(n) + return {"a": a, "b": b, "pa": panel_a, "pb": panel_b, "da": drv_a, "db": drv_b} + + +func _check_n3(r: Dictionary) -> void: + var pa: FakePanel = r["pa"] + var pb: FakePanel = r["pb"] + var da: FakeDriver = r["da"] + var drv_b: FakeDriver = r["db"] + var a = r["a"] + var b = r["b"] + Kit.check("n=3 帧序列相对偏移逐位相等(%s vs %s)+相邻差恒一(每步恰一帧)〔测速 V1〕", + str(_norm(da.frames)) == str(_norm(drv_b.frames)) and da.frames.size() == 3 + and _consecutive(da.frames) and _consecutive(drv_b.frames)) + Kit.check("n=3 运行态读数序列逐位相等+收尾双运行态假+面板置假序列同构〔测速 V2〕", + str(da.seen) == str(drv_b.seen) and da.seen == [true, true, true] + and not a.is_running() and not b.is_running() + and not pa.running and not pb.running + and str(pa.running_log) == str(pb.running_log)) + Kit.check("n=3 进度串序列逐位相等(%d 段)〔测速 V3〕", + str(pa.progress_log) == str(pb.progress_log) and pa.progress_log.size() == 4) + var got: Dictionary = r["b"].out_stats + var deaths: Dictionary = got.get("death_nodes", {}) + Kit.check("n=3 夹具 stats 六键与按假驱动固定返回值重算的期望逐键相等〔测速 V4〕", + int(got.get("wins", -1)) == 2 + and _arr_eq(got.get("dmg_all"), [12.0, 6.0, 12.0]) + and _arr_eq(got.get("hp_left_win"), [3.0, 3.0]) + and _arr_eq(got.get("hp_boss"), [5.0, 5.0]) + and _arr_eq(got.get("rounds_all"), [4, 2, 4]) + and deaths.size() == 1 and int(deaths.get("3.battle", -1)) == 1) + Kit.check("n=3 报告全文逐字相等且含渲染面四关键串〔测速 V5〕", + pa.report == pb.report and pa.report != "" + and pa.report.contains("关卡完成率") and pa.report.contains("关卡完成剩余 HP 均") + and pa.report.contains("败北节点") and pa.report.contains("N=3")) + + +func _check_n0(r: Dictionary) -> void: + var pa: FakePanel = r["pa"] + var pb: FakePanel = r["pb"] + var da: FakeDriver = r["da"] + var drv_b: FakeDriver = r["db"] + var a = r["a"] + var b = r["b"] + Kit.check("n=0 退化:两形态驱动零回调、帧索引序列空(同步收尾零帧)〔测速 V1〕", + da.frames.is_empty() and drv_b.frames.is_empty() and da.calls == 0 and drv_b.calls == 0) + Kit.check("n=0 运行态置假序列 [true, false] 双形态同构+收尾双假〔测速 V2〕", + str(pa.running_log) == str(pb.running_log) and pa.running_log == [true, false] + and not a.is_running() and not b.is_running()) + Kit.check("n=0 进度串单段逐字相等〔测速 V3〕", + str(pa.progress_log) == str(pb.progress_log) and pa.progress_log == ["模拟中:0/0 …"]) + Kit.check("n=0 夹具 stats 六键零计〔测速 V4〕", + int((r["b"].out_stats as Dictionary).get("wins", -1)) == 0 + and ((r["b"].out_stats as Dictionary).get("dmg_all") as Array).is_empty() + and ((r["b"].out_stats as Dictionary).get("hp_left_win") as Array).is_empty() + and ((r["b"].out_stats as Dictionary).get("hp_boss") as Array).is_empty() + and ((r["b"].out_stats as Dictionary).get("rounds_all") as Array).is_empty() + and ((r["b"].out_stats as Dictionary).get("death_nodes") as Dictionary).is_empty()) + Kit.check("n=0 报告全文逐字相等且非空(空 stats 渲染面)〔测速 V5〕", + pa.report == pb.report and pa.report != "") + +# 帧序列归一(相对首帧偏移——串行驱动下两形态帧段错位、结构可比性归一承载) +func _norm(fr: Array) -> Array: + var out: Array = [] + var base := int(fr[0]) if fr.size() > 0 else 0 + for i in fr.size(): + out.append(int(fr[i]) - base) + return out + + +# 相邻差恒一(每步恰占一帧——判据①的序列面证据) +func _consecutive(fr: Array) -> bool: + for i in range(1, fr.size()): + if int(fr[i]) - int(fr[i - 1]) != 1: + return false + return true + +# 数组逐元素浮点比对(stats 桶重算期望——类型化比较,消 str 形态歧义) +func _arr_eq(a, b: Array) -> bool: + var aa := a as Array + if aa == null or aa.size() != b.size(): + return false + for i in b.size(): + if absf(float(aa[i]) - float(b[i])) > 0.0001: + return false + return true diff --git a/reports/iter44-反哺点-2026-10-30.md b/reports/iter44-反哺点-2026-10-30.md new file mode 100644 index 00000000..442da564 --- /dev/null +++ b/reports/iter44-反哺点-2026-10-30.md @@ -0,0 +1,26 @@ +# 迭代 44 反哺点清单——2026-10-30 + +要写进权威文档的点逐条列落点(改哪一节、改成什么、依据);用户在会话中裁决后归文档批落文。实现批禁写 `design/` 与根规则文件,本清单是唯一移交通道。 + +## 架构文档(design/engineering/architecture/architecture.md) + +1. **「模块归属·呈现层」ui_registry 行现状化**——现登记「实现批建件」态;实现后改现状陈述:UiRegistry/UiScreens/UiBattle/UiModals 四件已建(全 RefCounted、类变量 3/9/5/14、UiModals.flow 随容器构造即建)。 +2. **同表 main_ui_build 行「层序逻辑不在此件——唯一事实源仍是 main.LAYER_CONTRACT + _apply_layer_contract()」句失效**——唯一事实源已迁 `ui_registry.LAYER_CONTRACT`+`ui.apply_layer_contract(root)`;该句改指 ui_registry(依据=设计文档 §3.3 建议句+实现实况)。 +3. **「呈现层装配与会话生命周期」节屏成员寻址迁注册面句核对**——v1.25 已预写「探针挂载假设=main.ui.<域>.<句柄>」;实现实况与其一致,但需补两句实况细节:①ModalFlow 实例随 UiModals 容器构造建立(装配早期注入点依赖其先在座);②探针契约面新增两件取证探针(shot_probe/fx_shot_probe)随批改址。 +4. **「战斗编排域拆件定稿」节轴丁两行「构造=引用束注入」句与实态口径差**——实态=RoundCycle 三参直构(host/fx_host_ref/reward_grant_ref,五段构造透传不落成员)、BattleFacts 零参直构;句按设计文档 §8 预判「三参直构无需束」现状化。 +5. **「模块归属·功能层」RoundCycle/BattleFacts 两行实测值换代**——RoundCycle 类变量 8/公开方法 3;BattleFacts 类变量 15(贴顶)/公开方法 4(reset_round+三 mark);宿主 battle_orchestrator 实测 15 成员/811 行(文件长键 1035 已远超实况——基线 refresh 后自然消失)。 +6. **「测试与质量域」S13 豁免第三条注销句现状化**——v1.25 预写「设计闭合、实现批注销」;键账已达成(宿主实测 15),注销句转已实施事实。**同节 tune_sim 测量面豁免注销句现状化随卡③ v1.1 恢复**:裸键已消除(实测命中消失、全文件零循环);注销句保持(注销对象是 await 豁免、不依赖行为改动申报),句内「随批申报行为改动一处(收尾置假还原)」半句已失效(前提被迭代 41 测试债批 215da315 消化、申报已剥离)——现状化时一并修正为收尾现行陈述(「运行态收尾在 run() 尾每批一次执行」)。 + +## gdstyle 基线与闸面(tools/) + +7. **基线 refresh 义务(用户人工,非落文)**——`python tools/gdstyle_gate.py --refresh`:预期消失键=main.gd max-class-variables=49(实测 15)+battle_orchestrator max-class-variables=38(实测 15)+battle_orchestrator max-file-length=1035(实测 811)+轴丙遗留归绿键;合计消失 ≥2 条触发骤降守卫,须 `--force`。与待办清单「零豁免五键清偿批」批末一次人工 refresh 合并执行。 +8. **S13 no-self-assign 检查的误报形态观察**(登记不阻塞)——`X.y.z = y` 形态(赋值链尾名与右侧成员同名)被判自赋值;本批以 self 前缀消解(level_runner:520)。若后续批次再现此巧合,可考虑工程批把检查器改为比较完整接收链(登记归「迭代 42 批末移交小件」同类工具小件)。 + +## 待办清单(根流程清单——状态回退拟稿,落笔归上游段/用户) + +9. **「零豁免五键清偿批」条目测速循环批子项状态(净零处理——原回退拟稿作废)**:卡③ 停卡呈报→用户裁重裁→写卡会话重写 v1.1(零待裁点,重裁时订正 S8 十文件与两处行号注记)→本会话恢复施工收口——条目状态直推「文档设计已闭合+已写卡+已编迭代 44」无回退中间态;重裁经过实况记录于实现报告 §5/§5.1,本条不再承载状态回退义务。 +10. **「零豁免五键清偿批」条目键账进度换代**——五键中两键已真实清偿(main.gd 49→15、battle_orchestrator 37→15),待基线 refresh 收账;呈现层状态件批(迭代 45 在册)两键不受影响。 + +## 其他 + +11. **main.gd 死 preload 存量**(工程小件登记建议)——拆件遗留的零使用 preload 常量 8 条(UiAssembler/TuneSimController/DebugSessionScript/TreasureInteractController/CurioInteractController/CampShopPresenter/BoardResolversScript/三场景 preload 中 main 零使用者);本批仅清理与迁移直接相关的 ModalStack/RowPacer 两条,其余属存量债不扩范围;归工程改动直改批顺路清理。 diff --git a/reports/iter44-实现报告-2026-10-30.md b/reports/iter44-实现报告-2026-10-30.md new file mode 100644 index 00000000..153ae6ab --- /dev/null +++ b/reports/iter44-实现报告-2026-10-30.md @@ -0,0 +1,118 @@ +# 迭代 44 实现报告(组合根注册面批+战斗编排归约批+测速循环批)——2026-10-30 + +状态:三卡全部施工收口(卡③ 按停卡呈报建议案重裁为 v1.1 后施工);迭代总收口(批末闸/审计/合并)进行中。 + +## 0. 各卡状态 + +| 卡 | 状态 | 提交(worktree iter44,基点 c8f5091d) | +|---|---|---| +| 卡① 组合根注册面批 | **收口**——main.gd 49→15、四新件建件、全消费面改址 | bee0682f(T1 建件)/b89ee850(T2 改形)/784d581b(T3 改址)/1929516a(T4 消灭收口) | +| 卡② 战斗编排归约批 | **收口**——battle_orchestrator 38→15(实测 37 轴丙基数起算)、RoundCycle/BattleFacts 两新件 | 24f3ca51(T1 BattleFacts)/a832965a(T2 RoundCycle)/15c833e0(T3 宿主收束)/3e9d3afa(T4 键账) | +| 卡③ 测速循环批 | **收口(v1.1 重裁后施工)**——v1.0 停卡呈报→用户裁重裁→v1.1 剥离行为改动申报面后 T0–T3 全落 | c82ef23f(T1 步进状态机)/04783fae(T2 对拍套件) | + +## 1. 卡① 实际对照表(卡片 §8 十一行 → 实况) + +| # | 需求点 | 改动实况 | 验收测试实况 | 结果 | +|---|---|---|---|---| +| 1 | 注册面建件+合同机器迁驻 | ui_registry.gd(LAYER_CONTRACT 逐字迁入+node(key) 19 键 match+apply_layer_contract 唯一落位口)+ui_screens/ui_battle/ui_modals 三容器 | 系统测试 V1c(system_test.gd:94-106 改址 main.ui.LAYER_CONTRACT/main.ui.node,尾缀〔装配生命批 V1〕保留)——树序逐对保序 | 绿 | +| 2 | main.gd 改形回归(49→15) | 28 迁出+7 消灭+1 新增=22(T2 中间态)→15(T4 终态);六转发方法+_flush_row_queue 退役 | run_smoke 15 项 0 失败+run_system_test 69 项 0 失败;S13 静态判据=gdstyle 实测 15(基线键 49 已不命中、成死键) | 绿 | +| 3 | 装配与流程件改址 | main_ui_build 八步赋值目标 root.ui.*(八步顺序/add_child 序/连接语句逐条不变);三流程件同表映射;smoke_boot get() 4 改 2 留 | 冒烟主屏存活面六断言+系统测试 V2 句柄等价五连比(system_test.gd:108-109 改址) | 绿 | +| 4 | 探针面批量改址 | 探针九件+probe_ui_util+shot_probe/fx_shot_probe(决定记录 2)共约 460 行机械改写 | probe_gate --click --full 全段退出码 0(九件 PASS) | 绿 | +| 5 | 系统测试面改址+RunScope 生命周期 | system_test V1a/V1c/V2/V3 矩阵取址改指 | V1–V9 全绿(V3a–V3i 存活性矩阵第五引用面逐面同源) | 绿 | +| 6 | 别名/接缝成员消灭(7 个) | _seed_edit/board_view/ap_bar/_row_queue/_modals/_settlement_stop_pending/_deferred_modal 全删;消费方改址(pacing_queue 4 处+接缝探针三件+main 自身) | pacing_queue 绿+ui_struct_probe 登记序断言(改 flow.stack)+t17_defer_probe 挂起态断言(改 flow._deferred) | 绿 | +| 7 | 六转发模态方法退役 | _open_modal/_close_modal/_on_modal_closed/_input_locked/_open_modal_after_playback/_on_pump_ended 退役;调用方改指 ui.modals.flow 五真方法+pump_ended 注入改 flow 方法 | modal_flow 绿+系统测试 V3g 模态开合段+ui_click_probe 模态开合路径 | 绿 | +| 8 | V1d 源码断言重锚 | source_guard_test.gd 重锚全 presentation 目录扫描(move_child 只许 ui_registry.apply_layer_contract 体内),尾缀〔注册面 V1〕 | source_guard 绿(8 checks) | 绿 | +| 9 | V6 源码断言改串 | theme_projection_test.gd V6 改断言 ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed),尾缀〔注册面 V2〕 | theme_projection 绿(164 checks) | 绿 | +| 10 | 视觉零变化主证 | 全批呈现层改址零像素改动 | render_gate golden 零差异(shot_probe 七屏+golden 全过) | 绿 | +| 11 | 改址完整性收口 | 49 名逐名 grep 残留=0(词形态+字符串键形态两轮全量扫描,T3 提交信息留档) | 批末代码域闸归迭代总收口(待跑);grep 自检静态判据已留档 | 静态部分绿 | + +静态判据申报执行实况(卡片 §8 域口径):S13 键账对账=gdstyle 实测 main.gd 15、四新件零新键(T1/T4 两轮 static_scan 确认);基线 refresh 留用户人工(见 §4 遗留义务)。 + +## 2. 卡② 实际对照表(卡片 §11 V0–V6 行 → 实况) + +| 行 | 需求点 | 改动实况 | 验收测试实况 | 结果 | +|---|---|---|---|---| +| V0 | 行为零变化(总判据) | 成员迁移与改指;RNG 五流零触碰(behavior_roll 随件迁移取号序逐字);五信号留宿主;两快照签名载荷逐字 | 关联套件全绿零改址应红项(round_flow/settle_segments/skill_unlock/trigger_cond/seal_polarity/feat_vouchers/variant_placement/meta_forge/adventure_feats/treasure_effects/roll_multiplier/breakpoint_resume/level_shop_economy/table_structure_guard) | 绿 | +| V1 | 两新件建件接线 | battle_facts.gd(15 成员贴顶+reset_round 逐字+三 mark 写口)+round_cycle.gd(8 成员+三参直构+五段构造逐字迁入) | settle_segments_test V1 断言(_rounds 在座且 oc 回指+_facts 在座+五段段件齐 _rounds)〔编排归约 V1〕 | 绿 | +| V2 | reset_round 复位边界 | reset_round 只清 reveals/seals/path 三项 | round_flow_skill_pool_test 内嵌断言:终局后 presentation_snapshot()["refresh_used"] == 末轮结算快照 economy.refresh_used〔编排归约 V2〕 | 绿 | +| V3 | 段驱动与摇号迁移 | 三函数迁出;宿主调用点五处改址;trigger_cond 3 处改指 | round_flow 全套+settle_segments V4-3+skill_unlock+trigger_cond(改指后绿)——同种子回放断言零改址钉住 | 绿 | +| V4 | _hero_atk 派生读 | 成员删→私有函数;combat_segment :56 改调 | roll_multiplier_test 内嵌:override 贯通 spawn+缺省回退表值〔编排归约 V4〕;攻击单 base_value 贯通由既有 hero_atk_val=100 全链路夹具承载 | 绿 | +| V5 | treasure_inventory 注入改址 | 宿主成员与 setter 删、五处读改指、level_runner:520 改经 reward_grant() 出口直注 | treasure_effects_test 内嵌引用同一性断言〔编排归约 V5〕+ob8/ob11/ob13 既有 use_treasure 受理链改址后全绿 | 绿 | +| V6 | 封印判定账/装板溯源/受理事实迁移 | 事实族改指(grant_segment 18 处+宿主全量);三 mark 写口升级;attune 读源改 _facts | seal_polarity/feat_vouchers(契约 2 计数纯净 R2)/variant_placement/meta_forge/adventure_feats/table_structure_guard(attune 外部面零改址应绿——最硬的外部面零变化证)全绿 | 绿 | + +行为改动申报核对:diff 中行为改动零条(全部非行为改动——成员迁移与改指;完美写口升级=同一时点同一增量改经方法单点)。测试削弱申报:零申报兑现(测试改指只动成员寻址;新增断言 V1/V2/V4/V5 四条+V1d/V6 重锚两条=卡片 §8 申报全集)。 + +## 3. 实现决定记录(代拍板白名单外事项备案) + +卡①: +1. **ModalFlow 实例随 UiModals 容器构造即建**(ui_modals._init)——装配第二步 row_pacer.pump_ended 注入早于第八步 flow 建立,转发壳退役后需 flow 先行在座;查询/钩子 Callable 注入时点不变(_build_modal_flow 内语句序不变),八步顺序零变化。 +2. **shot_probe/fx_shot_probe 纳入改址**——两件是 render_gate 取证探针、不在卡片「探针九件」清单(probe_gate 段构成),但同为 main.X 一级地址消费方;T3 改址完整性 grep 自检抓漏后纳入(render_gate 对照表行 10 的承载件,属消费面全量改址义务)。 +3. **ui_tune_probe 零连接巡检白名单登记 flow.close**——表 D 申报「closed bind 目标改 flow.close.bind」的机检联动:巡检白名单原钉 main._on_modal_closed(组合根转发壳),改钉 main.ui.modals.flow.close(同一连接的新宿主);判据面「closed 外发消费目标是模态编排口」语义不变,conn_count=11 断言零变化。 +4. **layout_probe 三区 get() 字符串键改容器字段键**——卡片表 D 字符串键清单只列 smoke_boot 6 处;layout_probe:60 `main.get(key)`(key="stage"/"info_panel"/"board_zone")为漏网字符串键,改 `main.get("ui").battle.get(key)`(战区域容器字段名与三区合同键同名)。 +5. **curio_overlay/confirm_dialog 两词在装配/流程件替换表漏列**——main.gd 表有、main_ui_build 与三流程件表漏,冒烟(Invalid assignment on main.gd)与 ui_click_probe sl7(开窗调用静默失效)抓漏后补改;教训=机械改写必须逐词对照 T0 名单全量扫描(T3 终检已按 49 名逐词复查全域归零)。 + +卡②: +1. **grant_segment._seal_preview_area 的 oc.treasure_inventory 读为 T3 盘点漏项第五处**(卡片 T3 列宿主四处读)——SCRIPT ERROR 抓漏后改指 reward_grant.treasure_inventory。 +2. **treasure_effects_test 五处直构注入 obX.treasure_inventory**(ob8/ob11/ob13 注入与读、ob14 段)——设计文档 §11 盘点「1 处」漏项;同属「成员寻址改指、断言零变化」改经 reward_grant() 出口。 +3. **level_runner 注入行加 self 前缀**消 S13 no-self-assign 文本巧合(`X.treasure_inventory = treasure_inventory` 左侧链尾名与右侧成员同名);语义零变化。 +4. **_hero_atk 派生读落位**:受理段写点删除、spawn 读点改调、combat_segment 消费改调——「读面即引用、无第二账」先例同款。 +5. 宿主内 encounter/attunement 词替换以占位符保护三处字符串键(behavior_roll 前置流名 "encounter"/BoardInstaller bundle 键 "encounter"/enc.get("attunement") 键)——键字面零变化。 + +## 4. 遗留读数义务与登记项 + +- **gdstyle 基线 refresh(用户人工,批末总收口后)**:`python tools/gdstyle_gate.py --refresh`——卡① 死键 1 条(main.gd max-class-variables=49)+卡② 预期死键 1 条(battle_orchestrator max-class-variables=38;文件长键 1035 已由轴丙归绿基线在册需一并核对)+轴丙遗留归绿键,合计消失 ≥2 条触发骤降守卫须 `--force`(与待办清单「零豁免五键清偿批」批末一次人工 refresh 合并执行口径一致)。 +- **实机目检总清单(登记不阻塞)**:①三屏切换与模态开合实机过一遍(卡① T4 义务——冒烟/探针/golden 已机检承载,真人观感复核登记);②战斗一局全流程观感(卡② 结构迁移零行为变化的一般性目检)。 +- **能力清单核对(收尾核对义务)**:本批卡① 零新增能力(改址只换地址)、卡② 零新增能力(新增断言全部内嵌既有函数)——game/tests/README.md 零增删改,批末核对表上代表用例名与实际落地一致(未改名,核对通过)。 +- **批末代码域闸**(run_all_gates.sh --code)归迭代总收口——卡③ 裁决后与 reshape 串联执行。 + +## 5. 卡③ 停卡呈报(缺陷移交——按卡片 §13 L3 条款①预设停机点) + +**分歧点**:卡③ §3 断言「`_run_one_level` 尾部两行 :67 `panel.set_sim_running(false)`/:68 `_running = false` 每局执行一次(潜伏红本体)」与现行代码不符——实测两行在 `run()` 尾 :53-54(for 循环外、每批恰一次),`_run_one_level` 尾部 :67-68 实为败局节点记账。 + +**定谳证据**(三重): +1. git 历史:迭代 41 测试债批提交 215da315(2026-10-29 合回)提交信息明载「两个产品回归随批修(……**tune_sim 运行态收尾归还 run**)」——潜伏红已由该批先修复。 +2. T0 实跑面一:`--only=standee_fx_sweep` 全绿(18 checks)——wd8 三断言(`drv.seen == [true, true]`/收尾双运行态假/报告非空,现位 :145-147)现行全 PASS。 +3. T0 实跑面二:ui_tune_probe 全链路 PASS——⑤ 守卫环 1800 帧预算内退出、输出四串在(模拟 N=10 完整输出)。 + +**两种读法**: +- 读法 A(证据支持):卡片成文(2026-10-29 v1.0 定稿)时点潜伏红确实在(u23 卷入形态),同日合回的迭代 41 测试债批把收尾归还 run() 尾——卡片「现状考证」被上游批施工消化变质。215da315 提交信息与代码现状一致。 +- 读法 B:卡片考证当时即误读(:67-68 为败局记账非置假)。与 215da315 提交信息矛盾,不采。 + +**影响面**: +- 已申报行为改动 V3(收尾置假还原)**无对象**——还原动作已由迭代 41 测试债批完成并申报。 +- 目标 2「wd8 三断言与 ui_tune_probe ⑤ 零改动**转绿**」变「零改动**保持绿**」。 +- 核心义务仍有效:S13 await-in-loop 裸键消除(tools/gdstyle_baseline.txt:15 在册、基线 16 键之一)与双实现对拍套件(帧相位等价执行化证据)——对拍夹具与产品码的申报差异由两处减为一处(收尾还原差异消失,仅余 stats 暴露成员)。 +- T0「实跑定谳」义务已完成(上列三重证据),实现段零施工(停摆卡不提交)。 + +**建议案**(供写卡段/用户裁决): +1. 卡③ 退回写卡段重裁:剥离行为改动申报(对照表 V3 行删、§1 目标 2/3 措辞改「保持绿」)、T0 判读出口改「两面全绿=前提已由迭代 41 测试债批消化,直接按无行为改动形态开工」;保留裸键消除+双实现对拍义务(夹具=现行产品码 await 形态逐字副本——收尾在 run() 尾形态——与产品码唯一申报差异=夹具暴露 stats 供对拍)。 +2. 若用户不愿重写卡:裁「卡③ 剥离出迭代 44」(roadmap 行拆批),裸键消除与对拍随下一迭代重编。 + +**待办清单条目状态回退拟稿**(落笔归上游段/用户,本批禁写根流程清单):「零豁免五键清偿批」条目的测速循环批子项状态由「文档设计已闭合+已写卡+已编迭代 44」回退为「文档设计已闭合+待写卡(现状考证已被迭代 41 测试债批消化——行为改动申报面失效,卡片重裁后重编)」。 + +## 6. 批末全量域闸与闸红修复(收口实况) + +**闸况**:reshape 后第三次全量域闸(--code)全绿——00 冒烟/01 系统测试/03 场景探针/05 渲染闸/10 熵增闸绿;**02 单元与集成、09 静态扫描从基线红改善归绿**(基线记的迭代 41 时代存量红指纹已在册待换代——现真绿);回执 commit 对齐分支尖(b27db010)。 + +**闸红修复三笔(新红必修,均孤跑复绿)**: +1. S9 R1:tune_sim_controller `_sim_stats` 裸 Dictionary 注解改 `Dictionary[Variant, Variant]`(Variant 参数化现行口径)——本批新码写法违规。 +2. ui_input_probe :482/:488 case⑮ 装配直构注入 `settlement.attunement` → `_facts.attunement`——卡② 成员迁移的探针消费漏网(T0 映射谱系只扫 main./root./self. 前缀,直构注入形态不在谱系);SCRIPT ERROR 中断 _ready 致探针挂死超时。 +3. run_time_fatal_test :75 BoardInstaller 束键值 `oc.encounter` → `oc._facts.encounter`+meta_progression_forge_test 三处编排器 encounter 直读改指(:340/:749/:758)——同型漏网(负面套件名与触面无字面关联=关联档选测盲区;meta_forge 分片复现、单进程未现的夹具路径差异)。 + +**实现决定记录补(白名单外备案)**:跨卡触面(卡② 迁移成员被卡① 已改址的探针/未申报套件消费)只有全量闸能抓——关联档选测的盲区实证;全域终扫(encounter/attunement 直读形态)后同类漏网清零。 + +## 7. 独立验收审计结论与记录级缺漏补记 + +审计报告:reports/iter44-验收审计-2026-10-30.md——**结论=通过(可合并)**。21 行对照表逐行通过、判据绑定面自跑全绿、测试削弱零发现(37 条被删断言逐条有配对新行)、非行为改动成立、证据链在案。 + +记录级缺漏三项(不阻塞,随本节补记): +1. 卡③ 对拍串行驱动为白名单外决定——已在 §5.1 卡③ 实现决定记录 1 备案(审计方读的是早期版本);提交 04783fae 信息同载。 +2. main_overlay_flow.gd:4 头注仍写已退役的 root._open_modal/_close_modal 转发方法名——词面遗留(卡面头注义务只列 main.gd 与 main_ui_build 两处);闸后代码面禁改(改则回执作废),留反哺点通道随文档批或下一代码批顺路改写。 +3. 卡② grant_segment 事实族改指实测 18 处 vs 卡面「约 23」估计偏差——记录偏轻,本节补记(T0 点名清单实数为准)。 + +例外备案:V1d 扫描面为 presentation 平铺 27 文件口径(与卡面 glob 一致、只扩不缩;递归口径下批前既有 ui_kit.gd:819 即红——非本批引入)。 + +## 8. 停机状态与收口(进行中) + +- 迭代总收口(冻结→并 master→reshape→批末全量域闸→独立验收审计→合并)**未启动**——按停机规则「迭代收口合并须等停摆卡闭合、或用户裁拆批后整体进行」。 +- 卡① 卡② 全部收口提交在 worktree iter44 分支(9 笔提交),冻结待收口。 diff --git a/reports/iter44-验收审计-2026-10-30.md b/reports/iter44-验收审计-2026-10-30.md new file mode 100644 index 00000000..5a070a7d --- /dev/null +++ b/reports/iter44-验收审计-2026-10-30.md @@ -0,0 +1,99 @@ +# 迭代 44 独立验收审计报告——2026-10-30 + +审计方:未参与实现的独立会话(只读审计+自跑最窄抽查)。对象=worktree D:\code\g-plan2-iter44 分支 iter44 尖 9f1dda40,对照基线 master 尖 ab3b4a9d;材料=三张需求卡片(组合根注册面批/战斗编排归约批/测速循环批 v1.1)+实现报告+闸回执与日志。本报告不修改任何代码与测试,缺漏如实记录不代修。 + +**结论行:通过(可合并)**——三卡 21 行对照表逐行核到落点、判据绑定面自跑全绿、测试削弱零发现(断言计数 22 文件零减少)、非行为改动判定成立、证据链在案;2 项记录级缺漏+1 处注释级陈旧指址+4 项例外备案,均不动行为、判据与验收闭合(见 §6),不构成合并阻塞。 + +## 1. 逐行核对——卡①(§8 十一行) + +| # | 判定 | 凭据(文件:行/自跑) | +|---|---|---| +| 1 | 通过 | V1c 改址 `main.ui.LAYER_CONTRACT`/`main.ui.node`(system_test.gd:90/:94,尾缀〔装配生命批 V1〕保留);自跑 run_system_test 69 项 0 失败;LAYER_CONTRACT 与基线逐字一致(git show ab3b4a9d 对读,唯一差异=注释指址 `_apply_layer_contract`→`apply_layer_contract`,白名单 3) | +| 2 | 通过 | main.gd `^var` 计数 49(基线)→15(尖);自跑 run_smoke 15 项 0 失败+run_system_test 69 项 0 失败;gdstyle 零基线外新键 | +| 3 | 通过 | main_ui_build.gd `build()` 八步名序与基线逐字同;`_build_screens` 赋值序/add_child 序不变;42 条 connect 一一对应(仅 :97/:206 两处 closed bind 改 `flow.close.bind`=表 D 申报);smoke_boot get() 4 改 2 留(settings_store/run_scope 留);冒烟六断言+V2 五连比(system_test.gd:108-109)自跑绿 | +| 4 | 通过 | 探针面 diff 归一化对账 88 处非平凡行全部落表 A/C/D+实现决定 2/3/4;probe_gate 日志全绿(.gates/probe.log 06:53,全段);ui_tune_probe 审计自跑 PASS | +| 5 | 通过 | system_test 改址全对账(V1a/V1b/V1c/V2/V3 矩阵 `_face_snapshot` mf→`main.ui.modals.flow`、rp→`main.ui.screens.row_pacer`);V3a–V3i 存活性矩阵自跑全绿(69 项含) | +| 6 | 通过 | 七别名消费方改址逐处对上(pacing_queue 4 处、ui_struct_probe `flow.stack`、t17_defer_probe `flow._deferred`、ui_click_probe `settlement_stop_timer.is_armed()`);自跑 `--only=pacing_queue --only=modal_flow` 双绿;改后断言判据零变化(归一化对账) | +| 7 | 通过 | 六转发方法+_flush_row_queue 全库零残留(grep 只余 1 处陈旧头注,见 §6 缺漏 2);modal_flow 24 checks 绿+系统测试 V3g 段绿 | +| 8 | 通过 | source_guard_test.gd:69-92 重锚——尾缀〔注册面 V1〕;自跑输出「V1d 层序落位单口(presentation 27 文件——apply_layer_contract 之外 move_child 调用点=0)」;扫描面较改前(单文件)只扩不缩;口径例外见 §6 例外 1 | +| 9 | 通过 | theme_projection_test.gd:725-726 改断言新串 `ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)`,尾缀〔注册面 V2〕;自跑 164 checks 绿 | +| 10 | 通过 | render_gate 日志全绿(.gates/render.log 06:54,shot_probe 七屏+golden);diff 零像素面改动(改址只动地址词) | +| 11 | 通过 | 审计独立 grep:49 名旧一级地址 `main./root./self.` 代码级残留=0(仅 3 处注释级命中);代码域闸日志六件全绿(§5);留档偏薄见 §6 例外 5 | + +## 2. 逐行核对——卡②(§11 V0–V6) + +| 行 | 判定 | 凭据 | +|---|---|---| +| V0 | 通过 | 关联套件全绿:审计自跑 8 套件(settle_segments/round_flow_skill_pool/roll_multiplier/treasure_effects/pacing_queue/modal_flow/source_guard/theme_projection)+批末闸 unit.log 分片全绿(15 suites shard 4/4);behavior_roll 随件迁移取号序逐字(§4 核对);五信号留宿主(battle_orchestrator.gd:12-16);两快照出口在宿主(:677/:739) | +| V1 | 通过 | settle_segments_test.gd:28-29 两断言〔编排归约 V1〕:`oc._rounds != null and oc._rounds.oc == oc`+`oc._facts != null`(check_quiet 静默通过=套件 18 checks 零失败);五段段件齐在 _rounds 由 :30-34 五条既有接线断言改指 `oc._rounds._X_seg` 承载(尾缀〔迭代27 T2〕保留,纯改址) | +| V2 | 通过 | round_flow_skill_pool_test.gd:112-115〔编排归约 V2〕:终局后 `presentation_snapshot()["refresh_used"]` == 末轮结算快照 `economy.refresh_used`(经 settlement_snapshot 末次发射承接);自跑实跑输出 5 实例(L1·D0×3+L2·D0×2)全 PASS;reset_round 只清三项(battle_facts.gd:32-35) | +| V3 | 通过 | roll_round_actions 与基线 `_roll_round_actions` 逐行对读一致(仅成员改指;`oc.rng.open_stream("behavior_roll")` 流名与开流时点不变,round_cycle.gd:102);settle_if_due 闸序 settling→battle_result→phase 逐字、终局早退不回置;trigger_cond_test 3 处改指(:119/:126/:131);自跑 round_flow 134 checks+settle_segments+trigger_cond 全绿 | +| V4 | 通过 | roll_multiplier_test.gd:45-59 四断言〔编排归约 V4〕:override 100→spawn hero_unit atk==100;缺省→hero 表值(check_quiet 静默通过);攻击单 base_value 贯通由既有 `_hero_attack_row_with`(:343,hero_atk_val=100 全链路停手结算行)承载——两消费点(spawn+base_value)闭合;combat_segment.gd:56 改调 `oc._hero_atk()`,宿主派生读 :69/:282 | +| V5 | 通过 | treasure_effects_test.gd:311-315〔编排归约 V5〕:`ob14.reward_grant().treasure_inventory == adr14.runner().treasure_inventory` 引用同一性;use_treasure 受理链由 ob8/ob11/ob13 既有段承载(:311 ob8 直写改指后绿);level_runner.gd:520 注入改经 `reward_grant()` 出口(`self.` 前缀=实现决定 3) | +| V6 | 通过 | seal_polarity(_perfect 直写→_facts 8 处对读)/feat_vouchers(契约 2 计数纯净段改指后全绿)/variant_placement/meta_forge/adventure_feats/table_structure_guard——批末闸 unit 分片全绿;三 mark 写口(grant_segment.gd:311/:379/:394 与基线直写同点同增量);attune 三外部消费面零改址(main.gd/treasure_interact_controller/table_structure_guard 均不在改址 diff) | + +## 3. 逐行核对——卡③ v1.1(§12 V1–V3) + +| 行 | 判定 | 凭据 | +|---|---|---| +| V1 | 通过 | tune_sim_batch_parity_test.gd 五面断言〔测速 V4/V5〕自跑绿(n=3 stats 六键逐键重算+报告逐字含四关键串;n=0 六键零计);run 可等待契约由 wd8 `await ctrl.run(2)`(standee_fx_sweep_test.gd:144,零改动)承载;全文件零循环(grep -c "for \|while " = 0)、代码 await 仅 :69 一处 `await _batch_finished` | +| V2 | 通过 | 五面对拍断言〔测速 V1–V3〕自跑绿:帧序列相对偏移逐位+相邻差恒一(每步恰一帧)、运行态序列逐位+收尾双假、进度串逐位;n=3+n=0 两案各 5 断言全 PASS;manifest 增行(run_all.gd:189);拓扑例外见 §6 缺漏 1 | +| V3 | 通过 | wd8 三断言〔wd8〕自跑 PASS(18 checks:`drv.seen == [true, true]`/收尾双运行态假/报告非空——standee_fx_sweep_test.gd 不在本批 diff=零改动);ui_tune_probe 审计自跑 PASS(⑤ 守卫环 1800 帧预算内退出+输出四串);收尾四语句迁入 `_finish_batch()`(:87-92)与基线 run() 尾次序逐句同(close→报告→置假→_running),`_run_one_level` 零触碰(不在 diff) | +| 静态 | 通过 | gdstyle_gate.py 审计自跑 exit 0:命中 7 全基线内、基线外新违规 0;基线 :15 `tune_sim await-in-loop` 裸键不命中=死键(消失键不参与红判定) | + +## 4. 测试削弱检查(对照三卡申报全集) + +- **断言计数对账**:22 个改动测试/探针文件 `Kit.check|check(|check_quiet` 计数基线→尖逐一比较——零文件减少;仅 5 增(roll_multiplier 36→40=V4 四断言、round_flow 100→101=V2、settle_segments 16→18=V1 两条、treasure_effects 58→59=V5、tune_sim_batch_parity 0→10=新套件)。与申报新增面(卡② V1/V2/V4/V5 四条+卡① V1d/V6 重锚+卡③对拍五断言)完全一致。 +- **删除断言逐条对账**:全 diff 被删 check/断言行 37 条逐条找到配对新行——语义不变只换地址(含 smoke_boot 三 get、system_test V2 五连比、pacing 四断言、V1d/V6 两条);无任何断言删除后无配对。 +- **阈值/扫描面**:零阈值放宽;扫描面唯一改动=V1d 单文件→27 文件(申报为强化,成立);static_scan.sh S8/S7 两清单增行 round_cycle.gd(卡② T2 申报;battle_facts.gd 未增行=卡片明示「零协作调用不入清单」,成立)。 +- **换下层直调**:零发现——探针仍走真实按钮/输入路径(pressed.emit/push_input 等既有形态未动)。 +- **wd8 三断言**:文件不在 diff=零改动,自跑保持绿。**ui_tune_probe 巡检白名单**(实现决定 3):`host == main and m == "_on_modal_closed"` → `host == flow_host and m == "close"`(ui_tune_probe.gd:214-216)——属卡①表 D「closed bind 目标改 flow.close.bind」的机检联动,同一连接新宿主登记;conn_count=11 断言与消息逐字不变(:223-224 与基线 :221-222 对读);判据面「closed 外发消费目标=模态编排口」语义保持。**判定:不属削弱**。 +- 卡② §15 零申报、卡③ §16 零申报——兑现成立(超出申报的删改零处)。 + +## 5. 非行为改动判定与证据链 + +**卡①**:装配序(八步名序、add_child 序、赋值序)逐字不变;42 条连接语句一一对应;LAYER_CONTRACT 19 项逐字迁驻;move_child 单口由 V1d 27 文件扫描绿+grep 复证(全 presentation 仅 ui_registry :84/:98 两处调用,views/ui_kit.gd:819 为批前既有、不在 diff);main._ready 仅插入 `ui = UiRegistry.new()` 一行于 _build_ui 之前;`_build_modal_flow` 注入语句序与基线逐字同(flow 实例改随 UiModals._init 构造=实现决定 1,注入时点不变)。 +**卡②**:成员迁移与改指——roll_round_actions/settle_if_due 逐字迁移对读成立;grant_segment 三 mark=同点同增量;encounter/attunement 词替换的三处字符串键保护(实现决定 5)grep 复证(behavior_roll 前置流名 "encounter" 字面在位);treasure_inventory 注入时序(宿主构造后 :520)不变。 +**卡③**:收尾四语句迁移=同帧同相位(对拍 V2 运行态序列+V5 报告逐字承载,自跑绿);`_run_one_level` 零触碰。 + +**证据链**: +1. 卡③ T0 三重证据在实现报告 §5 在案:215da315 提交信息明载「tune_sim 运行态收尾归还 run」(审计直读提交信息复证)+两面实跑读数;审计自跑两面(wd8 套件+ui_tune_probe)复证现行全绿——前提消化链条完整。 +2. 卡③ v1.1 重裁凭据:卡 v1.1 状态行+§15 裁决记录(2026-10-30 停卡重裁三点)在案。 +3. 卡①卡②实现决定记录(报告 §3 共 5+5 条)对照卡① §10 七条、卡② §17 五条白名单——白名单外事项(shot/fx_shot 纳入、layout_probe get 键、curio/confirm 漏列补改、flow 随容器构造、grant 第五处读、treasure_effects 五处直构、self 前缀、占位符保护)均有备案,无遗漏。 +4. 闸凭据:receipt.json 现为 docs 域回执(9f1dda40 提交时 pre-commit 钩子生成,b27db010 时代,全绿);代码域末跑凭据=.gates 六件日志(smoke 15/0、system_test 69/0、static_scan ok、probe_gate 全绿、render_gate 全绿、unit 分片全绿),时间戳 06:49–06:54 恰落 b27db010(06:49:41 提交)与 9f1dda40(06:57:19 提交)之间——绑定终态代码(9f1dda40 只含报告文件)。 +5. 红线:diff 只触 `game/`+`reports/` 两目录;零 `randomize()`/自建 RandomNumberGenerator;零触及 `design/`、`game/tests/README.md`、两份根流程清单。 + +## 6. 缺漏与例外(如实记录,不修) + +**缺漏(记录级,不动判据)**: +1. **卡③实现决定记录缺位**:对拍驱动由卡 T2 的「先启动两形态、后分别 await」并行拓扑改为**串行驱动+相对帧偏移归一**(tune_sim_batch_parity_test.gd:145-162)——卡 §14 白名单六条不覆盖,应在实现报告「实现决定记录」节备案;实际只在提交 04783fae 信息中有案(理由=GDScript 协程动态调用强制 await、无并行启动形态+_init 时点 root 未挂树实测)。报告 §3 无卡③条目。实质判据未损(相对偏移+相邻差恒一仍承载「每步恰一帧+相位同点」,审计自跑绿)。 +2. **main_overlay_flow.gd:4 头注陈旧指址**:注释仍写「全部开合走组合根模态单口(root._open_modal/_close_modal)」,两转发方法已退役、实际调用全改 `root.ui.modals.flow.open/close`——「指址须真」词面违例,行为无涉,批内漏改。 +3. **卡②改指处数偏差记录偏轻**:grant_segment 实改 18 处与卡片「约 23」的 L2 偏差,实况数在报告 §2 V6 行在案但未入「实现决定记录」节(卡 §14 L2 要求记入该节)。 + +**例外(依据在案,不判红)**: +1. **V1d 扫描面口径**:实现为 DirAccess 平铺(res://presentation 顶层 27 个 .gd),不含 views/、scenes/ 子目录——与卡片字面 glob(`game/presentation/*.gd`)一致、较改前单文件只扩不缩;但「全 presentation 目录零第二落位点」按递归读法不成立:批前既有 `views/ui_kit.gd:819` 有一处 `body.move_child(...)`(视图内行排序、非层序落位;ui_kit.gd 不在本批 diff、归迭代 42 尾波清偿批在册债)。ui_registry.gd:3 头注「全仓唯一 move_child 点」沿袭旧 main.gd 注释口径、与该既有事实并存。若日后递归化扫描面,须先处置该行。 +2. **回执被覆盖**:receipt.json 为 docs 域回执(报告提交钩子生成);代码域末跑以六件日志为凭(§5 第 4 条),与报告 §6「第三次全量域闸全绿、02/05/09 从基线红改善归绿」叙述吻合。 +3. **`_finished` 成员写而不读仍保留**(tune_sim_controller.gd:26/:64/:88)——卡 §14 白名单 3 明示「零读者确认后可删」,保留属白名单内两可选择,键账只升 1(11/15 在阈内)。 +4. **报告 row4 计数口径差**:「约 460 行机械改写」(报告 §1)与 784d581b「422 行」——统计口径差,不影响判据。 + +## 7. 审计自跑记录(独立复证清单) + +| 项 | 结果 | +|---|---| +| `python tools/run_smoke.py` | PASS——15 项 0 失败 | +| `python tools/run_system_test.py` | PASS——69 项 0 失败(V1a/V1c/V2/V3a–V3i 全过) | +| `--only=source_guard --only=theme_projection` | 绿——V1d「presentation 27 文件…调用点=0」〔注册面 V1〕+V6 新串〔注册面 V2〕+theme 164 checks | +| `--only=settle_segments --only=round_flow_skill_pool --only=roll_multiplier --only=treasure_effects` | 4 套件绿——V1 两断言、V2 五实例、V4 四断言、V5 一断言(check_quiet 静默通过=零失败) | +| `--only=pacing_queue --only=modal_flow` | 双绿 | +| `--only=tune_sim_batch_parity --only=standee_fx_sweep` | 双绿——对拍 10 断言〔测速 V1–V5〕+wd8 三断言 | +| `res://probe/ui_tune_probe.tscn`(headless) | PASS——⑤ 守卫环预算内退出+输出四串+全链路通过 | +| `python tools/gdstyle_gate.py` | exit 0——命中 7 全基线内、零新违规;tune_sim 裸键不命中 | +| grep:tune_sim 循环/await | 循环 0;代码 await 仅 run() :69 一处 | +| grep:49 名旧一级地址残留 | 代码级 0(仅注释级 3 处) | +| 成员数 | main.gd 49→15;battle_orchestrator 37→15;容器 3/9/5/14 | +| 断言计数对账 | 22 文件零减少、5 增(+4/+1/+2/+1/+10) | +| diff 归一化对账 | 88 处非平凡行对全部落表 A/C/D+已记录决定 | + +## 8. 总结论 + +**通过(可合并)。** 三卡对照表 21 行全部核到落点且判据绑定面当轮实跑成立;测试削弱零发现;三卡「非行为改动」判定在代码级对读与实跑两面成立;证据链(卡③ T0 三重证据、v1.1 重裁、卡①卡②决定记录、闸日志)在案。缺漏三项(卡③决定记录缺位、main_overlay_flow.gd:4 陈旧头注、卡②计数记录偏轻)与例外四项均属记录/注释级,不触行为零变化、判据绑定与验收闭合;建议随批末收口由实现侧补记(补记动作归收口段,不由本审计代作)。gdstyle 基线 refresh(--force)仍属用户人工遗留义务(反哺点 7 在案)。 diff --git a/reports/需求卡片-测速循环批-2026-10-29.md b/reports/需求卡片-测速循环批-2026-10-29.md index c7748f9a..98b32a9b 100644 --- a/reports/需求卡片-测速循环批-2026-10-29.md +++ b/reports/需求卡片-测速循环批-2026-10-29.md @@ -1,36 +1,36 @@ # 需求卡片——测速循环批(tune_sim_controller 循环内 await 真实消除) -> **版本状态**:v1.0 定稿——零待裁点(2026-10-29 用户裁:信号步进状态机形态、收尾置假还原作为已申报行为改动、双实现对拍入批、错误路径差异接受并文档化)。 +> **版本状态**:v1.1 定稿——零待裁点(2026-10-30 停卡重裁:实现段 T0 三重证据定谳 v1.0 的「潜伏红」前提已被迭代 41 测试债批消化〔215da315——运行态收尾归还 run〕,按停卡呈报建议案剥离已申报行为改动 V3、T0 义务改现状复核、夹具申报差异两处减一处;2026-10-29 原四点裁决中②作废,①③④仍有效——裁决记录见第 15 节)。 > **接收方**:待办清单条目「零豁免五键清偿批——S13 余量五键真修四卡(2026-10-29 登记,来源=会话提及)」——本卡为其四卡中的测速循环批卡。 -> **来源**:reports/设计-测速循环去await-2026-10-29.md(2026-10-29 设计闭合)。**唯一事实源**:`reports/设计-测速循环去await-2026-10-29.md` + `design/engineering/architecture/architecture.md` v1.25「测试与质量域」S13 测量面豁免注销句(2026-10-29 零豁免裁:原循环内 `await` 豁免〔2026-10-27〕注销——tune_sim_controller 改 process_frame 信号常驻连接+步进状态机,每帧一局、帧相位逐点对齐;随批申报行为改动一处:运行态收尾由「每局置假」还原为「整批收尾单次置假」,消除与 wd8/ui_tune_probe 断言面的静态矛盾)。 +> **来源**:reports/设计-测速循环去await-2026-10-29.md(2026-10-29 设计闭合;§2.3 潜伏红推演与 §10 待裁点 1 的「还原」申报已随停卡重裁作废,§3–§7 形态与等价性论证仍有效)。**唯一事实源**:`reports/设计-测速循环去await-2026-10-29.md` + `design/engineering/architecture/architecture.md` v1.25「测试与质量域」S13 测量面豁免注销句(2026-10-29 零豁免裁:原循环内 `await` 豁免〔2026-10-27〕注销——tune_sim_controller 改 process_frame 信号常驻连接+步进状态机,每帧一局、帧相位逐点对齐;注销对象是 await 豁免、不依赖行为改动申报,句保持;句内「随批申报行为改动一处」半句已失效,随实现批现状化时一并修正——见第 6 节)+ 停卡呈报三重证据(iter44 worktree 分支 `reports/iter44-实现报告-2026-10-30.md` §5——重裁依据)。 ## 1. 目标与完成定义 -这批做什么:把 S13 静态质量闸(gdstyle)在册裸键 `game/presentation/tune_sim_controller.gd|quality/await-in-loop` 真实消除——`run(n)` 的 for 循环内 `await process_frame` 改为 process_frame 信号常驻连接+步进状态机(每帧回调执行一局、步尽次帧收尾断连),`run()` 只留函数级一次 `await _batch_finished` 保持协程身份;改后全文件零 for/while,该键从文本形态根除、此文件对这个键结构性免疫。随批执行两项已裁事项:把「每局置假」还原为「整批收尾单次置假」(已申报行为改动——消除与两个在库断言面的静态矛盾);新增双实现对拍套件,把帧相位等价论证变成当轮真实执行证据。 +这批做什么:把 S13 静态质量闸(gdstyle)在册裸键 `game/presentation/tune_sim_controller.gd|quality/await-in-loop` 真实消除——`run(n)` 的 for 循环内 `await process_frame` 改为 process_frame 信号常驻连接+步进状态机(每帧回调执行一局、步尽次帧收尾断连),`run()` 只留函数级一次 `await _batch_finished` 保持协程身份;改后全文件零 for/while,该键从文本形态根除、此文件对这个键结构性免疫。随批执行一项已裁事项:新增双实现对拍套件,把帧相位等价论证变成当轮真实执行证据。**本批零行为改动**——收尾置假已在 run() 尾每批一次执行(迭代 41 测试债批 215da315 归还后的现行形态),本批收尾四语句同帧同相位迁入 `_finish_batch()`,语句集与语义零变化(第 3、4 节)。 什么状态算完: 1. `tune_sim_controller.gd` 全文件零循环、`await` 仅 `run()` 函数级一处;`python tools/gdstyle_gate.py` 对账该裸键消失(消失键不参与红判定)、无基线外新键。 -2. 测量语义逐点等价(第 4 节四判据)经双实现对拍在当轮真实跑批中证明——五面对拍全等;wd8 三断言与 ui_tune_probe ⑤ 零改动转绿。 -3. diff 中已申报行为改动恰一处(收尾置假还原,对照表 V3 行),无第二处行为改动。 +2. 测量语义逐点等价(第 4 节四判据)经双实现对拍在当轮真实跑批中证明——五面对拍全等;wd8 三断言与 ui_tune_probe ⑤ 零改动保持绿。 +3. diff 中行为改动零处——收尾四语句迁移为非行为改动(同帧同相位、语义零变化,对照表 V3 行),无任何行为漂移。 4. 脚本错误中断路径差异作为声明缺口写入实现报告(禁静默)。 5. 批末代码域闸全绿;实现报告与反哺点清单写齐方算收口。 ## 2. 设计事实源 -设计依据唯一出处=`reports/设计-测速循环去await-2026-10-29.md`(2026-10-29 设计闭合,零待裁点):§4 推荐案(process_frame 信号步进状态机伪代码结构)、§5 等价性证明清单(四判据+已声明缺口)、§6 键账预期、§7 行为零变化风险点。架构面凭据=`design/engineering/architecture/architecture.md` v1.25「测试与质量域」S13 测量面豁免注销句(2026-10-29 零豁免裁落文)——豁免注销、行为改动申报面与本卡一致。待办清单条目「零豁免五键清偿批」状态=文档设计已闭合+待写卡(本卡即其中一卡的写卡段产出)。 +设计依据唯一出处=`reports/设计-测速循环去await-2026-10-29.md`(2026-10-29 设计闭合,零待裁点):§3 形态评估(process_frame 信号步进状态机采纳)、§4 推荐案(伪代码结构)、§5 等价性证明清单(四判据+已声明缺口)、§6 键账预期——以上各节不受前提消化影响、仍是本卡依据;该文档 §2.3 潜伏红推演与 §10 待裁点 1「还原」申报已作废(前提消化证据见停卡呈报)。架构面凭据=`design/engineering/architecture/architecture.md` v1.25「测试与质量域」S13 测量面豁免注销句(2026-10-29 零豁免裁落文)——豁免注销句保持,句内行为改动申报半句随实现批现状化修正(第 6 节同步面)。前提消化与重裁凭据=iter44 worktree 分支 `reports/iter44-实现报告-2026-10-30.md` §5 停卡呈报(git 历史证据 215da315+T0 两面实跑绿)。待办清单条目「零豁免五键清偿批」状态=已写卡+已编迭代 44(本卡 v1.1 重裁段产出)。 -## 3. 现状事实(代码级,2026-10-29 实查;开工 T0 按当轮 master 重测重锚——行号基线为本日 master) +## 3. 现状事实(代码级,2026-10-30 实查;开工 T0 按当轮施工基面重锚——行号基线为本日 master) -- `game/presentation/tune_sim_controller.gd`(全件 68 行):`run(n)` 在 :36-52——:37 置运行态真、:40-42 开共享 JSONL 日志会话、:43 `panel.set_sim_running(true)`、:44 进度 0/n、:45 建 stats 六键束、:46 取自动策略驱动、:47-50 for 循环体(`_run_one_level` → 进度串 → :50 `await panel.get_tree().process_frame`——命中键的语句)、:51-52 收尾(close_session+报告注入)。`_run_one_level` 在 :56-68(5 参),**尾部两行 :67 `panel.set_sim_running(false)`/:68 `_running = false` 每局执行一次**(潜伏红本体,见下)。类成员 5(db/panel/_base_hp/_driver/_running)+常量 2(:7 SEED_BASE=4200、:8 PlaytestLogger preload);头注 4 行。 -- 断言面一〔wd8〕`game/tests/unit/presentation/standee_fx_sweep_test.gd:126-134`:FakePanel(内类 Node,:20-40——setter 面并入树供 get_tree)+FakeDriver(:44-55——`run_level` 回调内读 `ctrl.is_running()` 逐局记 `seen`);:129 直构控制器(字典夹具 db)、:131 `await ctrl.run(2)`(run 必须可等待、挂起至整批完成的契约)、:132-134 三断言——`drv.seen == [true, true]`/收尾 `not ctrl.is_running() and not panel.running`/报告非空。套件在 run_all.gd manifest 在册(res://tests/unit/presentation/standee_fx_sweep_test.gd,expect 0)。 -- 断言面二〔探针 ⑤〕`game/probe/ui_tune_probe.gd:63-83`::65-66 设滑条与 N=10、:67 `press_simulate_for_probe()`(真实按钮 pressed 信号链)、:68-71 守卫环(`while main.debug_overlay.is_running()` 逐帧等——SIM_GUARD_FRAMES=1800 在 :16)、:72-73 预算内未完成判红、:74-83 输出四串断言(关卡完成率/关卡完成剩余 HP 均〔口径换代 V7〕/败北节点/N=10)。面板语义面 `debug_overlay.gd`:`set_sim_running` :81-83 → `_sync_sim_button` :107-110(模拟与两层调试两钮禁用+「模拟中…」文案——浮窗运行态贯穿整批的可见面)、`is_running` :77-79。 -- 调用面 `game/presentation/main_debug_flow.gd`:`_on_tune_simulate` :72-77——守卫(活跃结算/冒险早退)后 `root._tune_sim.run(n)` **不 await 调用**(返回的协程态丢弃——新形态必须保持此调用形态可用、零改动);`_start_debug_levels` :84 守卫读 `root._tune_sim.is_running()`(两层调试入口互查守卫)。 -- 静态矛盾推演(设计文档 §2.3——T0 实跑定谳)::67-68 每局执行 → wd8 首局完成即把两个运行态置假 → `drv.seen` 实测 `[true, false]` 必红;探针按下返回后首局已在同一同步段内跑完、运行态已假 → 守卫环立即退出 → 输出停在进度串 → 四串断言全红。推断这两行原在循环之后(「循环体提函数」搬运时被卷入提取体——设计文档 §2.3),wd8 写于 d8 批且当时必绿可反证;两面均不在近期批次的关联档选测覆盖内(套件名与涉案文件无字面关联、probe_gate --full 非恒跑三保险),潜伏红因此未暴露。 -- 键账:`tools/gdstyle_baseline.txt:15` = 该文件唯一在册键(await-in-loop 裸键,基线 16 键之一)。S13 扫描面=产品码三目录(game/core+data+presentation——probe/sim/tests 不在面,`game/tests/static_scan.sh` S13 头注在案);S8 文件集=九文件显式清单(static_scan.sh :317——不含本件);S9/S10 扫描面同不含 game/tests/。 +- `game/presentation/tune_sim_controller.gd`(全件 68 行):`run(n)` 在 :36-54——:37 置运行态真、:40-42 开共享 JSONL 日志会话、:43 `panel.set_sim_running(true)`、:44 进度 0/n、:45 建 stats 六键束、:46 取自动策略驱动、:47-50 for 循环体(`_run_one_level` → 进度串 → :50 `await panel.get_tree().process_frame`——命中键的语句)、:51-54 收尾四语句(`close_session()` :51 → 报告注入 :52 → `panel.set_sim_running(false)` :53 → `_running = false` :54)——**收尾置假在 run() 尾、for 循环外、每批恰一次**。`_run_one_level` 在 :57 注释/:58-68(5 参),尾部 :66-68 为败局节点记账(defeat 分支)——非置假。类成员 5(db/panel/_base_hp/_driver/_running)+常量 2(:7 SEED_BASE=4200、:8 PlaytestLogger preload);头注 4 行。 +- 前提消化定谳(v1.0「静态矛盾推演」作废依据):git 历史——迭代 41 测试债批提交 215da315(2026-10-29 合回)提交信息明载「两个产品回归随批修(……tune_sim 运行态收尾归还 run)」,收尾置假两行由该批归还 run() 尾并随该批申报;v1.0 成文(2026-10-29)与该批合回同日,其「现状考证」被上游批施工消化变质(读法 A;读法 B「考证当时即误读」与 215da315 提交信息矛盾,停卡呈报 §5 不采)。T0 实跑三重证据(iter44 worktree,停卡呈报 §5):wd8 三断言(现位 :145-147)全 PASS(套件 18 checks 全绿);ui_tune_probe 全链路 PASS(⑤ 守卫环 1800 帧预算内退出+输出四串在)。**潜伏红不存在**——两面现行全绿,「零改动转绿」改「零改动保持绿」。 +- 断言面一〔wd8〕`game/tests/unit/presentation/standee_fx_sweep_test.gd:139-147`:FakePanel(内类 Node,:20-40——setter 面并入树供 get_tree)+FakeDriver(:44-55——`run_level` 回调内读 `ctrl.is_running()` 逐局记 `seen`);:142 直构控制器(字典夹具 db)、:144 `await ctrl.run(2)`(run 必须可等待、挂起至整批完成的契约)、:145-147 三断言——`drv.seen == [true, true]`/收尾 `not ctrl.is_running() and not panel.running`/报告非空。套件在 run_all.gd manifest 在册(res://tests/unit/presentation/standee_fx_sweep_test.gd,expect 0,manifest :188)。 +- 断言面二〔探针 ⑤〕`game/probe/ui_tune_probe.gd:63-83`::65-66 设滑条与 N=10、:67 `press_simulate_for_probe()`(真实按钮 pressed 信号链)、:68-71 守卫环(`while main.ui.modals.debug_overlay.is_running()` 逐帧等——SIM_GUARD_FRAMES=1800 在 :16)、:72-73 预算内未完成判红、:74-83 输出四串断言(关卡完成率/关卡完成剩余 HP 均〔口径换代 V7〕/败北节点/N=10)。面板语义面 `debug_overlay.gd`:`set_sim_running` :81-83 → `_sync_sim_button` :107-110(模拟与两层调试两钮禁用+「模拟中…」文案——浮窗运行态贯穿整批的可见面)、`is_running` :77-79。 +- 调用面 `game/presentation/main_debug_flow.gd`:`_on_tune_simulate` :72-77——守卫(活跃结算/冒险早退)后 :77 `root._tune_sim.run(n)` **不 await 调用**(返回的协程态丢弃——新形态必须保持此调用形态可用、零改动);`_start_debug_levels` :84 守卫读 `root._tune_sim.is_running()`(两层调试入口互查守卫)。 +- 键账:`tools/gdstyle_baseline.txt:15` = 该文件唯一在册键(await-in-loop 裸键,基线 16 键之一)。S13 扫描面=产品码三目录(game/core+data+presentation——probe/sim/tests 不在面,`game/tests/static_scan.sh` S13 头注在案);S8 文件集=**十文件**显式清单(static_scan.sh :317 现行实查——不含本件);S9/S10 扫描面同不含 game/tests/。 - 产品码 process_frame 订阅面(T0 实查项):本件 :50(循环内 await)+`settlement_fx_view.gd:94`(play_damage_number 内一次性 await 等文本度量——战斗结算呈现期,与模拟批不并存)。现库无「run 途中新增订阅者」场景。 - 报告渲染面 `game/presentation/hud_presenter.gd` :197-218 `tune_sim_report`(静态函数,零触碰计划):空数组 `_mean`/`_p90` 双守卫(:221-227/:230-235)、完成率分母 `maxi(1, n)`(:206)——n=0 退化路径渲染安全(本批对拍 n=0 案的前提,实查在案)。 -- 能力表现状:`game/tests/README.md` 能力表零 tune_sim/调试浮窗行;锚到本批断言宿主套件的唯一既有行=「站桩敌人呈现与结算演出落点」(代表用例 `standee_fx_sweep_test.gd:_init`——wd8 断言所在函数)。 +- 能力表现状:`game/tests/README.md` 能力表「调参测速批量模拟(逐帧步进与统计读数)」行已入表(:131——写卡段 v1.0 同批义务,真实入口=批量模拟入口,代表用例=对拍套件);断言宿主 wd8 套件锚既有行「站桩敌人呈现与结算演出落点」(:130)。 ## 4. 设计契约(违反即返工的红线条款,标 🔴 触碰=L3 停机呈报) @@ -40,30 +40,30 @@ ③ 终止条件与终止时读数不变——终止=计数走满 n;close_session/运行态复位/报告注入在同帧执行; ④ 输出逐字不变——报告=未触碰的 `HudPresenter.tune_sim_report`;stats 是 (种子, atk_pct, hp_pct) 的纯函数、帧时点不进测量值;JSONL 由未触碰的自动策略逐行写、close 时点同帧。 违反即返工;等价性对拍失败=L3 停机呈报。 -- 🔴 **行为改动唯一=收尾置假还原**(已申报——对照表 V3 行):`_run_one_level` 尾部两行(:67-68)删除,运行态复位收进 `_finish_batch()` 收尾单次执行;禁任何其他行为漂移——`_on_tune_simulate` 不加运行态早退守卫(设计文档待裁点 3 备选否决)、发布态零触碰、探针①–④⑥–⑩ 段零触碰、`HudPresenter`/`PlaytestLogger`/`sim/level_autoplay.gd`/`debug_overlay.gd` 零触碰。 +- 🔴 **行为改动零处(收尾语义零变化)**:收尾置假已在 run() 尾每批一次执行(:53-54,215da315 归还后的现行形态)——本批 `_run_one_level`(:58-68)零触碰、运行态复位语义零变化;收尾四语句(:51-54)迁入 `_finish_batch()` 为同帧同相位语句迁移(判据③承载——非行为改动,对照表 V3 行)。禁任何行为漂移——`_on_tune_simulate` 不加运行态早退守卫(设计文档待裁点 3 备选否决)、发布态零触碰、探针①–④⑥–⑩ 段零触碰、`HudPresenter`/`PlaytestLogger`/`sim/level_autoplay.gd`/`debug_overlay.gd` 零触碰。 - 🔴 **脚本错误中断路径差异=声明缺口**(设计文档 §5 缺口 1,接受并文档化):GDScript 无错误陷阱 API、差异不可消除——旧形态步进体内脚本错误=协程死亡静默停摆(运行态永真、报告永不落);新形态=计数未推进、次帧重试同一步(确定性错误逐帧重试,探针 1800 帧守卫可抓;生产态=持续重试的可见坏,与旧「静默卡死」同级不同形)。实现报告文档化义务(T3),禁静默、禁造第三条路(错误陷阱/守卫兜底均已否决);测量批遇错本就整批作废,两形态都不产出可用读数。 -- 🔴 **S8 结算路径 await 零命中面不触碰**:本件不在 S8 文件集(九文件显式清单实查不含本件);本批零触碰 S8 文件集与一切闸件。 +- 🔴 **S8 结算路径 await 零命中面不触碰**:本件不在 S8 文件集(十文件显式清单实查不含本件);本批零触碰 S8 文件集与一切闸件。 - 🔴 **拆分边界纪律**(判据权威=code-conventions §1.1,卡内执行条款):本批零拆出单元——步进状态机收编本件(职责语句与不拆理由见第 8 节);①拆出单元须有可陈述职责语句与单一变化原因;②命不出名字、只能说「剩下的成员」的打包=凑计数袋,禁;③「过阈值」只作发现信号——本批键消除靠形态根除、不靠拆件凑数;④按纪律拆不出内聚单元→停卡呈报;⑤禁沿阈值轴拆。 - 🔴 **RNG 与种子零触碰**:SEED_BASE=4200 与逐局种子切分(SEED_BASE+i)零变化——stats 是种子纯函数是等价性论证的前提。 - 🔴 **注释纪律**:新增头注/注释零变更叙事(「由 await 改为信号」类句禁——沿革归 git);S11 注释禁词由术语表解析供给,新增注释用白话描述现行形态与信号契约。 ## 5. 范围边界 -做什么:第 3 节四件事——T0 现状定谳(实跑两断言面)、`tune_sim_controller.gd` 步进状态机改造+收尾还原、双实现对拍套件、键账对账与收口。 +做什么:第 3 节四件事——T0 现状复核(两面实跑应绿)、`tune_sim_controller.gd` 步进状态机改造(收尾语义零变化)、双实现对拍套件、键账对账与收口。 不做什么(明示去向): - 零豁免其余四键(main.gd 类变量 49、battle_orchestrator 类变量 38、board_cell_unit 30、standee_body 19)——各归组合根注册面批/战斗编排归约批/呈现层状态件批(同条目四卡,待办清单在册)。 - `tools/gdstyle_baseline.txt` 禁实现批中途刷新——批末由用户人工重生成(第 6 节)。 - `_on_tune_simulate` 运行态早退守卫——不收件(+1 行为面、system_test 前置断言随之需复核,代价大于收益,设计文档待裁点 3 否决)。 -- `design/` 整个目录、两份根流程清单——实现批禁写;该改的点列反哺点清单。`game/tests/README.md`:测量面专属能力行已随本卡写卡段同批入表(「调参测速批量模拟」行——写卡同批义务,grilling 复核订正),实现批禁写红线不变。 +- `design/` 整个目录、两份根流程清单——实现批禁写;该改的点列反哺点清单。`game/tests/README.md`:测量面专属能力行已随本卡 v1.0 写卡段同批入表(「调参测速批量模拟」行),实现批禁写红线不变。 - 探针其余段与套件其余断言族(standee_fx_sweep 的 wd2/wd4/wd5/wd12 面)零触碰;`architecture.md` 其余豁免条目(battle_orchestrator 第三条注销句现状化归战斗编排归约批反哺点)零触碰。 ## 6. 关联文档同步面 | 文档 | 关联点(本批实现会改变它的哪条陈述) | 同步义务 | |---|---|---| -| design/engineering/architecture/architecture.md | 「测试与质量域」S13 测量面豁免注销句(v1.25——「设计闭合、实现批建件」批后现状化;随批申报行为改动句随之成为已实施事实) | 设计权威文档——实现批禁直改;逐条列进 `reports/iter-反哺点-<日期>.md`(豁免注销句现状化),用户裁决后归文档批落文 | +| design/engineering/architecture/architecture.md | 「测试与质量域」S13 测量面豁免注销句(v1.25——「设计闭合、实现批建件」批后现状化;句保持——注销对象是 await 豁免、不依赖行为改动申报;句内「随批申报行为改动一处(收尾置假还原)」半句已失效〔前提被迭代 41 消化、申报已剥离〕,现状化时一并修正为收尾现行陈述) | 设计权威文档——实现批禁直改;逐条列进 `reports/iter44-反哺点-2026-10-30.md`(豁免注销句现状化,该清单 #6 已登记暂停点、随本卡 v1.1 恢复),用户裁决后归文档批落文 | | game/tests/README.md | 能力表——测量面专属能力行「调参测速批量模拟(逐帧步进与统计读数)」已随本卡写卡段同批入表(真实入口=批量模拟入口,代表用例=本卡对拍套件);manifest 计数行随套件增减浮动(自助声明,零义务) | 机械面文档——实现批禁写;入表已完成于写卡段(本卡 v1.0 同批),实现期零改动 | | tools/gdstyle_baseline.txt | tune_sim await-in-loop 裸键(:15,该文件唯一在册键)随键消失成死键 | 重生成为人工专用——批末由用户执行 `python tools/gdstyle_gate.py --refresh`(键数 16→15,降幅 <10% 不需 --force);禁实现批中途刷新 | | design/gdd/(全目录) | 无关联文档——测量工具私有机制改造,不改游戏规则与玩家可见行为(tune_sim 全链路发布态不可达,设计文档 §7.3) | 无修订预期;契约句被触碰=停卡呈报走反哺点通道 | @@ -72,18 +72,18 @@ | 文件 | 新增/复用 | 职责 | 行为改动 | |---|---|---|---| -| game/presentation/tune_sim_controller.gd | 复用(改造) | 批量模拟控制器——process_frame 信号常驻连接+步进状态机;收尾单点 `_finish_batch`;全文件零循环 | 行为改动一处(V3 收尾还原)+结构改形(V1——零漂移由对拍证) | -| game/tests/unit/presentation/tune_sim_batch_parity_test.gd | 新增 | 双实现对拍配对套件——遗留 await 形态夹具(内类)+五面对拍断言 | 非行为改动(测试面) | +| game/presentation/tune_sim_controller.gd | 复用(改造) | 批量模拟控制器——process_frame 信号常驻连接+步进状态机;收尾单点 `_finish_batch`(四语句同帧同相位迁入);全文件零循环 | 非行为改动(结构改形——收尾语义零变化,零漂移由对拍证;回归锚面=对照表 V3 行) | +| game/tests/unit/presentation/tune_sim_batch_parity_test.gd | 新增 | 双实现对拍配对套件——现行 await 形态夹具(内类,收尾在 run() 尾的现行形态)+五面对拍断言 | 非行为改动(测试面) | | game/tests/run_all.gd | 复用(改造) | manifest 增一行(新套件注册,expect 0) | 非行为改动(测试基建) | -结构图与第 8 节互指:新增件一件(对拍套件),其「为什么不能复用/落点唯一」理由见第 8 节;其余两行均为既有文件改造。 +结构图与第 8 节互指:新增件一件(对拍套件),其「为什么不能复用/落点唯一」理由见第 8 节;其余两行均为既有文件改造。行为改动列全为零——每行的验收落点见对照表 V1/V2/V3 行。 ## 8. 落位与可复用性预检 - **步进状态机不引入新文件(本件内改造)——为什么不拆新件**:单一职责已在件内——`tune_sim_controller` 的职责就是批量模拟控制,步进机制(每帧一局的节律与帧相位承载)是它的私有运行机制,随批生灭(run 期活成员,批末失效下次 run 重写),没有独立于批量模拟的存在意义;拆出「步进机」件命不出独立职责语句=凑计数袋(code-conventions §1.1 判据②④禁)。复用面核查:全仓 grep 无信号步进机同职责实现可复用;其余四形态(SceneTreeTimer 链/Timer 节点/递归协程/线程)已被设计文档 §3 逐一否决(相位不同或键不消)。 -- **对拍套件落 `game/tests/unit/presentation/` 的唯一性与命名**:落点唯一——与断言宿主 wd8 同目录同协议(extends SceneTree+Kit 收尾),单元层承载;命名 `tune_sim_batch_parity_test.gd`(领域命名、无 ID 前缀);grep 同名与同义标识符实查无第二处。**遗留 await 形态副本全仓只许此一份**——产品码禁留第二实现(旧形态只活在测试夹具内);tests/ 不在 S13/S8/S9/S10 扫描面(扫描面=产品码三目录与 B1 九文件清单,实查在案),夹具内循环 await 合法在册。 +- **对拍套件落 `game/tests/unit/presentation/` 的唯一性与命名**:落点唯一——与断言宿主 wd8 同目录同协议(extends SceneTree+Kit 收尾),单元层承载;命名 `tune_sim_batch_parity_test.gd`(领域命名、无 ID 前缀);grep 同名与同义标识符实查无第二处。**遗留 await 形态副本全仓只许此一份**——产品码禁留第二实现(旧形态只活在测试夹具内);tests/ 不在 S13/S8/S9/S10 扫描面(扫描面=产品码三目录与 S8 文件集,实查在案),夹具内循环 await 合法在册。 - **复用面**:夹具的假面板/假驱动复用 wd8 的形态先例(FakePanel/FakeDriver 同形——代码在各套件内自建,不跨套件共享 helper,禁散落);收尾复用 `test_kit.gd` 的 Kit.finish(零断言专用退出码协议)。 -- **分层与交互**:控制器保持 RefCounted、零 Node 入树;process_frame 连接经 `panel.get_tree()` 取树(与现状同一出口);对拍套件 `--script` 模式零 autoload 依赖(db 用字典夹具,同 wd8 :129 先例);零新增跨层出口、零数值表读取、零随机。 +- **分层与交互**:控制器保持 RefCounted、零 Node 入树;process_frame 连接经 `panel.get_tree()` 取树(与现状同一出口);对拍套件 `--script` 模式零 autoload 依赖(db 用字典夹具,同 wd8 :142 先例);零新增跨层出口、零数值表读取、零随机。 ## 9. 实现规范预算(gdstyle 默认阈值对账:公开方法 ≤20、类变量 ≤15、单函数分支 ≤8、返回 ≤6、参数 ≤5、局部 ≤10、函数 ≤50 行) @@ -93,7 +93,7 @@ |---|---|---| | quality/await-in-loop(裸键,本案标的) | **消除** | 全文件零 for/while;await 仅 `run()` 函数级一处(`await _batch_finished`)——结构性免疫 | | quality/allocation-in-loop | 不命中 | 文件零循环;`PlaytestLogger.new()` 在 run 顶层(现状即如此) | -| max-function-length ≤50 | 达标 | run 约 22 行、`_step_frame` 约 12 行、`_run_one_level` 删两行后约 10 行 | +| max-function-length ≤50 | 达标 | run 约 22 行、`_step_frame` 约 12 行、`_finish_batch` 约 6 行、`_run_one_level` 现行 11 行零触碰 | | max-branches ≤8 | 达标 | `_step_frame` 一个 if/else;run 一个 if/else | | max-local-variables ≤10 | 达标 | run 局部 3(logger/stats/autoplay——余者转成员);`_step_frame` 局部 1 | | max-returns ≤6 | 达标 | 全 void | @@ -108,15 +108,15 @@ ## 11. 任务卡 T0..T3(估时为区间;工作量限制只属迭代层——合计 1–1.5 工作日) -**T0 现状定谳与读数重锚(0.25 工作日)** +**T0 现状复核与读数重锚(0.25 工作日)** -- [ ] 实跑 wd8 单套件(开发期最窄确认,不作验收凭据):`godot --headless --path game --script res://tests/run_all.gd -- --only=standee_fx_sweep`——记录 :132-134 三断言实况;红=潜伏红定谳成立。套件若因在册无关缺陷先红于 wd8 断言(待办清单「迭代 41 测试债修复」在册的 set_trans 一类),先核该债修复状态、不得借本批顺手修非申报面——读数以「wd8 断言可达性」为准呈报(L2 记录) -- [ ] 实跑 ui_tune_probe 单探针:`…console.exe --headless --path game res://probe/ui_tune_probe.tscn`——记录 ⑤ 守卫环与输出四串实况 -- [ ] 判读:两面按第 3 节静态推演红=前提成立开工;两面实跑全绿=潜伏红证伪=设计前提崩塌——L3 停卡呈报(第 13 节) -- [ ] 按当轮 master 重锚第 3 节全部 file:line 与成员计数;漂移处就地改卡并记录 +- [ ] 实跑 wd8 单套件(开发期最窄确认,不作验收凭据):`godot --headless --path game --script res://tests/run_all.gd -- --only=standee_fx_sweep`——记录 :145-147 三断言实况,**应绿**(绿=前提已被迭代 41 测试债批消化,按无行为改动形态开工)。套件若因在册无关债先红于 wd8 断言,先核该债修复状态、不得借本批顺手修非申报面——读数以「wd8 断言可达性」为准呈报(L2 记录) +- [ ] 实跑 ui_tune_probe 单探针:`…console.exe --headless --path game res://probe/ui_tune_probe.tscn`——记录 ⑤ 守卫环与输出四串实况,**应在** +- [ ] 判读:两面实跑全绿=重裁前提成立开工;任一面红=现状与重裁前提失配(迭代 41 修复被回退或新潜伏红)——L3 停卡呈报(第 13 节) +- [ ] 按当轮施工基面重锚第 3 节全部 file:line 与成员计数;漂移处就地改卡并记录(本卡行号基线=2026-10-30 master;同迭代卡①改址过 main_debug_flow/ui_tune_probe——两件相关行实测未漂移,仍以 T0 重锚为准) - [ ] 实查产品码 process_frame 订阅面——预期两处(本件 :50+settlement_fx_view.gd:94);出现 run 途中新增订阅者场景=L2 记录+代码评审注记(设计文档 §5 缺口 2) -产出:潜伏红定谳读数(改动前基线证据,独立验收审计要核对);重锚后行号。 +产出:现状复核读数(两面绿——前提消化链条的当轮证据,独立验收审计要核对);重锚后行号。 **T1 步进状态机改造(0.25–0.5 工作日)** @@ -124,16 +124,16 @@ - [ ] 增 `signal _batch_finished` 与 6 个 run 期活成员(名字按设计文档 §4.1 逐字:`_sim_n: int`/`_sim_i: int`/`_sim_stats: Dictionary`/`_sim_autoplay`/`_sim_logger`/`_finished: bool`) - [ ] `run(n)` 改形:setup 段(:37-46 语句逐字保持);n>0 分支——`is_connected` 守卫 connect `panel.get_tree().process_frame`(恰好连接一次,迭代 39 先例)→ `_step_frame()` 首步同步执行(与 await 形态「第 0 局在调用栈内跑完」对齐)→ `await _batch_finished`(函数级单次 await——键消除落点);n=0 分支直调 `_finish_batch()`(循环零进同步收尾,与 await 形态一致不挂起) - [ ] 新增 `_step_frame() -> void`(0 参):`_sim_i < _sim_n` 分支——`_run_one_level(_sim_autoplay, _sim_stats, _sim_i, panel, _sim_logger)`+`panel.set_sim_progress("模拟中:%d/%d …" % [_sim_i + 1, _sim_n])`(串逐字保持现码 :49)+`_sim_i += 1`;else 分支——先 disconnect(防再入)→ `_finish_batch()` → `_batch_finished.emit()` -- [ ] 新增 `_finish_batch() -> void`(0 参):`_finished = true` → `close_session()` → `panel.set_sim_running(false)` → `_running = false` → `panel.set_sim_report(HudPresenter.tune_sim_report(_sim_n, panel.atk_pct(), panel.hp_pct(), _sim_stats, _base_hp))`(四语句相对次序按此草图,对两断言面不可观测——白名单 5) -- [ ] `_run_one_level` 删尾部两行(:67-68),其余逐字保持(5 参签名不变) -- [ ] 孤立验证:`--only=standee_fx_sweep` 全绿(wd8 三断言零改动转绿=潜伏红消除实证)+ui_tune_probe 单探针绿(probe_gate --full 段承载) +- [ ] 新增 `_finish_batch() -> void`(0 参):收尾四语句由 run() 尾 :51-54 迁入(`close_session()`→报告注入→`panel.set_sim_running(false)`→`_running = false`,同帧同相位——判据③;`_finished = true` 置位为新增首句,对两断言面不可观测——白名单 5) +- [ ] `_run_one_level` **零触碰**(:58-68 逐字保持——败局节点记账非置假;本批行为零改动的锚面) +- [ ] 孤立验证:`--only=standee_fx_sweep` 全绿(wd8 三断言零改动保持绿=收尾语义零变化实证)+ui_tune_probe 单探针绿(probe_gate --full 段承载) - [ ] 自检:新增头注/注释零变更叙事、S11 禁词零命中 -取用:T0 重锚行号。产出:`_batch_finished` 信号与 `_step_frame`/`_finish_batch` 签名(T2 对拍新形态即产品码本体);wd8 转绿读数。 +取用:T0 重锚行号。产出:`_batch_finished` 信号与 `_step_frame`/`_finish_batch` 签名(T2 对拍新形态即产品码本体);wd8 保持绿读数。 **T2 双实现对拍夹具(0.25–0.5 工作日)** -- [ ] 建 `game/tests/unit/presentation/tune_sim_batch_parity_test.gd`(extends SceneTree,Kit 收尾,全类型标注):内类 FakePanel(Node——wd8 同形 setter 面,另带进度串/报告记录数组)+FakeDriver(RefCounted——固定返回值两档〔victory/defeat,同 wd8 :53-55 取值〕,零随机;逐局记录帧索引与运行态读数,帧索引经宿主注入的计数 Callable 读取)+AwaitLoopRunner(RefCounted——现 `run()` await 形态逐字副本,含自身 `is_running()` 镜像;**与产品码的申报差异仅两处**:尾部两行按还原语义置收尾、批末把 stats 六键束存夹具成员供对拍读取) +- [ ] 建 `game/tests/unit/presentation/tune_sim_batch_parity_test.gd`(extends SceneTree,Kit 收尾,全类型标注):内类 FakePanel(Node——wd8 同形 setter 面,另带进度串/报告记录数组)+FakeDriver(RefCounted——固定返回值两档〔victory/defeat,同 wd8 :53-55 取值〕,零随机;逐局记录帧索引与运行态读数,帧索引经宿主注入的计数 Callable 读取)+AwaitLoopRunner(RefCounted——现 `run()` await 形态逐字副本,含循环内 await 与收尾四语句在 run() 尾的现行形态(:47-54 语句逐字),含自身 `is_running()` 镜像;**与产品码新形态的申报差异仅一处**:批末把 stats 六键束存夹具成员供对拍读取——产品码 stats 经报告公开面观测,第 17 节直查受控例外申报) - [ ] 对拍驱动:两形态各经 `run(n)` 真实入口驱动(新形态=真 TuneSimScript 直构,旧形态=夹具直构;同参假面板/假驱动各自实例);拓扑钉死——测试宿主先连 process_frame 帧计数、后启动两形态(两形态下「计数先于步进体」相对次序同构,序列可比);n=3 常规+n=0 退化两案 - [ ] 五面对拍断言(落点 `_init`,尾缀〔测速 V1〕..〔测速 V5〕):V1 帧索引序列——两形态逐局记录序列逐位相等(每步恰一帧+相位同点的执行证据);V2 运行态序列——驱动内 `is_running()` 读数序列逐位相等+收尾双运行态假相等;V3 进度串序列——面板记录的 set_sim_progress 串序列逐位相等;V4 stats 六键逐位——夹具暴露 stats 与测试内按假驱动固定返回值重算的六键期望(wins/dmg_all/hp_left_win/hp_boss/rounds_all/death_nodes)逐键相等;V5 报告全文逐字——两形态面板记录的 set_sim_report 串逐字相等,n=3 案含「关卡完成率/关卡完成剩余 HP 均/败北节点/N=」渲染面(n=0 案只断逐字相等——空 stats 渲染无剩余 HP 行,`_mean`/`_p90` 空守卫实查在案) - [ ] `run_all.gd` manifest 增行 `{"path": "res://tests/unit/presentation/tune_sim_batch_parity_test.gd", "expect": 0}` @@ -147,27 +147,27 @@ - [ ] 关联档复跑:`--only=standee_fx_sweep,tune_sim_batch_parity`+ui_tune_probe(probe_gate --full 段)全绿 - [ ] 批末代码域闸全绿:`tools/run_all_gates.sh --code`(改动含 game/ 下非 .md 行为载体——`--code` 档) - [ ] 错误路径声明缺口写入实现报告(第 4 节 🔴 条全文迁入——禁静默) -- [ ] 产出物落位卡:`reports/iter-实现报告-<日期>.md`(各卡状态+实机目检总清单〔本批零目检项〕+遗留读数义务+实现决定记录)与 `reports/iter-反哺点清单-<日期>.md`(第 6 节同步面逐条落点)写齐,方算收口 +- [ ] 产出物落位卡:`reports/iter44-实现报告-2026-10-30.md`(各卡状态+实机目检总清单〔本批零目检项〕+遗留读数义务+实现决定记录)与 `reports/iter44-反哺点清单-2026-10-30.md`(第 6 节同步面逐条落点)写齐,方算收口 -编队注记:本卡与零豁免其余三卡无产品码触面交集(本卡触面=tune_sim_controller.gd+game/tests/ 两件);run_all.gd manifest 行与并行批可能交叠——编队归编排段(待办清单条目口径:编队待迭代 42 合回)。 +编队注记:本卡与零豁免其余三卡无产品码触面交集(本卡触面=tune_sim_controller.gd+game/tests/ 两件);run_all.gd manifest 行与并行批可能交叠——编队归编排段。本卡在 iter44 worktree 施工(卡①卡② 已收口冻结在分支),T0 重锚以 worktree 基面为准。 ## 12. 验证闸——改动—验收对照表 -域口径:本批改动全为 `game/` 下非 .md 行为载体——批末跑代码域闸;里程碑步(T 段)出口跑一次全套闸,波内修复只做出红最小单元的最窄确认,新增测试先行孤立验证再并入全套(测试执行两入口铁规不破)。三件保险(冒烟/系统测试/静态扫描)恒跑;02 单元与集成按关联档选套件——**本卡显式申报套件:standee_fx_sweep_test(wd8——套件名与涉案文件无字面关联,diff 推导可能漏选,特此申报)+tune_sim_batch_parity_test(新增)**。测试方式两分法:本批全部调试接口自动化(假驱动/假面板直构+真实按钮信号链探针),零用户目检项(tune_sim 全链路发布态不可达——登记不阻塞条款零登记项)。**静态判据申报(不入对照表,test-conventions §8.4 出口)**:S13 await-in-loop 裸键消除无独立运行期提交面——判据=`python tools/gdstyle_gate.py` 与 `game/tests/static_scan.sh` 退出码(T3 执行),属静态判据+评审注记申报观测面局限;对拍夹具不在 S13/S8/S9/S10 扫描面的声明同此口径(扫描面=产品码三目录与 B1 九文件清单,static_scan.sh 实查在案)。**声明缺口(不入对照表——证明做不出出口)**:脚本错误中断路径差异不可消除,见第 4 节 🔴 条,实现报告文档化(T3)。**能力清单**:本批新增能力一行「调参测速批量模拟(逐帧步进与统计读数)」已随本卡写卡段同批入表(写卡同批义务,grilling 复核订正原「既有名锚定+后续入表」悬置形态)——对照表三行能力列改锚该行;manifest 计数行随套件增减浮动。 +域口径:本批改动全为 `game/` 下非 .md 行为载体——批末跑代码域闸;里程碑步(T 段)出口跑一次全套闸,波内修复只做出红最小单元的最窄确认,新增测试先行孤立验证再并入全套(测试执行两入口铁规不破)。三件保险(冒烟/系统测试/静态扫描)恒跑;02 单元与集成按关联档选套件——**本卡显式申报套件:standee_fx_sweep_test(wd8——套件名与涉案文件无字面关联,diff 推导可能漏选,特此申报)+tune_sim_batch_parity_test(新增)**。测试方式两分法:本批全部调试接口自动化(假驱动/假面板直构+真实按钮信号链探针),零用户目检项(tune_sim 全链路发布态不可达——登记不阻塞条款零登记项)。**静态判据申报(不入对照表,test-conventions §8.4 出口)**:S13 await-in-loop 裸键消除无独立运行期提交面——判据=`python tools/gdstyle_gate.py` 与 `game/tests/static_scan.sh` 退出码(T3 执行),属静态判据+评审注记申报观测面局限;对拍夹具不在 S13/S8/S9/S10 扫描面的声明同此口径(扫描面=产品码三目录与 S8 文件集,static_scan.sh 实查在案)。**声明缺口(不入对照表——证明做不出出口)**:脚本错误中断路径差异不可消除,见第 4 节 🔴 条,实现报告文档化(T3)。**能力清单**:本批新增能力一行「调参测速批量模拟(逐帧步进与统计读数)」已随本卡 v1.0 写卡段同批入表(README :131)——对照表三行能力列锚该行;manifest 计数行随套件增减浮动。 | 行 | 需求点 | 改动(文件:行/行为) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 能力行 | |---|---|---|---|---|---|---|---| -| V1 | 步进状态机改造(循环内 await 消除的落码面) | tune_sim_controller.gd :36-52 改形(run 函数级单次 `await _batch_finished`,全文件零循环)+:56-68 拆出 `_step_frame`/`_finish_batch` | 复用 wd8 驱动形态 `await ctrl.run(2)`(:131——run 协程身份契约,零改动);新增对拍 V4/V5(stats 六键逐位+报告逐字,落点 tune_sim_batch_parity_test.gd:_init,尾缀〔测速 V4/V5〕) | 单元 | TuneSimScript 直构(字典夹具 db+假面板+假驱动,wd8 :129 同款)经 `run(n)` 真实入口;对拍两形态同入口 | run 可等待(await 挂起=整批完成放行)+stats 六键逐位+报告全文逐字 | 新增:调参测速批量模拟(逐帧步进与统计读数) | +| V1 | 步进状态机改造(循环内 await 消除的落码面) | tune_sim_controller.gd :36-54 改形(run 函数级单次 `await _batch_finished`,全文件零循环;收尾四语句 :51-54 迁入 `_finish_batch`)+:58 起新增 `_step_frame`/`_finish_batch` | 复用 wd8 驱动形态 `await ctrl.run(2)`(:144——run 协程身份契约,零改动);新增对拍 V4/V5(stats 六键逐位+报告逐字,落点 tune_sim_batch_parity_test.gd:_init,尾缀〔测速 V4/V5〕) | 单元 | TuneSimScript 直构(字典夹具 db+假面板+假驱动,wd8 :142 同款)经 `run(n)` 真实入口;对拍两形态同入口 | run 可等待(await 挂起=整批完成放行)+stats 六键逐位+报告全文逐字 | 新增:调参测速批量模拟(逐帧步进与统计读数) | | V2 | 双实现对拍(帧相位等价的执行化证据) | tune_sim_batch_parity_test.gd 新建+run_all.gd manifest 增行 | 新增五面对拍断言〔测速 V1–V3〕:帧索引序列/运行态序列/进度串序列逐位相等(落点 tune_sim_batch_parity_test.gd:_init) | 单元 | 两形态各经 `run(n)` 真实入口(n=3 常规+n=0 退化);帧索引经测试宿主 process_frame 计数 | 两形态五面记录序列逐位相等(每步恰一帧+相位同点的当轮执行证据——判据本体) | 新增:调参测速批量模拟(逐帧步进与统计读数) | -| V3 | 行为改动——收尾置假还原(已申报唯一行为改动) | tune_sim_controller.gd :67-68 删(每局置假)→ `_finish_batch` 收尾单次置假 | 复用 wd8 三断言〔wd8〕(`drv.seen == [true, true]`/收尾 `ctrl.is_running()` 假+`panel.running` 假/报告非空——零改动)+ui_tune_probe ⑤ 守卫环与输出四串(:63-83——零改动) | 单元+探针 | wd8 假驱动回调读 `is_running()`(`await ctrl.run(2)`);探针 `press_simulate_for_probe()` → 按钮 pressed 信号链 → `run(10)` → 守卫环逐帧复查 | 驱动逐局运行态读数序列 [true, true]+收尾双运行态假+守卫环 1800 帧预算内退出+`output_text()` 四关键串(浮窗运行态贯穿整批的可观测提交面) | 新增:调参测速批量模拟(逐帧步进与统计读数) | +| V3 | 运行态语义零变化(收尾迁移的回归锚面——非行为改动行) | 收尾四语句(:51-54)迁入 `_finish_batch` 同帧同相位——非行为改动判断依据:语句集与语义零变化、对两断言面不可观测,等价证据由对拍 V2/V5 承载;`_run_one_level` :58-68 零触碰 | 复用 wd8 三断言〔wd8〕(:145-147——`drv.seen == [true, true]`/收尾双运行态假/报告非空——零改动**保持绿**)+ui_tune_probe ⑤ 守卫环与输出四串(:63-83——零改动保持绿) | 单元+探针 | wd8 假驱动回调读 `is_running()`(`await ctrl.run(2)`);探针 `press_simulate_for_probe()` → 按钮 pressed 信号链 → `run(10)` → 守卫环逐帧复查 | 驱动逐局运行态读数序列 [true, true]+收尾双运行态假+守卫环 1800 帧预算内退出+`output_text()` 四关键串(浮窗运行态贯穿整批的可观测提交面——绿=保持绿,非转绿) | 新增:调参测速批量模拟(逐帧步进与统计读数) | -反向核对:diff 中行为改动共一处(V3 行);其余为非行为改动——V1 结构改形(零漂移判据=对拍五面全等+wd8/probe 零改动转绿)、V2 测试面新增、manifest 增行(测试基建)。孤儿断言:对拍五断言挂 V1/V2 两行,无孤儿。 +反向核对:diff 中行为改动共零处;其余为非行为改动——V1 结构改形(零漂移判据=对拍五面全等+wd8/probe 零改动保持绿)、V2 测试面新增、V3 回归锚面行(非行为改动)、manifest 增行(测试基建)。孤儿断言:对拍五断言挂 V1/V2 两行,wd8/probe 既有断言挂 V3 行,无孤儿。 ## 13. 异常分级 - **L1 自救**:编译错、断言尾缀措辞、夹具记录数组名笔误——就地修复,重跑最窄单元确认。 - **L2 记录继续**:T0 套件因在册无关债先红于 wd8 断言(记录可达性事实,不借批顺手修);行号重锚漂移(就地改卡记录);产品码 process_frame 订阅面发现第三处(记录+评审注记——非 run 途中新增场景不红)。 -- **L3 停机写报告等用户**:①T0 潜伏红证伪——两面实跑全绿=「每局置假系搬运卷入」前提崩塌,停卡退回写卡段重裁行为改动申报面;②等价性对拍失败——五面任一不等=测量语义漂移,设计前提崩塌;③机检预算互斥——改造后实测键超第 9 节预算或基线外新键=上游缺陷停卡退回写卡段(含禁为机检保留无效成员条款冲突);④S13 对账 await 键未消或冒新键;⑤判据绑不上可观测提交面(证明做不出=停卡呈报,禁自造注入破坏或回退取证)。 +- **L3 停机写报告等用户**:①T0 两面任一红=现状与重裁前提失配(迭代 41 修复被回退或新潜伏红),停卡呈报;②等价性对拍失败——五面任一不等=测量语义漂移,设计前提崩塌;③机检预算互斥——改造后实测键超第 9 节预算或基线外新键=上游缺陷停卡退回写卡段(含禁为机检保留无效成员条款冲突);④S13 对账 await 键未消或冒新键;⑤判据绑不上可观测提交面(证明做不出=停卡呈报,禁自造注入破坏或回退取证)。 ## 14. 代拍板白名单(逐条列,超清单记实现报告「实现决定记录」) @@ -175,20 +175,20 @@ 2. 头注 `_batch_finished` 契约行措辞(信号语义与发射时点不变)。 3. `_finished` 成员零读者确认后删去(成员 11→10——键账只降不升,行为与断言面零变化)。 4. 夹具假面板/假驱动成员命名微调(记录语义不变;与 wd8 同形自建、不跨套件共享)。 -5. `_finish_batch` 收尾段四语句相对次序按第 11 节 T1 草图次序落码(对两个断言面不可观测——实现期查 git 历史原序可进一步对齐)。 +5. 收尾段语句相对次序——现行 run() 尾次序(close→报告→置假→_running)与设计草图次序(close→置假→_running→报告)对两个断言面均不可观测,两种皆可落码(实现期查 git 历史原序可进一步对齐);`_finished = true` 置位为新增首句(成员写,不可观测)。 6. 排版机械改写(空行、preload 顺序、注释换行)。 ## 15. 待裁点 -零待裁点,开工门=U0。(裁决记录:2026-10-29 用户裁四点——①信号步进状态机形态采纳〔SceneTreeTimer 链/Timer 节点/递归协程/线程四形态淘汰依据见设计文档 §3〕;②收尾置假还原作为已申报行为改动随批实施;③双实现对拍入批;④脚本错误中断路径差异接受并文档化。) +零待裁点,开工门=U0。(裁决记录:2026-10-29 用户裁四点——①信号步进状态机形态采纳〔SceneTreeTimer 链/Timer 节点/递归协程/线程四形态淘汰依据见设计文档 §3〕;②收尾置假还原作为已申报行为改动随批实施【已作废——前提被迭代 41 测试债批 215da315 消化,v1.1 剥离】;③双实现对拍入批;④脚本错误中断路径差异接受并文档化。2026-10-30 停卡重裁(用户裁,按停卡呈报建议案逐条落卡):剥离行为改动申报 V3、T0 义务改现状复核、夹具申报差异两处减一处;重裁沿用 2026-10-29 grilling 复核门结论(reports/复核-grilling-零豁免五卡-2026-10-29.md,修正全落),重裁差异面未新增设计裁决。) ## 16. 测试削弱申报 -无申报项——wd8 三断言与 ui_tune_probe ⑤ 零改动零削弱;对拍五断言为新增非改判;夹具与产品码的两处申报差异(收尾还原语义+stats 暴露成员)系新增件自身设计、设计文档 §5 在案,非既有断言削弱。禁删断言、禁放宽阈值、禁缩小扫描面、禁把真实入口换成下层直调;未申报的削弱由独立验收审计判红。 +无申报项——wd8 三断言与 ui_tune_probe ⑤ 零改动零削弱(保持绿);对拍五断言为新增非改判;夹具与产品码新形态的申报差异仅一处(夹具暴露 stats 供对拍读取)系新增件自身设计、设计文档 §5 在案,非既有断言削弱。禁删断言、禁放宽阈值、禁缩小扫描面、禁把真实入口换成下层直调;未申报的削弱由独立验收审计判红。 ## 17. 独立验收审计 -合并前由未参与实现的会话或子代理执行;报告落 `reports/iter-验收审计-<日期>.md`,未过不得合并。审计至少做五件事:①逐行核对第 12 节对照表(测试存在、真的执行、尾缀可 grep、真实入口未绕过改动点);②抽查判据绑定面——帧索引序列是否真从 process_frame 发射点记录、对拍拓扑两形态是否同构可比、V3 行判据是否钉在运行态提交面而非终态兜底;③核对 T0 定谳证据在案(改动前 wd8/probe 红读数——潜伏红链条完整,缺 T0 读数=审计不过);④核对行为改动唯一性——diff 中除 V3 外无第二处行为改动;⑤检查测试削弱(对照第 16 节零申报)与内部字段直查受控例外(本批零直查——新形态 stats 经报告公开面观测)。 +合并前由未参与实现的会话或子代理执行;报告落 `reports/iter44-验收审计-2026-10-30.md`,未过不得合并。审计至少做五件事:①逐行核对第 12 节对照表(测试存在、真的执行、尾缀可 grep、真实入口未绕过改动点);②抽查判据绑定面——帧索引序列是否真从 process_frame 发射点记录、对拍拓扑两形态是否同构可比、V3 行判据是否钉在运行态提交面(保持绿证据,非终态兜底);③核对现状复核证据在案(T0 两面实跑绿读数+停卡呈报三重证据引注〔215da315 提交信息〕——前提消化链条完整,缺 T0 复核读数=审计不过);④核对行为改动零处——diff 中任何行为改动(含收尾四语句语义漂移)=审计不过,收尾迁移的等价性由对拍 V2 运行态序列+V5 报告逐字承载;⑤检查测试削弱(对照第 16 节零申报)与内部字段直查受控例外(本批零直查——新形态 stats 经报告公开面观测)。 ## 18. MC/数值与 RNG 零触碰声明 diff --git a/tools/render_gate.sh b/tools/render_gate.sh index b39e209a..d6443f25 100644 --- a/tools/render_gate.sh +++ b/tools/render_gate.sh @@ -18,6 +18,10 @@ # ④ 基准档 1280 单档(Q4)。1920 档注记:16:9 全屏下 stretch=canvas_items/aspect=keep 等比 # 1.35×+左右黑边各 96px,构图与基准档一致(分辨率案甲)——分辨率维度不重复建闸。 # (1920 布局机检面现无承载,待补。) +# ⑤ 脚本错误扫描(三硬闸之二):shot/fx/board 三段 Godot 日志逐段过 tools/error_scan.py, +# 命中 SCRIPT ERROR/Parse Error 即红(标记与 run_smoke.py 同源);python 工具自身输出不扫。 +# 此类错误不改退出码、不计失败数,且会中断所在函数的剩余断言——只能由外部扫日志。 +# 新增日志段必须同步加扫描调用,漏加=该段静默错误漏网。 # 过程产物纪律(Q2):一切截图 PNG 落 mktemp 临时区,闸毕即弃——任何过程 PNG 禁入 git; # 例外=game/tests/golden/ 基线(测试资产入库,.gitignore 已核不挡)。 # 窗口化运行会短暂弹 Godot 窗口(本机可开窗口径);禁改 --headless 跑本闸 @@ -41,6 +45,15 @@ kill_orphans() { local p; for p in $(orphans); do taskkill //F //PID "$p" >/dev/null 2>&1; done } +scan_log() { # scan_log <段名> <日志>——脚本错误扫描;日志不可读(error_scan exit 2)同判红 + local name="$1" log="$2" + if ! python "$REPO/tools/error_scan.py" "$log" >"$TMP/err_scan.txt" 2>&1; then + echo "[render_gate] FAIL — $name 脚本错误扫描未通过(命中或日志不可读):" + sed -n '1,6p' "$TMP/err_scan.txt" | sed 's/^/ /' + _rc=1 + fi +} + # 冷检出刷导入(.godot 不入 git,fresh worktree 首跑一次性付) if [ ! -f "$GAME/.godot/uid_cache.bin" ]; then echo "[render_gate] 冷检出:editor --quit 刷导入(一次性,可到 1-2 分钟)" @@ -66,6 +79,7 @@ if [ "$_rc" -eq 0 ]; then _rc="${PIPESTATUS[0]}" [ "$_rc" -ne 0 ] && echo "[render_gate] FAIL — shot_checker A-D 断言存在红(S1 停机点:首跑存量 FAIL 全量呈报登记待办清单)" fi +scan_log "shot_probe" "$TMP/shot.log" echo "======== fx_shot_probe 三时刻取证(Q7) ========" FX="$TMP/fxshots" @@ -77,6 +91,7 @@ if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then elif [ "$st" -ne 0 ]; then echo "[render_gate] FAIL — fx_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1 fi +scan_log "fx_shot_probe" "$TMP/fx.log" fx_evidence() { # 取证双检:模式文件存在且非零字节(零字节/缺失任一即 FAIL——Q7) local pat="$1" min="$2" n=0 f for f in "$FX"/$pat; do @@ -101,6 +116,7 @@ if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then elif [ "$st" -ne 0 ]; then echo "[render_gate] FAIL — board_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1 fi +scan_log "board_shot_probe" "$TMP/bd.log" for b in board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png board_meta.json; do if [ ! -s "$BD/$b" ]; then echo "[render_gate] FAIL — 取证缺失/零字节: $b"; _rc=1; fi done diff --git a/tools/run_all_gates.sh b/tools/run_all_gates.sh index 05885698..86c5dbfc 100644 --- a/tools/run_all_gates.sh +++ b/tools/run_all_gates.sh @@ -11,11 +11,11 @@ # 回执即作废须重跑;跑闸时若代码面还带着未提交改动,回执会标 dirty_game,合回闸拒绝采信。 # # 用法:tools/run_all_gates.sh [--code|--docs|--quick] -# --code 只跑代码域闸(00 冒烟测试/01 系统测试/02 单元与集成测试/03 探针 --click/ -# 04 探针 --full/05 渲染闸/09 静态扫描+10 熵增闸) +# --code 只跑代码域闸(00 冒烟测试/01 系统测试/02 单元与集成测试/03 场景探针/ +# 05 渲染闸/09 静态扫描+10 熵增闸) # ——本批改动含 game/ 下非 .md 的行为载体(代码/测试脚本/探针/场景/数据表/素材)时用。 # 关联档选闸(2026-10-29 用户裁,推广自 2026-10-28 影响面选闸):00 冒烟/01 系统测试/ -# 09 静态扫描三件保险恒跑不降;03/04 探针与 05 渲染闸维持影响面口径(本批 game/ 改动 +# 09 静态扫描三件保险恒跑不降;03 场景探针与 05 渲染闸维持影响面口径(本批 game/ 改动 # 全在测量面 game/tests、game/sim 时记 SKIP);02 单元与集成改「选套件跑」——套件集= # 需求卡片申报 ∪ diff 推导并集(算法与套件族映射表见脚本中部),横切面(公共件/数据表/ # autoload/数值·存档·RNG·结算触面)回落全套件;回执 scope 字段记 full|related。 @@ -218,7 +218,7 @@ suite_terms_for() { # suite_terms_for <仓库相对路径> → stdout: 套件 # ── 关联档选闸(2026-10-29 用户裁,推广自 2026-10-28 影响面选闸)── # 三件保险恒跑不降:00 冒烟/01 系统测试/09 静态扫描(10 熵增闸照旧两域都跑)。 -# 03/04 探针与 05 渲染闸维持影响面口径:本批 diff 相对 master 分叉点的 game/ 改动若全部落在 +# 03 场景探针与 05 渲染闸维持影响面口径:本批 diff 相对 master 分叉点的 game/ 改动若全部落在 # 测量面(game/tests/**、game/sim/**——测试与模拟驱动件,不触场景树与渲染输出)则记 SKIP。 # 02 单元与集成改「选套件跑」:套件集=申报 ∪ 推导 取并集—— # 申报:在册需求卡片(本批 diff 触及的 reports/需求卡片*.md ∪ roadmap「进行中」迭代行登记的 @@ -241,7 +241,7 @@ if in_domain code; then _behavior="$(echo "$_gfiles" | grep -vE '^(game/tests/|game/sim/)' || true)" if [ -n "$_gfiles" ] && [ -z "$_behavior" ]; then NONBEHAVIOR=1 - echo "[gates] 影响面选闸:game/ 改动全在测量面(tests/sim)——03/04 探针/05 渲染闸 → SKIP(00/01/09 三件保险恒跑)" + echo "[gates] 影响面选闸:game/ 改动全在测量面(tests/sim)——03 场景探针/05 渲染闸 → SKIP(00/01/09 三件保险恒跑)" fi # ── 02 选套件推导(只对行为面代码文件做——测量面文件不进推导,其验证就是 02/09 自身)── _codefiles="$(echo "$_gfiles" | grep -viE '\.md$' | grep -vE '^(game/tests/|game/sim/)' || true)" @@ -412,15 +412,12 @@ gate "00 冒烟测试" "smoke_boot.log" "$PY" "$REPO/tools/run_smoke.py" gate "01 系统测试" "system_test.log" "$PY" "$REPO/tools/run_system_test.py" # ── 2. 单元与集成测试 ── gate_unit -# ── 3. 场景探针 · 行为走查段 ── -# ── 4. 场景探针 · 断言段(iter27 漏跑的就是这一段)── +# ── 3. 场景探针(走查+断言十件一次跑齐;iter27 漏跑断言段事故后固化为无条件整闸)── if [ $NONBEHAVIOR -eq 1 ]; then - NAMES+=("03 探针 --click"); EXITS+=("SKIP"); LOGS+=("") - NAMES+=("04 探针 --full"); EXITS+=("SKIP"); LOGS+=("") - echo ""; echo "[gates] 03/04 探针 → SKIP(影响面选闸:测量面改动不触场景树与探针断言面)" + NAMES+=("03 场景探针"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 03 场景探针 → SKIP(影响面选闸:测量面改动不触场景树与探针断言面)" else - gate "03 探针 --click" "probe_click.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --click - gate "04 探针 --full" "probe_full.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --full + gate "03 场景探针" "probe.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --click --full fi # ── 5. 渲染面(窗口化,依赖 PIL)── if [ $QUICK -eq 1 ] || [ $NONBEHAVIOR -eq 1 ]; then diff --git a/待办清单.md b/待办清单.md index 0b1cb2f6..f5df4bf2 100644 --- a/待办清单.md +++ b/待办清单.md @@ -92,9 +92,9 @@ **状态**:待执行 -#### 闸基线 02/09 两键换代——存量红指纹已随迭代 41 测试债批归绿(2026-10-29 登记,来源=批次移交) +#### 闸基线 02/05/09 三键换代——存量红指纹已消除或现行不复现(2026-10-29 登记、2026-10-30 并入 05,来源=批次移交) -**实况**:证据=迭代 41 测试债批代码域闸实测(worktree 分支 `testdebt-20261029`,回执 commit afd1a41f、域=code、scope=full、零新增红),02 单元与集成与 09 静态扫描两闸判「基线红 → 现绿(改善)」;基线 `game/tests/.gates/baseline.json` 里这两键仍记着已消除的存量红指纹(02 fp f9865f1a5a44=9 套件失败;09 fp 1a9ad773489f=S9 24 处+S10 4 处+S11 1 处),留着不动的后果=日后同类红回归会被判「存量红·对基线零新增」静默放行。待做=两键按归绿实测重生成(写 exit=0/绿指纹),改的是 `game/` 下非 .md 文件=代码面,须随下一个代码批在 worktree 内一并落、禁主区直改;人工确认凭据=上述回执与 reports/迭代41测试债批-实现报告-2026-10-29.md 第三节。本条目无设计段(机械重锚),不走写卡与编排。 +**实况**:证据=迭代 41 测试债批代码域闸实测(worktree 分支 `testdebt-20261029`,回执 commit afd1a41f、域=code、scope=full、零新增红),02 单元与集成与 09 静态扫描两闸判「基线红 → 现绿(改善)」;05 渲染闸在 2026-10-30 脚本错误扫描批主区真跑 `bash tools/render_gate.sh` exit=0 全绿(shot/fx/board 三段扫描零命中、残留 Godot=0、golden changed 0.00%),其红键在现行主区不复现。基线 `game/tests/.gates/baseline.json` 里这三键仍记着历史红指纹(02 fp f9865f1a5a44=9 套件失败;05 fp de89589e49ed;09 fp 1a9ad773489f=S9 24 处+S10 4 处+S11 1 处),留着不动的后果=日后同类红回归会被判「存量红·对基线零新增」静默放行。待做=三键按归绿实测重生成(写 exit=0/绿指纹),改的是 `game/` 下非 .md 文件=代码面,须随下一个代码批在 worktree 内一并落、禁主区直改;05 须在素材齐盘预检后跑出真值(冷检出空盘态下的渲染红不作数)。人工确认凭据=上述回执、reports/迭代41测试债批-实现报告-2026-10-29.md 第三节与本批提交说明。本条目无设计段(机械重锚),不走写卡与编排。 **改动面**:游戏代码 @@ -110,12 +110,12 @@ #### 零豁免五键清偿批——S13 余量五键真修四卡(2026-10-29 登记,来源=会话提及) -**实况**:用户 2026-10-29 裁「不允许任何豁免」——S13 三条豁免注销(architecture.md v1.25 落文),五键真实修复至阈值内:main.gd 类变量 49→15(组合根注册面:ui_registry 一门面+三域容器,约 867 处探针/系统测试引用改址 `main.ui.<域>.<句柄>`,7 别名/转发成员消灭)/battle_orchestrator 类变量 38→15(RoundCycle+BattleFacts 两件,叠加迭代 42 轴丙 BoardInstaller;encounter 归 BattleFacts、attune 改判状态两成员随件)/board_cell_unit 类变量 30→10+standee_body 类变量 19→10(各拆两独立文件,gdstyle inner-class 展平计数口径)/tune_sim 循环内 await 消除(process_frame 信号步进状态机;含**申报行为改动**一处:运行态收尾由每局置假还原为整批收尾单次置假——消除与 wd8/ui_tune_probe 断言面的静态矛盾,T0 实证定谳)。设计闭合凭据=architecture.md v1.25+五份设计文档(reports/ 下五件 设计-组合根注册面-2026-10-29.md/设计-战斗编排类变量归约-2026-10-29.md/设计-单格绘制状态件-2026-10-29.md/设计-站桩状态件-2026-10-29.md/设计-测速循环去await-2026-10-29.md)。四卡已成文(均零待裁点开工门 U0): +**实况**:用户 2026-10-29 裁「不允许任何豁免」——S13 三条豁免注销(architecture.md v1.25 落文),五键真实修复至阈值内:main.gd 类变量 49→15(组合根注册面:ui_registry 一门面+三域容器,约 867 处探针/系统测试引用改址 `main.ui.<域>.<句柄>`,7 别名/转发成员消灭)/battle_orchestrator 类变量 38→15(RoundCycle+BattleFacts 两件,叠加迭代 42 轴丙 BoardInstaller;encounter 归 BattleFacts、attune 改判状态两成员随件)/board_cell_unit 类变量 30→10+standee_body 类变量 19→10(各拆两独立文件,gdstyle inner-class 展平计数口径)/tune_sim 循环内 await 消除(process_frame 信号步进状态机;**零行为改动**——v1.0 申报的收尾置假还原已被迭代 41 测试债批 215da315 先行完成并申报,2026-10-30 实现段按卡片预设停机点停卡、三重证据定谳前提已消化,按停卡呈报建议案重裁剥离行为改动申报:iter44 worktree 分支 reports/iter44-实现报告-2026-10-30.md §5)。设计闭合凭据=architecture.md v1.25+五份设计文档(reports/ 下五件 设计-组合根注册面-2026-10-29.md/设计-战斗编排类变量归约-2026-10-29.md/设计-单格绘制状态件-2026-10-29.md/设计-站桩状态件-2026-10-29.md/设计-测速循环去await-2026-10-29.md)。四卡已成文(均零待裁点开工门 U0;测速循环批卡 2026-10-30 停卡重裁 v1.1——前提消化后剥离行为改动申报、T0 改现状复核、夹具申报差异两处减一处,仍零待裁点): 关联文档清单: - 组合根注册面批 reports/需求卡片-组合根注册面批-2026-10-29.md - 呈现层状态件批 reports/需求卡片-呈现层状态件批-2026-10-29.md(单格+站桩两设计并一卡) -- 测速循环批 reports/需求卡片-测速循环批-2026-10-29.md +- 测速循环批 reports/需求卡片-测速循环批-2026-10-29.md(v1.1,2026-10-30 停卡重裁) - 战斗编排归约批 reports/需求卡片-战斗编排归约批-2026-10-29.md(接收方=在册条目「战斗域结构深拆批」宿主瘦身子项,不在本条目记账) 编队待迭代 42 合回(battle_orchestrator/board_view 触面碰撞约束),具体迭代归属归编排段;编入前置=各卡过 grilling 复核门。gdstyle inner-class 计数截断缺陷已在册(architecture v1.25)——基线登记值欠估,各批按真实口径施工;各批全部合回后一次人工 --refresh --force 收账(一并清迭代 42 归绿 6 键离账——批末实测已归绿而基线仍记 16 键:board_view 四键+level_runner `_enter_node`+battle_orchestrator 文件长;各留债键按重锚时实测重登记,冻结禁恶化口径不变)。2026-10-29 编排(用户裁编法):卡 A 组合根注册面批+卡 B 战斗编排归约批+卡 D 测速循环批编入**迭代 44**(≈7.875 个工作日,中值合计);卡 C 呈现层状态件批编入**迭代 45**(与补件批同批,卡 C 先于其 board_hover 半边施工)。grilling 复核门已过(reports/复核-grilling-零豁免五卡-2026-10-29.md,五卡有条件成立、修正全落,提交 9855bf0a)。 @@ -327,9 +327,11 @@ **实况**:扩板已落地(四档板型 10/12/13/14 在跑),2026-10-26 文档面已按单点维护归一:①格边长口径的唯一出处收敛到 [战斗屏规格](design/gdd/ux/battle.md)「棋盘格尺寸公式」——公式含格间距与垂直预留,可读下限 30px,1280 档算例 43/36/33/30 标注为随布局变化的非契约值;②[美术圣经](design/art/art-bible.md) §8.1 改为指路 battle.md、只锁资产侧两条比例,§8.2 甲类整数倍纪律已剔除砖面 tile(瓦片随格边长整格铺满,允许非整数倍);③[HUD框架](design/gdd/systems/表现与支撑/HUD框架.md) 只留装板可行性不等式,删去 36/45 定值档与旧算例;④demo 原型 44px 那一路随 `src/` 退役消失。剩余为代码面两处与口径不同步:`game/probe/layout_probe.gd:15` 的格子可读门槛 `MIN_CELL := 36.0` 仍取 12×12 的旧锚点值,比 battle.md 的可读下限 30px 严——现探针只进 10×10 的 L1 故仍全绿,一旦覆盖 13×13(33px)与 14×14(30px)即会误报红;`game/tests/unit/core/board_gen_reward_test.gd:53` 的格边长冒烟只跑 L1/L2,13×13 与 14×14 无覆盖。断言改动后按 `game/tests/README.md`「能力清单」核对受影响行(验证集清单已退役——test-conventions §8.3)。 +- 需求卡片:[需求卡片-棋盘格边长口径收口批-2026-10-30](reports/需求卡片-棋盘格边长口径收口批-2026-10-30.md)(2026-10-30 已写卡,零待裁点——算式单点提取经 `BoardView._cell_for`/探针门槛 36→30 对齐 battle.md 可读下限/冒烟扩 13×13 与 14×14;探针不扩到 L3,最大板可读面改由实板面积推定断言承担) + **改动面**:游戏代码 -**状态**:待写卡 +**状态**:已写卡 #### 立绘清晰度回退评估——是否改用高清图(2026-10-13 登记,来源=playnote) @@ -364,15 +366,6 @@ **状态**:待美术设计(备选) -#### 在途草稿整理批——3349 个失管草稿(2026-10-06 登记,来源=审计发现) - -**实况**:审计 P2-1——`artwork/ui-reuse-2026-09-23/` 独占 3043 个草稿(91%),vN 命名率 0%,冗余 41 文件,残留非素材文件(.url ×8 / .ogg ×6 / .ai ×2 / .swf ×1);根因=脚本链覆盖「生成→验证」但**缺「草稿整理」段**。整理涉及**批量文件移动/删除**,按用户惯例须先 dry-run 呈报清单、经确认再分批执行。执行窗口随实现批;开工=用户点名。 -- 相关文档:[art-bible](design/art/art-bible.md) §8.3 归档纪律(原则已写入)· 资产管线批(工具+文件操作) - -**改动面**:美术素材+工具代码 - -**状态**:待写卡 - #### 战斗区背景图替换——B 区舞台插画 + 临时光柱退役(2026-10-07 登记,来源=会话提及) **实况**:B 区舞台现行=程序化平色底(`UiKit.STAGE_BG`,`battle_stage_view.gd:47`)+ **顶光柱氛围层**(`:55-63` 四柱,x 基准 110/900/1055/1210@1280,60 段叠层矩形自顶向下渐宽窄渐隐〔顶 α26→0〕,层序=bg < 光柱 < 世界)——二者均系 §6.1 方向稿的**临时占位**,用户口径「光柱只是临时方案,换正式战斗背景后效果更好」。正式件已立资产契约:`stage/stage_bg.png`(源 1280×270,B 区全高含明细带底;三关换挡走 §4.3 `wall_ambient` tint 槽,不按板出三张),属审计 P1-2 查得的「B 区舞台背景插画 0 张」缺口。**光柱在背景到位后顺带退役或吸收进背景绘制**(不再作为独立程序层)。**与立绘探顶解耦**:大体档「允许向上溢出盒顶 N px」的判定(2026-10-07 用户裁甲案)**不依赖光柱是否存在**,换背景零返工。 @@ -384,6 +377,29 @@ **状态**:待美术设计 +#### 棋盘封印印记图形换代——现件仍是扫雷旗形,改法阵或锁链待设计(2026-10-30 登记,来源=会话提及) + +**实况**:用户口径=棋盘封印图标现在还是按旧扫雷的做法画成一个旗子,希望换成法阵那样的封印、或锁链;形未定,先做设计讨论再出图。现状证据: + +- 现件 `game/assets/art/board/seal_mark.png`(32×32 纯黑剪影)=经典扫雷旗形(旗面 + 旗杆)。2026-09-28 那次只定了色(旗朱→封印紫→封印黑 `#000000`),图形一直没动。 +- 消费面两处,换形同时受影响:棋盘侧 `game/presentation/views/tile_palette.gd:19`(`TILE_SEAL_MARK` 常量)→ `board_view.gd:616` 未开格封印槽绘制,盘上缺图时回落单字「封」(`board_view.gd:654-658`);战末弹层的「金蜡封徽记」复用同一张图(金底 52×52 面板 `battle_end_overlay.gd:65-69`,叠 32px 印记 `:84`)——蜡封徽记要不要跟着换是独立一裁。 +- 文档侧口径仍以旗为形:art-bible §4.1 封印黑条内含「旗杆米白保可读」这一旗形前提、§3.2 封印印记条只锁色与格内栈位;art-assets-checklist §4 封印特效行写「印记旗面走封印黑」「印记常显形态可与 §3.3 旗面像素化合并出」;ux/battle.md:128 呈现术语注记与 :270 悬停三态光标「封印(印记形)」。 +- 术语层已闭合,本条只欠呈现层:「旗」概念 2026-10-18 用户裁全库退役(动作=封印、印记=封印印记、板面单字=「封」),图形停在退役前的形态。 + +关联文档清单: +- 相关文档 [美术圣经](design/art/art-bible.md) v3.23——§3.2 封印印记条(:167)· §4.1 封印黑条(:202)· §1.2 格内优先级栈 · §1.4 格子样张 +- 相关文档 [美术资源依赖清单](design/art/art-assets-checklist.md)——§3.3 雷/封印印记/数字格面像素化(:118)· §4 封印特效行(:162) +- 相关文档 [美术素材状态清单](design/art/art-status-checklist.md)——P2「雷/封印印记/数字砖面全套」◐ · P3「封印特效」⬜ +- 相关文档 [战斗屏 UX](design/gdd/ux/battle.md)(Last Updated 2026-10-29)——:128 呈现术语注记 · :270 悬停预承诺三态光标 · :261 未开格封印交互行 +- 相关文档 [术语表](design/gdd/术语表.md) v1.25——「封印」条(:15)· 用语规范禁用字面条(:184) +- 相关目检项 [体验验证清单](体验验证清单.md) 第 4 条「封印印记贴图三关加载」(:317,现判据只验贴图是否加载、不验形状) + +**缺口**:还差一次封印印记图形的设计讨论(形未定,法阵/锁链两案并列,禁代裁),讨论须一并裁五点:①形=法阵(圆形阵纹)还是锁链(束缚感),或第三种方案;与危险格巢穴纹、奖励徽章的剪影语言(art-bible §3.1 直线几何体)互辨是否成立。②色=现封印黑 `#000000` 是 §4.1 不变槽成员,法阵若带发光或色相即与该条口径冲突,须重登记不变槽与对比度机检判据(现判据=未开格墙顶对纯黑 ≥4.4:1)。③尺寸与遮挡=32×32 内保持可读,格内优先级栈「数字 > 小人 > 封印印记」下法阵铺满整格是否侵入数字字形区(§3.2 验收句禁止)。④连带件=战末金蜡封徽记与悬停「封印(印记形)」光标是否同步换,还是各留一件。⑤与「封印特效=施印瞬间特效 + 印记常显形态」两件的关系=合并出一件还是分两件。裁决写入 art-bible §3.2/§4.1 与两份美术清单条款后,才可出样张与终稿(终稿文件名沿用代码消费名 `seal_mark.png`,除非裁决换名)。 + +**改动面**:文档+美术素材 + +**状态**:待美术设计 + ### 教学引导 > **节头注记(2026-10-04 用户拍板)**:教学引导定稿批整体改排 **Beta 阶段**——当前 Alpha 版不做教学相关工作,本节五条全部随之改排(2026-09-19 的 Alpha 排期拍板被本拍板取代)。同日五问讨论已形成初裁与设计提案,提案已归档 `archive/` 目录(磁盘留存、不入 git,Beta 批立项底稿,引用以其「归档状态与初裁记录」节为准)。初裁摘要:①教学形态=丙1 混合(首次冒险编排式首遇提示〔挂 first_script 锚、不锁操作〕+其余全程情景触发);②「因果教学顺序」问题**注销**——只教扫雷操作,结算读法不设教学(用户裁:结算只是表达、展现、演出,无策略内容;展示层每行带来源标注已自带归因;教学引导.md 旧前提〔设计目的句/P0 候选归因提示/验收第 2 条〕随 Beta 批文档定稿清除);③图例面板按循环段落分组、无结算段;④不做「再教一次」——改走独立「帮助」手册面(全部内容放一个文档);⑤教学文案内容权归设置与本地化(两文档负责表对齐随 Beta 批)。**未裁三疑点**(Beta 立项时先裁):增益叠层节奏条目去留/帮助入口位置(设置面为推荐)/图例面板与帮助手册的内容分工。 @@ -453,17 +469,6 @@ ### 文档与结构 -#### qa/ 实现报告收尾·孤儿遗留七项余二项(2026-10-14 登记,来源=批次移交) - -**实况**:qa 收尾批核查 iter19 系列出口报告遗留移交项时发现**不在待办清单在册**的七项(acceptance-package §三.2/card0 §5.3/followup §七.3/uibackfill §6.2-4/verify §6.1/3/7)。已结案的(tree_rules 受理闭环、X/Y 双源、S1 七项违例修复、提灯目检项、tests.yml 休眠等)已在各报告标注结案证据,不入本条。2026-10-24 处置:第②③④项随迭代 36 卡C 落地销号(营地 STONE 深档六处、迷雾三关派生行、开场后缀条件加算区),第⑥项按用户 2026-10-24 裁整条注销(TG_* 装饰带非文字,不另立探针)。余二项均为挂号备查/顺路件,无排期承诺(第①项 design/diagrams 越界 2026-10-28 实查核销——目录磁盘与 git 均已不存在,越界实体消失,白名单甲/乙两案失去对象): - -5. **字面量墨收测前置**(iter19 卡②遗留):bcard 属性描述/类型脚注/✓章/primary_button 字四处内联 Color8 无常量形态,收测需先在 ui_kit 立常量(呈现批顺路件,改 ui_kit 越当批面)。落点=呈现优化批(ui_kit 常量面) -7. **商店房截图纳屏**(iter19 卡①遗留):商店房截图纳屏留后续呈现批(考证结论在 shot_probe 头注)。本项原并列的「`数据配置.md:223` 历史行死指」半句已于 2026-10-21 实查核销——禁写变更记录规约生效后该文件的版本史行整段已不存在(现全文 212 行,文内零版本史行),被指对象随之消亡。落点=呈现批 - -**改动面**:游戏代码+工具代码 - -**状态**:待写卡(备选) - #### 主角棋盘小人像素管线重产——原「美术资产链路审计两项 P0」之 P0②(2026-10-09 登记,来源=审计发现) **实况**:主角棋盘小人未走像素管线(插值糊图),重产事项未在任何登记处在册;审计报告 HTML 件从未进版本库、现已删除,现状以本条正文为准。2026-10-13 用户令随「UI 美化三层诊断」登记:本条属第三层「观感提升」,Alpha 阶段需完成。原 P0①(文档面:两处引用已丢失的冻结样张路径)2026-10-28 按用户裁乙销号——art-bible §1.4 冻结记录与 art-status-checklist P0 样张行删路径、留「产物原件已丢失」,机检报告凭据(git 提交 7a847c7)保留。 @@ -474,29 +479,6 @@ **状态**:待美术设计 -#### 技术债观察登记——长函数余量与 lambda 连接防回归(2026-10-15 登记,来源=批次移交) - -**实况**:证据=迭代 27 需求卡片两项「登记待办清单观察」义务批内漏登、卡片核对时补登——①**长函数 17 个**中非结算段的装配/校验函数(`level_map_screen.gd` 选路装配 155 行/`camp_overlay_view.gd` 152 行/`debug_overlay.gd` 114 行/`treasure_item_table.gd` schema 校验群等;`main._build_ui` 已随场景拆分自然解体、`_settle`/`_resolve_hero_attack` 已随波二拆段=17 中 3 件已消解,余 14 件);②**lambda 连接 76 处**(presentation 6/smoke 62/tests 8——实读无泄漏,登记防回归)。长函数拆分无独立需求驱动不动;lambda 连接数若有审计工具化需求归类型闸后续。 - -**改动面**:游戏代码 - -**状态**:待写卡(备选) - -#### 测试域遗留(2026-10-16 登记,来源=批次移交) - -**实况**:证据=迭代 29 批内未收余量(交付报告与验收审计申报,报告已按吸收即死退役、git 历史可考)。迭代 29 把验收语义从「跑一遍没崩=通过」扭到「三层完备+判定可靠」,三硬闸(退出码/脚本错误扫描/断言数下限)已落地,L2/L3 从零建起承载。本批未收的余量集中登记在此,均不阻塞、不排期: - -| # | 事项 | 落点与要点 | -|---|---|---| -| 1 | **余下 3 个 core 模块仍零指名单测**:`board_data`/`reward_economy`/`skill_lottery` | 测试基建批。本批补了 minesweeper_rules/board_generator/config_db_resolver/config_db_cross_validator 四件;余三件按同一手法补(判据=能否在 `--script` 模式不加载 autoload 验完) | -| 2 | **render_gate 的 SCRIPT ERROR 扫描仍缺失** | 测试基建批。判定主体是 golden 容差与取证文件存在性,脚本错误不改退出码时无扫描面;本批只把矩阵状态如实记为「缺失」,收它的写法待裁 | -| 3 | **E 级卫生件**(test_kit static var 串台风险、4 套件自建断言出口、157 处 untyped 容器、76 处 lambda) | 工程债批。本批只统一了**新写**断言的出口,存量未动;test_kit 的 static var 是「并发跑批」的前置障碍(并发分层方案的前提) | -| 4 | **行覆盖率度量缺失** | 基建评估项。Godot 无内建、第三方属新基建;现行补洞靠「零指名清单 + 人工判定」,是否引入度量待裁 | - -**改动面**:游戏代码 - -**状态**:待写卡(备选) - #### 营地交互重做·五张需求卡片(2026-10-18 登记,来源=会话提及) **实况**:2026-10-17~18 需求讨论澄清会话产出结论文档,用户令按工作内容维度拆卡、卡片不预占迭代号。五卡已写(顺序=执行顺序):①存档制数据层改造批 ②主菜单三入口化批 ③选档界面新建批 ④营地场景图新建批 ⑤Esc 浮窗与营地顶栏批(各卡估时与内容见卡片自身)。三道复核:grilling 子 Agent 对抗审查(五卡各一)→v1.1;用户裁定「事项按推荐的来」→v1.2;用户两次订正(**卡④留在批内不踢出**;前置归用户自立批,Agent 不代跑不代产——曾越权起草的点选反馈设计稿 v0.1 已撤回删除)→卡④ v2.2;**卡④ 2026-10-21 用户令退回美术设计段(v2.3,非有效卡,美术设计闭合后重立卡)**。①②③⑤ 零待裁点可编工程波次:②③ 硬耦合同波(「进入存档」钮落点=选档界面),① 是 ③ 前置;⑤ 随 ④ 代码波同行。Alpha 出口裁决(2026-10-21 用户裁):五卡整体列入 Alpha——08「三 · 界面」已开条目,Alpha 出口依赖五卡完工与卡④三个用户侧前置销号。**工程波次一已落地(2026-10-22)**:卡①②③ 随迭代 33 实现批合回 master(合并提交 ebf40947/5c2a7bcf/508f190b,总收口 6e8f72b2),三卡需求卡片与批次产出物已按「吸收即死」退役(设计口径已吸收进 存档序列化.md/生涯.md/camp.md/08-Alpha版本标准.md v1.13/art-bible.md v3.18,反哺落文凭据=文档批 git 提交 1864f157+9f7b3e8a);卡④(营地场景图)退回美术设计段待重立卡、卡⑤(Esc 浮窗)随卡④ 代码波同行,两卡未编排——编入哪一迭代由用户经迭代编排段发令。关联文档: