Compare commits

..
37 Commits
Author SHA1 Message Date
DKChan 7ed648da54 合并(营地交互修复批·登记+文档定稿+写卡四轮复核): 分支 camp-interact-20261105 四提交合入——cc2be27b 登记一条待办(三件同批)/00d7bbfb 文档定稿四处(camp.md v2.8 §顶栏栏体定值〔#6E4526×α0.80、48px 全宽浮层、栏内只钥石徽章一件右位顶格〕+art-bible v3.43 §6.2 同步指回+HUD框架 §提示与弹层三条纪律·其三确认弹窗按钮位次定值+待办状态推进)/4a650d0e 写卡一张三需求点+两行能力清单/0977a548 件3 设计终裁改判(撤「通用铺面出口+收编三处同形旧实现」, 落 UiKit.camp_hud_band_surface(host) 单参具名出口、色值 CAMP_HUD_BAND=Color(CAMP_HEADER_BAND,0.80)、ui_assembler.gd:224-230 与 ui_kit.gd:486-491/:537 一律不动; R9 删、验收回 R1–R8、估时 2.0 工作日)+轮次4 独立复核(17 条: 硬伤 2=R4 坐标口径按行矩形左位必打空[行 ALIGNMENT_CENTER, 双钮 444<壳宽 460]改取 child(0) 全局矩形中心、_smoke_camp_hud_band 接 _initialize 不成立[只调 _split_tail 且尾段 quit, 不被 await=假绿]改接尾段 await 链; 事实错 4=四消费方行号 72/76-78/82→73/79-80/83、场景根代码 new 位置 camp_scene_view:13-14→scenes/camp_screen.gd:13-14 并据此推翻轮次3 沿用值、modal_shell 不产 HSeparator、confirm_dialog:27-29 是同机制先例非右对齐先例; 一致性 4/补写 3/确认 5; 不采信 1=复核者把 presentation_encapsulation_sweep_test.gd 路径误记 unit/contracts/ 实在 unit/presentation/); 零待裁点, 本卡不编迭代(用户口径: 写完卡即收口); 撞车处置=待办清单两处 README 能力表一处冲突按「master 侧现行文+分支侧新增在其上重放」解: 篝火/营地图鉴两行取 master 真输入化现行版, 分支侧旧措辞撤, 新增两行(确认弹窗按钮位次与语义同钉/营地顶栏栏体定值与落座)插回原相邻位并同步 R4 新口径; 待办清单侧 master 已退役的「三关色相×五组微光互辨抽测」条不复活(合并后条目数 28→31=本批净加三条), 营地条状态 文档设计已闭合+已写卡; 本步凭据: 合并后主区 pipeline_gate 直跑 PASS(待办条目 31/roadmap 至迭代 53/超长 0/卡片口径 2 张全过)+pre-merge-commit 前置检查绿(读数见本合并输出)
ci / gates (push) Failing after 7m5s
2026-11-06 19:19:18 +08:00
DKChan e76a31c494 合并(真输入化批·收尾对账): 真输入化条目实况合并后换代(两批合计+402函数注记,剩余面=视图层方法面)+新登记发现四项待裁条+roadmap待编池去已完成QA面板条; 本步凭据: 流水线闸PASS(28条目,区内凭据) 2026-11-06 07:58:06 +08:00
DKChan 03ebb4741d 收尾(真输入化批·合并后待办对账): 真输入化条目实况按合并后状态换代(两批合计51+1与63处+注记402函数,剩余面=视图层方法面); 新登记发现四项待裁条(route时序假绿面/停手图例宽度违规/图例承载待裁/tune_sim规格缺口); roadmap待编池去已完成QA面板条(master侧删条后遗留失步顺手消); 本步凭据: 流水线闸PASS 2026-11-06 07:57:42 +08:00
DKChan fcb3ecd8ae 合并(测试面真输入化+判据出处注记批): WP1 七探针63处emit转真输入+逐文件判据出处注记; WP2 全tests+probe树402验证函数挂# oracle注记(指针=design/全域规格句); WP3 capability_audit G道机检(覆盖+指针可核,自检五道)+test-conventions §8.3「测试函数的判据出处注记」条款+README四能力行随批订正; WP4 收尾换代; 撞车处置=screen_flow/ui_click两探针与待办清单取master侧(QA harness批d3c85390先行转换已验证),分支侧注记30条在master版上重放(空格规整+粘行修复); 区内全量档绿(193902ad)+解冲突后两探针直跑绿+capability_audit --strict绿(402函数红项0); 本合并凭据: premerge前置检查绿读数见本合并输出 2026-11-06 07:56:40 +08:00
DKChan 193902ad8f 收尾(真输入化批·待办处置): 测试面真输入化条目完成删除(直改序列——7探针63处转真输入+判据出处注记401函数全库铺开+capability_audit G道机检+规约成文); QA总览面板条目实况更新(①承接完成撤残险引用,②③仍待执行); 新登记发现四项待裁条(route时序假绿面/停手图例宽度违规/图例真输入承载待裁/tune_sim规格缺口); roadmap当前位待编池换代; 本步凭据: 流水线闸PASS(27条目) 2026-11-06 07:43:51 +08:00
DKChan f3138df5df 文档(判据出处注记成文+README行订正): test-conventions §8.3 新立「测试函数的判据出处注记」条款(格式/验证函数定义/指针空间design全域/与链式锚分工/维护义务/被动文本局限披露/规格缺口处置)+机器闸清单同步; README能力行四行随批订正(:158七物件撤残险登记换真输入承载+:161篝火笔记本+:165布告板+:170五窗真点化注记); 本步凭据: run_all_gates.sh --docs 验收通过零新增红(基线一致,回执cba3d647) 2026-11-06 07:42:46 +08:00
DKChan cba3d64709 工程(判据出处机检·capability_audit G道): 新增判据出处注记机检——验证函数(含check/check_quiet/_problems.append/push_error判定调用,定义行与_check前缀除外)须带# oracle注记+指针须design/全域且行号可核; 自检四道扩五道(扫描面零文件exit2); 摘要行增注记函数数; 同批修正320+79行注记格式(:行号与判据句间补空格——ORACLE_RE要求)+smoke_boot两指针改锚architecture:10(README不属design/域); 本步凭据: capability_audit --strict绿(401函数红项0)+run_smoke PASS(15项0失败三闸全过) 2026-11-06 07:40:50 +08:00
DKChan 085717166d 工程(判据出处注记4/7·presentation+剩余探针+system/smoke/example): 21套件+11探针+3宿主逐函数/逐段挂# oracle注记——HUD框架:260-282拒绝三源/明细行/状态槽/微光/三区几何/对比度+camp.md:16/29/45+level-map:3/11+level-clear+node-reward+设置与本地化:95+锻造工坊窗口内容+battle.md:122/128/154+战斗表现:148+test-conventions:30/37/84+code-conventions:41/54+architecture:168+README术语表; tune_sim_batch_parity(调参测量域)零规格句不造锚——记规格缺口待裁; 本步凭据: 抽验4套件+5探针+system_test+ui_click直跑全绿 2026-11-06 07:31:03 +08:00
DKChan 9e26df16be 工程(判据出处注记3/7·battle+contracts+integration): 20套件80函数逐函数挂# oracle注记——战斗四文档验收句(战斗结算:360-366/敌人系统:295-311/增益状态:250-262/地图战斗资源:235-241)+排雷玩法:231/237+行动经济+地形变异:156-158+奇物+存档序列化断点域/迁移链+关卡节点图生成:132+architecture:89/90/107/144+code-conventions:40/41/54; 本步凭据: 六目录代表套件直跑全绿(seal/contracts×2/integration×3),其余随WP4全量档兜 2026-11-06 07:23:25 +08:00
DKChan ddc352a985 工程(判据出处注记2/7·core): 34套件135函数逐函数挂# oracle注记——行为套件锚owner文档验收句(冒险结构:283-287/关卡节点地图:176-184/棋盘生成:221-229/排雷玩法:228-238/行动经济/战斗结算:362/地图战斗资源/秘宝/天赋树:192-199/锻造工坊:215-220/元进度/存档序列化:61-181/数据配置:199-204/地形变异:153/奇物/关卡节点图生成:127-132),惯例与封装套件锚code-conventions:40/41与architecture:89/103/107/144; 本步凭据: 33套件exit=0+run_time_fatal=1(在册负码)全量直跑 2026-11-06 07:17:06 +08:00
DKChan 157fb62a17 工程(判据出处注记1/7·data_config): 12套件40函数逐函数挂# oracle注记——行为套件锚owner文档验收标准句(数据配置:198-204装载/查行/黄金向量/计数器/越域+锻造工坊:219/220切换语义与价格梯+关卡节点图生成:127+地形变异:153+冒险结构:281+难度曲线:316+元进度:148+architecture:89/90词表与RNG),负面套件锚数据配置:199/200校验类别与致命通道; 本步凭据: 6正套件exit=0+5负套件exit=1(在册期望码,直跑12/12) 2026-11-06 07:05:39 +08:00
DKChan 3537f8e2a5 工程(真输入化7/7·ui_click_probe): 37处emit→36处真输入+1保留——场景热点(dummy/chest/board)=push_click_at真点,窗关闭=真点底栏返回营地钮,选档新档/进档/删档/确认取消/设置返回/生涯返回/商店离开(真发射源shop_overlay_view:66)/地图与战斗状态角标/继续钮/下一层/终局继续与返回/弃局双击(3s窗)/图例开/停手=真点push_click,槽位tooltip=InputEventMouseMotion真悬停(enter/exited引擎同步派发,禁await——槽位随结算重建跨帧句柄失效实证); 1保留=:914禁用态P1守卫(真点被引擎吞,守卫只能信号沿验,计划内处置); 布局纪律:refresh_camp重建后真点前保三帧(帧尾call_deferred); 本步凭据: ui_click_probe headless直跑exit=0零FAIL(PROBE_SEED=7全跑批) 2026-11-06 07:01:55 +08:00
DKChan 5e6cec8353 工程(真输入化6/7·ui_struct_probe): 4处emit→真输入(锻炉/笔记本=场景命中区真点,forge关窗=真点返回营地钮,gate=按下在命中区+抬杆离场防误触确认——场景命中沿=抬杆,弹窗随抬杆同帧开,抬杆落原位经事件传播误触确认钮实证冒险误启,抬杆落(2,2)中性点); 2处保留emit+前提注记(route选路/图例开启——emit选路路径下battle_root未及显示且锁在,真点不可达); 判据出处注记9条(camp.md:10/19/45+level-map:3+battle.md:154+HUD框架:88+code-conventions:54/21×2+存档序列化:61+architecture:168); 本批发现记账: ①route信号路径屏幕切换时序——emit后battle_root持续不显示,battle_hud审计长期空转采样(假绿面) ②真点路径采样变实暴露停手(64)/图例(56)同容器宽度不等(在册equal_width纪律必红) ③图例开启真输入承载待①裁决后补——三项待裁收尾呈报; 本步凭据: ui_struct_probe headless直跑PASS exit=0 2026-11-06 06:31:32 +08:00
DKChan 7aa0126b22 Merge branch 'glow-freeze'
ci / gates (push) Successful in 34s
2026-11-06 06:19:09 +08:00
DKChan 8d170007be 工程+文档(三关色板终值冻结批): 互辨抽测闭合落文——art-bible v3.43 §6.3 三关两槽终值改指针式冻结(本节不抄色值字面量:终值=各关三块底型瓦件顶面受光档实测最亮值,wall_ambient 同关同槽、零新增色值;凭据句写明样图同哈希故目检面未重跑+§1.4 样张重跑 30/31 唯一红为改前基线缺位属在途素材区环境坑);§4.1 对比硬指标句按用户新立规矩去数值化(「未开格底 L≥0.30/饱和≤0.55/避禁入带」数值子句删,改述底槽-微光-已开格底相对亮度与相对饱和关系+指路执行件登记面);抽测执行件代表值口径订正(H2_VALUES 硬写的换色锚槽表值退役,改由 lit_slots() 从在役底型件像素按线性相对亮度实测;六道门值单点化为 FLOOR_L/CEIL_SAT/BANNED_HUE/NEAR_EPS/B_MIN_CH_DIFF/C_MODEL_TOL,run_checks 内两处内联 8/2 同批收编——本件即这些数值的唯一源);art-pipeline-tools.md 登记该件(选工具表一行+专节含命令/门值单点/坑);reports/读数表补 S3 目检判定表(四条全过)+S4 抽测通过结论;待办清单抽测条整条删除+新登记「美术圣经去数值化」条(状态待文档设计)。
本步凭据: 抽测件实跑 17 项 FAIL 0(dungeon 实测 #B49069 L=0.307 过门余量 0.007/underdark #BAC1D2 0.532/deep_underdark #969EBA 0.345)+样图与 2026-11-05 出图件逐字节同哈希(15d33fabb75256677254…,目检面未变)+§1.4 格子样张重跑 30/31 绿(唯一红 V10=在途素材区无改前基线件,拒用 --save-baseline 造「改前=改后」空对拍)+run_all_gates --docs 闸况与基线一致无新增红(流水线闸 PASS,条目 27;术语闸抓到「不得」两处已按 STC 词表改「禁止」)+目检口径记档(用户回话「没有问题,我觉得是可以分清楚的」,经追问四条覆盖范围答「四条全过」)。

订正备案: 本会话中代理一度判「dungeon 顶面受光档 L=0.235 不达圣经地板」并拟立案,系测量用 sRGB 加权亮度排序选像素、与该门线性域相对亮度口径不同尺度的失误;按件内口径实测 0.307 过门,未据此立任何条。
2026-11-06 06:18:59 +08:00
DKChan 4f48c40fd5 工程(真输入化5/7·screen_flow_probe): 14处emit→13处真输入——篝火/窗壳轮转/V6/V7/V8场景命中区push_click_at,二次确认真点_confirm_btn,选档返回/终局继续/生涯返回/清窗返回钮真点,esc返回主菜单真点_return_btn; V5b一处保留emit并注记(V5b被测面=窗开态遮罩挡场景点选,前提只能信号注入); 判据出处注记13条逐段挂(architecture:168互斥判据×2+test-conventions:49 boot组+camp.md:10/22/45×3+存档序列化:61选档+level-map:11/42+battle.md:57+settlement-summary:12); 本步凭据: screen_flow_probe headless直跑PASS exit=0(六步主流程+回退全绿) 2026-11-06 05:59:53 +08:00
DKChan e0b22f40fb 工程(真输入化4/7·shot_probe): 7处emit→真输入——设置开/关、生涯关、状态面板开/关=真点find_button命中钮,篝火=push_click_at热点中心,二次确认=真点_confirm_btn(ui_click_probe:702先例); 判据出处注记9条(code-conventions:54遮挡纪律+设置与本地化:95+camp.md:45/16/22+level-map:11/77+battle.md:122+HUD框架:264/277+test-conventions:84截图采集); 本步凭据: shot_probe窗口化直跑PASS exit=0(七屏1280x800全存,问题=0,battle draw_calls=194<200) 2026-11-06 05:55:36 +08:00
DKChan d68c32470e 工程(真输入化3/7·ui_tune_probe): 2处emit处置——boot开窗emit→_show_debug_overlay直调(发布态角标恒隐main.gd:138,信号路径已退役,:123在册先例——保留emit只会在隐态假开),关闭钮emit→真点push_click; 判据出处注记6条(test-conventions:49调试面×4组+architecture:168种子三分+code-conventions:21零连接巡检); 规格缺口发现#1: _overlay_zero_motion_contract零动效契约无现行文档句可锚(源卡17卡③已退役),记发现不造锚待收尾呈报; 本步凭据: ui_tune_probe headless直跑PASS exit=0(N=10模拟全链路) 2026-11-06 05:53:05 +08:00
DKChan 00a0b82a0a 工程(真输入化2/7·ui_probe): 5处emit→真输入——树/秘宝开窗=场景命中区push_click_at热点中心,关窗=真点底栏返回营地钮(camp_sub_view_base:66-84 closed单源),放弃拒按=真点abandon_btn; 同文件判据出处注记13条(level-map:59/11/42放弃与入口停点+camp.md:45/20/41/18窗壳余额入口+battle.md:122构建组+行动经济:258+HUD框架:275/274站桩tooltip图标键+地形变异:156+settlement-summary:20); 本步凭据: ui_probe headless直跑PASS exit=0(cells=144 board=12x12 ap_dots=8) 2026-11-06 05:49:37 +08:00
DKChan 7679208279 工程(真输入化1/7·ui_input_probe): 状态面板关闭钮 emit→真输入(push_click+release沿,p16局部变量)——case⑯关闭恢复链路改经输入管线; 同文件判据出处注记11条落case段(①-③→HUD框架:269悬停语义/④⑥→排雷玩法:231封印挖掘受理/⑤→:236拒绝三源/⑦-⑩→battle.md:154主角站桩落影/⑪→HUD框架:264数字可读/⑭→地形变异:154数据路由/⑮→HUD框架:268迷雾两态+元进度:156装配可见/⑯→HUD框架:277模态锁/⑰→HUD框架:267微光生命周期); 本步凭据: ui_input_probe headless直跑PASS exit=0(18断言组全绿含case⑯真点关闭恢复) 2026-11-06 05:44:55 +08:00
DKChan f73f3c1d47 合并(QA harness缺陷收口登记): P0条目「QA harness 三批缺陷与覆盖遗漏收口」新立(报告落reports带处置四分流)+统筹条目三批完成整条删除; --docs绿+流水线闸PASS(27条目); pre-merge钩子实弹(前置档读数见本合并输出)
ci / gates (push) Successful in 34s
2026-11-06 05:42:50 +08:00
DKChan a1f1c258ea 登记(QA harness缺陷收口·P0条目+报告落档): 新立「QA harness 三批缺陷与覆盖遗漏收口」P0条目(实况=reports报告指针+处置四分流:A1随camp-interact合回即消/A2·B1·B2走写卡/B3·B6归既有义务/B4·B5人工复核后分流); 报告落reports(八类逐条带文件行号与登记处指针); 统筹条目「QA 覆盖总览面板与自动试玩扩展」三批全落地完成整条删除(完成即删,§12.1)。读数: 流水线闸PASS(27条目)+--docs【验收通过】无新增红(exit=0) 2026-11-06 05:42:40 +08:00
DKChan d3c8539062 合并(QA harness 三批·测试面真输入化+覆盖总览面板+自动试玩扩展): 批1=两探针52处emit站点51处转真输入管线+1接线断言保留+probe_coverage_recorder覆盖登记件(锚链归一化键)+§2条文+滚轮press-release成对/折叠区滚入视野两实证规范; 批2=B覆盖总览面板四节(六层闸况/能力行×六层灯/控件下钻/本体对账)+义务层提取器7段103行+六源回执明细化闸壳挂点+本体枚举器865实体对账+豁免清单母版+登记件seen面; 批3=journey_player全动线机器自玩(16类屏到达/异常三类含归因通道/误报指纹跨跑静默实测生效/真输入命中30控件并入登记)。三批读数: 批1探针孤立直跑绿+全量档绿; 批2全量档绿+面板浏览器真实入口四节验过; 批3首跑机器复现在案缺陷「开局被开始层盖住」(营地交互修复批承接中)+二跑收敛PASS exit=0。方案偏差20条回写qa-harness讨论文档§10-§12; 三处环境/机制堵坑(.gd行尾LF/全局类缓存扫描/夹具清槽+排版帧)。合成并master c1acb47c(pre-merge钩子批)——本合并即该钩子第二次实弹
ci / gates (push) Successful in 7m15s
2026-11-06 05:28:00 +08:00
DKChan be0cab8356 Merge branch 'master' into qa-harness
# Conflicts:
#	.gitattributes
2026-11-06 05:24:31 +08:00
DKChan 49b4a63f93 合并(营地暖调口径): 营地改整体偏暖、安全温馨、与地下城形成对比;清除过时提案与其派生判据
ci / gates (push) Successful in 34s
落文三件: camp-scene-prompts.md 头部/§2/§2.1.1/§2.1.2/§4/§5 共 11 处改暖调口径、art-bible.md §6.2 指路句改结论内联、退役 reports/提案-营地色彩与氛围脚本-2026-10-23.md(吸收即死)。分支凭据 41f32e18: 文档域 8 道闸 --docs exit=0「验收通过·无新增红」(基线 c1acb47c,清理前后各跑一次)、全仓冲突词与提案引用零命中、tools/ 无冷暖判据闸件。开工前 CI 现查 c1acb47c run 90 success 真跑闸步。post-merge 钩子推送触发远端 CI。
2026-11-06 05:23:49 +08:00
DKChan 41f32e1845 文档(营地暖调口径): 按用户裁定改营地整体偏暖、安全温馨、与地下城形成对比,清除过时提案与其派生口径
口径依据=art-bible §2.8「全作最暖」与 §6.2「情绪与色域」;用户否认「远景要冷」为自己所裁,2026-10-23 提案自述过程稿且待作者追认、§5 第 11 条环境子板色值至今未登记。
design/art/camp-scene-prompts.md: 头部依据改指 art-bible §2.8/§6.2;§2 分层理由去冷色占比;§2.1.1 远景片提示词由禁暖改晴天暖调并补正向天光;§2.1.2 由去饱和灰褐暗土改日晒黄土地、放开暖金与暗角禁词、补受光斑块;§4 撤下黄度阈值/冷色占比≥55%/明度 0.30–0.45 三条派生判据,改按 §4.1 两道门登记回填;§5 第 7 条去「冷调」并写实黄土地。
design/art/art-bible.md §6.2「色彩与氛围依据」改指本文件 §2.8 与本节现行口径。
退役 reports/提案-营地色彩与氛围脚本-2026-10-23.md(吸收即死;两处指路句已改为结论内联)。
不动: 分片规格、几何分带、10 层结构、无路无草无物件判据、八件提示词、色值(仍按 §5 第 11 条登记)。
验证: 文档域 8 道闸 run_all_gates.sh --docs exit=0「验收通过·无新增红」(基线 c1acb47c,清理前后各跑一次);件核=全仓 *.md 冲突词与提案文件名引用零命中;tools/ 无读取冷暖判据的闸件,无 spec-AC 双答案。
开工前 CI 现查: c1acb47c run 90 success 且跑闸步真跑过;fd9c8ad9 的 run 86 红已由 ec1c0da1 追绿批闭合。
2026-11-06 05:23:32 +08:00
DKChan d7d4bc87d3 批3(QA harness·C自动试玩扩展): 新增game/sim/journey_player.gd+.tscn全动线机器自玩驱动件(开机→设置→存档三操作新档/删档/进档→营地五窗逐窗→Esc回退→大门开冒险→关卡推进→战斗→停点分发→终局收束→回菜单); 战斗策略委托既有auto_player,按钮/物件全真输入管线(ProbeUiUtil单点),两方法口如实标通道(地图候选=press_candidate自绘布局命中,棋盘格=click_cell BoardView公共口——棋盘管线面归ui_input_probe); 异常三类上报含机器自填归因初判+通道标签,fatal=诊断收集器级(battle_result=fatal_error),段间超时30s+战斗状态挂死90帧,屏互斥六对检查; 误报指纹ledger跨跑持久(同源第2次自动静默); 报告journey_report.js+命中30控件并入覆盖登记。读数: 首跑16类屏到达/上报2=机器首跑即复现在案缺陷「开局被开始层盖住」(营地交互修复批承接中——开始层可见直入locks_input谓词锁死战斗,C上报后夹具面隐藏继续走查,缺陷本体不掩盖); 二跑上报0/静默2/PASS exit=0(收敛机制实测生效); 冒险段0 fatal。实施期自修两坑: 陈旧槽文件夹具卫生(段首清槽)+开层排版帧(选档钮/面板继续钮同帧点击落空——选档空转假红与输入锁滞留两形态)。方案偏差6条回写qa-harness讨论文档§12 2026-11-06 05:23:29 +08:00
DKChan 8d8e4255c3 结转(QA harness批2·统筹条目推进): 进度①②已落地(批2=B面板四节+义务层提取器+六源回执明细化闸壳挂点+本体枚举器865实体对账,提交bb9ce8a1),③待批2验收后开工; 方案偏差6条回写qa-harness讨论文档§11(执行层挂点取编排器末尾单点/probe_manifest机制/本体缺口×探针命中交叉降级/义务层匹配口径/豁免清单落点/登记件seen面)。读数: 流水线闸PASS(27条目) 2026-11-06 03:22:09 +08:00
DKChan bb9ce8a117 批2(QA harness·B覆盖总览面板+本体枚举器): 义务层=tools/extract_capability_list.py(README能力清单四列表按旅程段解析,7段103行,用例指针剥反引号); 执行层=tools/coverage_receipts.py六源回执明细化(冒烟/单元集成/系统/探针/渲染/静态各一.js,名×结果×计数粒度,日志缺失=not_run亮灰不冒充绿)+run_all_gates.sh末尾挂点(观测件非判定件,解析失败打红字不翻闸判)+probe_manifest.js(面板file://无法列目录,按现存登记文件落清单); 机器义务双源之本体源=tools/coverage_enum.py(两扫描器+比对:.tres行集/schema词表字段/tscn按钮/gui_input自绘类/屏模态清单×extract_checks断言文料,865实体/缺口212); 呈现层=tools/coverage_report.html(四节:六层闸况/能力行×六层灯按旅程段/控件下钻交互源+渲染golden展示源/本体对账缺口×探针命中降级探针承载;状态灯四态+数据时间戳陈旧可见+完备性边界页眉;JSONP直载file://即开); 豁免=tools/coverage_exemptions.js人工维护母版; 登记件扩seen面(场景树全量BaseButton+db布局表hotspot_key公共面枚举,seen-hit=存在但零真输入覆盖缺口数据源)。读数: 两探针窄跑绿(screen_flow 19命中/116存在,ui_click 27命中/91存在——停手钮/树购买重建代等真缺口在册,弃图刷新确认链走视图方法属衔接条目余项); 六源明细化对真实闸日志抽核(unit82套件/系统69检查/探针10段/静态13条/golden逐屏L1-L3); 面板浏览器真实入口四节渲染验过; 全量档闸【验收通过】无新增红(exit=0,回执commit 07d4977f,六源明细化在闸内实跑=挂点真路径验证) 2026-11-06 03:21:10 +08:00
DKChan 07d4977f41 结转(QA harness批1·待办清单两条推进): 统筹条目实况补批1落地凭据(worktree qa-harness·两探针51处真输入化+1接线断言保留+覆盖登记件+§2条文,偏差八条回写方案文档§10),①改已落地②③待验收; 衔接条目已承接面改两探针52处站点(51转真输入+1标接线断言),剩余面收窄为视图层测试方法面+996条精锚。读数: 全量档闸【验收通过】闸况与基线一致无新增红(exit=0,回执commit 0ec53f42——含单元与集成4/4分片绿·场景探针族绿·渲染闸绿·静态扫描·文档域·熵增闸); 本批终态=qa-harness分支四笔(2cbd2ca9批1实现/88636868并master 1730a8fb/afce55b0 gd行尾属性/本笔) 2026-11-06 02:24:50 +08:00
DKChan afce55b09f 工程(.gd行尾属性·批1闸机堵坑): .gitattributes补*.gd text eol=lf——CRLF检出×套件内嵌r"""golden字面量逐位对拍吃进\r即假红(实证:settlement_wiring_snapshot_test在本worktree与主区双双1/3失败,同一字节Mac CI检出LF为绿),对齐tools/hooks例外同款; rm+重检出全区gd转LF零内容变化。读数: 导入扫描刷全局类缓存后LevelMapViewport入册(fog/forge两套件解析红清),pacing_queue挂死系缓存级联复跑绿(主区对照exit=0),快照套件修复后exit=0(封装5.1逐位对拍PASS) 2026-11-06 02:13:00 +08:00
DKChan 886368684c Merge branch 'master' into qa-harness
# Conflicts:
#	game/probe/probe_ui_util.gd
2026-11-06 01:52:17 +08:00
DKChan 2cbd2ca9a7 批1(QA harness·探针存量emit真输入化+覆盖登记写入): 两探针52处emit站点转真输入管线(push_input press+release成对)51处,保留1处置灰态穿透emit标〔接线断言〕; 新增probe_coverage_recorder.gd真输入覆盖登记件(旗标在位期间命中入账,锚链归一化键=具名/脚本锚+类#钮面文本+序位,@临时名不入键,JSONP落tests/.gates/coverage/每探针一件); probe_ui_util补coverage_hook单点+push_motion/push_wheel(滚轮press+release成对——只发press钉死鼠标焦点实证fork/返回营地两钮点击全失效)/window_panel/scroll_button_into_view(折叠区行钮rect越窗底栏实证误中返回营地钮,真输入滚轮滚入可视区再点); V5b遮挡试探订正=前窗经真实返回钮清账再真点开窗(emit直发穿遮罩属口径违例); 落点判定补钉2处(esc段子界面开窗落点/树窗返回钮落点); test-conventions §2新增UI驱动断言真输入管线条文(滚轮成对+折叠区滚入+落点判定+接线断言豁免口径); .gates/.gitignore收coverage/。读数: screen_flow_probe孤立直跑PASS(六步动线+屏级互斥全绿); ui_click_probe孤立直跑exit=0问题=0(budget=15210,三笔树购买请求全通,PROBE_SEED=7); 登记落盘ui_click=27控件/38命中/临时名跳过0,screen_flow=19控件/25命中。全量档闸(fd9c8ad9基线): 两新增红均判为主区推进漂移非本批引入——流水线闸红=待办清单QA条目证据句(master 1730a8fb已修),单元settlement_wiring_snapshot红待并master后窄跑复核; 本步凭据=两探针直跑绿+偏差8条已回写qa-harness讨论文档§10 2026-11-06 01:50:50 +08:00
DKChan 0977a548bf 写卡改判(营地交互修复批·件3 终裁落卡+轮次4 独立复核): 撤掉「铺满色面收成带形参的通用出口并收编三处同形旧实现」那一版设计, 落成用户 11-06 终裁形态——公共件一个具名出口 UiKit.camp_hud_band_surface(host) 形参只 host、色值 CAMP_HUD_BAND=Color(CAMP_HEADER_BAND,0.80)/PRESET_FULL_RECT 铺满/MOUSE_FILTER_IGNORE/挂宿主/返句柄全写死体内, ui_assembler.gd:224-230、ui_kit.gd:486-491、:537 三处同形写法不收编不动(形状撞车≠同一件事: 遮罩遮挡语义/页面底色档位/HUD 带定值三个变化原因, 绑起来即既不内聚又不低耦合且为没到来的问题做设计); R9 等值回归删、验收回 R1–R8、估时 2.25–2.5→2.0 工作日(无收编无两次全档), 契约4 加「禁开 color/pass_input 形参+禁收编」双禁、契约5 补 ColorRect 默认 STOP 漏设不报错的旁证, §15 Q1 裁决史补③④⑤(含 Q4 消解——用户否定的是该呈报前提而非某一案), §12 末段留两版作废判据的合成理由; 轮次4 由第四个未参与撰写只读子代理复核终态, 17 条: 硬伤 2(R4「按行矩形左位」必打空——行 ALIGNMENT_CENTER 且双钮可见宽 444<壳宽 460, 改取 child(0) 全局矩形中心; _smoke_camp_hud_band 接 _initialize 不成立——只调 _split_tail 且尾段 quit, 不被 await=静默不执行=假绿, 改接 _split_tail await 链)/事实错 4(四消费方行号 72/76-78/82→73/79-80/83、场景根 new 位置 camp_scene_view:13-14→scenes/camp_screen.gd:13-14 并据此推翻轮次3 沿用值、modal_shell 不产 HSeparator、:27-29 是同机制先例非右对齐先例)/一致性 4(§8.4 冠名 test-conventions、R4 读 main._confirm_consumer 注明探针先例 :272/:286 与扫描射程、§14 职责句去「摆放徽章」、R2 注明特有提交面由 R1 承载)/补写 3(徽章自高第二成因 18px 行高 FS_SUB、T3.4 补入树前调用时机、待办清单实况两轮→四轮)/确认 5(收编版残留零、40 处行号、FULL_RECT 铺满与 1280 承重链、const 派生合法性、契约面与 README 能力行逐字一致), 未新增待裁点; 不采信 1(复核者把 presentation_encapsulation_sweep_test.gd 路径误记 unit/contracts/, 实在 unit/presentation/, 结论经回读成立已按正确路径落卡); 本步凭据: pipeline_gate 直跑 PASS(待办条目 27/卡片口径 2 张全过)+tools/run_all_gates.sh --docs exit=0「验收通过, 闸况与基线一致无新增红」(基线 4a650d0e); 待办清单本条状态仍 文档设计已闭合+已写卡, 零待裁点, 开工门=U0 等用户批卡, 未合并未推送 2026-11-06 01:38:27 +08:00
DKChan 4a650d0ed3 写卡(营地交互修复批·一张卡三需求点): reports/需求卡片-营地交互修复批-2026-11-05.md 成文——件1 开局被开始层盖住(契约1 限开局/续玩两路径的隐藏点收一处+摘 main.gd:194 冗余句)、件2 弹窗钮序镜像(推进→次→维持,四处共用件同一序,成员与连接零动)、件3 顶栏加可见栏体(色值做成 UiKit 带 α 派生常量 CAMP_HUD_BAND=Color(CAMP_HEADER_BAND,0.80),矩形由顶栏件自建); 对照表 R1-R8 七格齐+判据出处行锚+能力行(两枚新增能力同批入 game/tests/README.md:162-163); 卡片头部申报 验收档=中、设计事实源三处定稿(camp.md v2.8/HUD框架 §提示与弹层/art-bible v3.43)+色值落点定稿与两项 Godot 4.7.2 实测(const 派生 const 编译通过 to_html=true 得 6e4526cc;色道单精度使 a==0.80 为 false → 断言禁浮点分量直等,改 to_html 比对); 三轮内改的四处架构违规=①R3 撤"升成员供测试直取"(抵触 architecture.md:168 视图单元接口/:323 禁直引实现内部,改公开子节点序)②顶栏 α 从视图内挪进公共件(依 ui_kit.gd:2 全站取色唯一出口)③契约4 措辞纠正(禁新增语义色而非禁派生常量,依 HP_BAR_TRACK:200 收口先例)④同步面补 architecture.md:175 CampHudBand 行; 两轮独立复核全落=轮次1(22 条:19 采信落卡/2 转用户裁 Q1 Q2/2 条经写卡段回读推翻)+轮次2(5 条:R3 双钮断言必红硬伤改 child(2)+子节点数非空前置、R5 补开营地前置(camp_scene_view.gd:51 初始隐)、T3.0 补入树后读数时机、行号八处与尺寸链确认、Q1 裁决解释面按自判落并向用户报备); 同批登记两条工程改动待办(认锚式不含中文段 pipeline_gate.py:68;层序落位单口扫描面不含子目录 source_guard_test.gd:85-99,均工具代码/待执行); 待办清单本条状态 文档设计已闭合+已写卡; 本步凭据: 流水线闸直跑 PASS(待办条目 27/卡片口径 2 张全过)+tools/run_all_gates.sh --docs exit=0「验收通过,闸况与基线一致无新增红」(回执 domain=docs 基线 00d7bbfb); 过程附记: 用语闸 doc_term 曾把待办条目里文件名 ui_registry.gd 的子串 registry 判为退役词疑似现行(基线绿→现红),已改以函数名 apply_layer_contract() 作锚避开误报、可检索性不丢,该误报属闸件面另议 2026-11-05 23:56:38 +08:00
DKChan 00d7bbfba1 文档设计(营地交互修复批·定稿落文四处): 待裁点清零后写入——①camp.md v2.8 §顶栏 补栏体定值(底=CAMP_HEADER_BAND #6E4526×不透明度0.80、高48全宽浮层、栏内只钥石徽章一件右位顶格〔徽章自高48=栏高故无顶距,系尺寸推导非选择〕、左位不放件,验收=目检窄带可辨+机检栏体呈色对落位带底色≥3:1) ②art-bible v3.43 §6.2 钥石余额行同步同组定值并指回 camp §顶栏 为唯一事实源(防两文档各说一套) ③HUD框架 §提示与弹层 由「两条纪律」改「三条纪律」新增其三=确认弹窗按钮位次定值(推进钮在左、维持现状钮〔取消/留下/否〕在右、三钮形态次推进居中,四处消费方共用同一件同一序,位次不随文案变语义由文案承担),出处按用户裁落 HUD框架 而非 interaction-patterns 库(该库文件头自声明「不引入新设计决策」且其成文来历用户存疑) ④待办清单本条状态 待文档设计→文档设计已闭合+待写卡,删「缺口」属性(§12.1:已闭合态不得残留),实况改写为三件一张卡+三份定稿指针带版本号,条目名「开局回主菜单」订正为「开局被开始层盖住」(真机现象系冒险已在底下开局、开始层盖住——system_test.gd:87-89 层序断言为据); 设计依据=本会话用户裁(α0.80 半透明档为定值/栏内单件/钮序左推进右维持)+预览人审「我看了预览的效果是OK的」; 本步凭据: pipeline_gate 直跑 PASS(条目25/卡片口径1张全过)+tools/run_all_gates.sh --docs exit=0 闸况与基线一致无新增红(回执 domain=docs,基线 cc2be27b) 2026-11-05 07:07:46 +08:00
DKChan cc2be27b9d 登记(营地交互修复批·用户裁一条条目): 三件同批不合卡收一条待办——大门无断点开局回主菜单/确认框否左是右/营地顶栏无栏体; 实况逐件指认 bug 证据行锚(camp_shop_presenter.gd:85 无条件显开始层+main.gd:211-212/223-224 大门两出口先关营地再开局+main.gd:292-311 _start_adventure 无隐藏句,该路径测试与探针零覆盖; confirm_dialog.gd:31-39 add_child 序=取消→次→确认; camp_hud_band.gd:10-17 透明 Control 无栏体)与 camp.md v2.7 §顶栏只定高48/全宽/浮层未定底色·排布·徽章落位; 缺口段留两项(顶栏补设计/确认框目标钮序待裁)——第2条按 art-bible §4.5 #7「破坏性确认禁止与常规确认同位」核实与本批钮序改动无关(共用件按对象路由不按位次,同位系既有状态,不随本批引入也不由本批修),故未把 §4.5 #7 拉进本条改动面; 改动面=文档+游戏代码; 状态=待文档设计(需求性条目含未闭合设计段,禁与待写卡并现)
本步凭据: 流水线闸直跑 PASS(待办条目 25 条/正文限长超限 0)+tools/run_all_gates.sh --docs exit=0 闸况与基线一致无新增红(回执 game/tests/.gates/receipt.json,基线 commit 69c417d8)
2026-11-05 05:03:39 +08:00
129 changed files with 3154 additions and 324 deletions

No files matched your search

+5 -5
View File
@@ -1,6 +1,6 @@
# 美术圣经(Art Bible)— Dungeon Sweeper
> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**;现行为 v3.42,历次修订明细见 git 提交史
> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**;现行为 v3.43,历次修订明细见 git 提交史
>
> **Art Director Sign-Off(AD-ART-BIBLE)**:历次批均未签核——此为 Lean/Solo 模式(非 PHASE-GATE)下的现行治理状态;full 门禁签核时机待用户表态。
> 风格基调:地牢题材但**非暗黑风**——轻快明亮的幻想冒险风,全年龄;像素风;角色 2~3 头身。
@@ -215,7 +215,7 @@
- **换色合并登记五处(用户裁,部件色落值)**|按本表「不过门即触发换色合并、禁加色绕行」执行,五处**零新增额度**(各件原拟新增候选一律不取):① 主角铲刃浅铁灰(原拟 `#A8AEB8`)→ 并入 `FOG_MEMORY_INK #C4C4C4`,**部件取值 = `#C4C4C4`**;② 主角提灯铜色(原拟 `#C08A3A`)→ 并入 `GOLD_DEEP #B07A14`,**部件取值 = `#B07A14`**;③ 食人魔酋长暖褐皮(原拟 `#B08050` / `#A97C4E`)→ 并入 `暖石 #B8A582`,**部件取值 = `#B8A582`**;④ 石君王灰皮(原拟 `#9A968C` / `#949089`,按**次级皮**口径——板甲视同体块主色、灰皮属次级皮故不受 L 0.22–0.45 约束)→ 并入 `STONE #786E62`,**部件取值 = `#786E62`**;⑤ 轻甲护甲图标木色 `#8A5A34`(深素底下 2.72:1 不过门;原拟新增木色亮档 `#C49A5E`)→ 并入 `WARM_FAINT #CDB88C`,**部件取值 = `#CDB88C`**(8.08:1)。**一色两岗代价已随裁定接受**:① 的 `#C4C4C4` 语义岗 = 迷雾记忆墨(§6.5)、② 的 `#B07A14` 属金族(§4.2 金只说收益与兑现)——两者均作角色随身件材质色用,不进信息层与语义物件岗;③④⑤ 无语义岗冲突。验收:机检〈五处部件取值点全部改指合并目标色,四枚原拟新增候选值零入板〉
- **换色合并登记三处**|按本表「不过门即触发换色合并、禁加色绕行」执行,三处零新增额度:①地台顶缘受光线 `#C4AC84` 已并入 `#C9AF83`(掩尘枚 `BRICK`,与 §8.2 高墙口沿高光同枚)——源常量已删除(全仓零引用,退役断言测试串为预期例外),绘制点全部改指 `#C9AF83`;②地台描边 `#5A4C3A` 已并入 `#4C4038`(代码 `PLAZA_EDGE`,ui_kit 唯一落点)——源常量已删除,绘制点全部改指;③舞台底 `#3E342A`(代码 `STAGE_BG`)→ 并入 `#3E362C`(状态槽底)——源常量已删除(常量表零键),背景资产接管该绘制面,程序化平色缺图回退改指 `SLOT_BACKDROP`(`#3E362C`)。程序化回退绘制与已落合并值的贴图保持一致。验收:机检〈①②两枚源常量全仓零引用,绘制点全部改指合并目标色〉
**对比硬指标(可测)**:数字可读性由**格底明度分档**承载——棋盘数字亮墨 **L≥0.75** 对**已开格底 L 0.08–0.14** ⇒ 直画对比 **≥4.5:1 恒成立**(WCAG AA,样张逐组合抽测冻结);**色牌后备**:色牌素色恒定 **L≥0.70**、牌上数字深墨 **L≤0.027** ⇒ 任意牌墨组合 ≥4.5:1(实测样例:#33261B/#F5EAD3 = 12.3:1)。未开格底槽(含全部图类型)相对亮度 **L≥0.30**(墙顶面受光档)、HSV 饱和 **≤0.55**、避 Boss 禁入色相带;已开格底走世界底层档(见上)。物件识别不依赖格底(§5.1 轮廓通道),封印印记、金色不作识别依赖。验收:机检〈亮墨对已开格底 ≥4.5:1;未开格底 L≥0.30、饱和 ≤0.55;任意牌墨组合 ≥4.5:1〉 ←原二;地形变异 AC-9
**对比硬指标(可测)**:数字可读性由**格底明度分档**承载——棋盘数字亮墨 **L≥0.75** 对**已开格底 L 0.08–0.14** ⇒ 直画对比 **≥4.5:1 恒成立**(WCAG AA,样张逐组合抽测冻结);**色牌后备**:色牌素色恒定 **L≥0.70**、牌上数字深墨 **L≤0.027** ⇒ 任意牌墨组合 ≥4.5:1(实测样例:#33261B/#F5EAD3 = 12.3:1)。未开格底槽(含全部图类型)与类型微光、已开格底之间存在**相对亮度与相对饱和关系**——禁止底槽吞没微光环、禁止底槽与已开格底同档;本节只定这层关系,阈值与准入判定由图类型色板互辨抽测的执行件承载(用法登记见 [art-pipeline-tools](art-pipeline-tools.md))。已开格底走世界底层档(见上)。物件识别不依赖格底(§5.1 轮廓通道),封印印记、金色不作识别依赖。验收:机检〈亮墨对已开格底 ≥4.5:1;未开格底对微光环与对已开格底的相对亮度关系经互辨抽测准入;任意牌墨组合 ≥4.5:1〉 ←原二;地形变异 AC-9
**色板增量纪律(数值状态 = 待定现用)**:全作色板硬上限 **48 色**(防色板无限膨胀的封口条款;**额度已用尽、余量 0**,后续余量以色板检查器实报为准);新色入板须同时过两道门:①§4.2 语义检查(不占语义色段/禁入带);②**与既有任一色的 RGB 三通道差禁止全部 <8/255**——即三通道里至少一通道 ≥8/255 即过门,「三通道差全部 <8」才算不过门(条文原写的「三通道差 ≥8/255」有「逐通道全过」与「禁止全灭」两种互斥读法,前者下现行色板 247 对不达标、与本节自认的「现行即执法」合规状态不能同真,故取后者;用户裁)。**额度已用尽:后续任何新增须先压缩或合并既有色**——按本节「额度不足时呈报作者」口径呈报,**压缩或合并哪几枚由作者裁,本节不代裁、不预指对象**。反例实证:#3A2E22 ↔ #362B20 三通道差均 <5,肉眼不可辨,同板共存只增噪。不过门即触发「换色合并」(把意图色并进最近既有色),**禁加色绕行**;加色义务同时登记于本表修订。**「待定现用」态释义**:数值**现行即执法**(色板检查器按此拦,§8.3)——非候选(不存在"等冻结"空窗)、非死规(后续需求引入时可变);调整走本节修订并登记,改值 = 修订不是违约。验收:机检〈色板总数 ≤48;新色入板过两道门〉
@@ -393,15 +393,15 @@
- **场景构成**:一张可点击的营地场景图(1280×800,与工程档同幅面)。远景=天空与远山;中景=**独立成层的木围墙**(密排竖木桩、桩顶削尖、不透缝无横木)与**半开的营地大门**;近景=中央篝火、左侧锻炉铁砧、右侧帐篷与宝箱、帐篷旁的笔记本,另有木桩与挂在围墙上的布告板。**中景带自底部往上约 2/3 处起**;**近景物件可覆盖围墙,唯一例外=中间门洞必须留空、不被任何物件遮挡**。七个可点物件的功能绑定归 `camp.md`,本节只定画法。
- **图层拆分(10 张)**:场景按「远景/中景/近景要素各自成层」拆——`scene_camp_bg` 背景层(z0,不透明全幅)=天空+远山+整片地面+门洞内一条通向远处的小路;`scene_camp_wall` 围墙层(z1,透明底)=横贯全宽密排木桩墙(左右两段)+两根略高的门柱,**门洞处留透明缺口**透出背景层的小路;`scene_camp_board` 布告板(z2)、`scene_camp_gate` 门扇(z3)、`scene_camp_dummy` 木桩/`scene_camp_forge` 锻炉/`scene_camp_tent` 帐篷(z4)、`scene_camp_chest` 宝箱/`scene_camp_notebook` 笔记本(z5)、`scene_camp_campfire` 篝火(z6,最前)=八个物件层,各透明底单件一张。**帐篷**为承载件、不可点。围墙独立成层的好处:桩高可单独锁死、门洞做透明缺口、背景层不混入暖木色。提示词与坐标 = `camp-scene-prompts.md`,消费常量 = `asset-path-map`。
- **交付形态**:**背景层 1 张(不透明全幅)+ 围墙层 1 张 + 八个物件层 = 10 张**,其中 9 张透明底、1 张不透明全幅 + 一份坐标表(贴图矩形/命中矩形/z 序)。背景层由**远景片 `bg_far`(2560×734)与地面片 `bg_floor`(2560×866)两片出图、按 y734 拼成一张**——整张一次出图时模型把远山挤到画面下半部,被围墙挡住后完全消失(实测三张里两张山体上缘落在墙顶以下)。接缝整条落在围墙背后,画面无接缝。**篝火例外**:命中矩形只取下半(火堆),上半(火焰)只作特效展示。围墙层落盘按细条裁,不做全幅(全幅约 3.9 MB 显存,细条约 0.35 MB);**实装落盘矩形 `(0, 203, 1280, 164)`、底锚 y367**(桩身 150px、门柱 164px 终稿锁定值,落带内〔围墙带 (0,203,1280,164)〕)。
- **色彩与氛围依据**:营地场景的冷暖分工(冷的大面积后退、暖的小面积前进)、空气透视三档与大面积用色规则 = `reports/提案-营地色彩与氛围脚本-2026-10-23.md`。本节不复述色值;新增环境色按 §4.1 场景环境子板条款登记(每屏一套、≤32 色、过 §4.2 语义检查与 RGB 三通道差 ≥8/255 两道门)。
- **色彩与氛围依据**:营地整体偏暖、安全温馨,与冒险中的地下城形成对比;现行口径 = 本文件 §2.8「全作最暖」与本节「情绪与色域」条,远景淡而退后、近景暖调为主。本节不复述色值;新增环境色按 §4.1 场景环境子板条款登记(每屏一套、≤32 色、过 §4.2 语义检查与 RGB 三通道差 ≥8/255 两道门)。
- **交互四态(零新素材,全部程序化)**:悬停=**轮廓亮边 + 浮动**(浮动须整像素吸附);按下=位移 1px;**禁用/未解锁=降明度 + 去饱和**(§2 已缩窄至情绪域,状态表达不受限),**仅锻炉(锻造工坊)与宝箱(秘宝匣)两件会有此态**;已解锁=常态。
- **钥石余额**:常驻画面顶部的一行小 HUD 栏(高 48px,**浮层覆盖,不挤压画面**)。
- **钥石余额**:常驻画面顶部的一行小 HUD 栏(高 48px,**浮层覆盖,不挤压画面**)。栏体底=`CAMP_HEADER_BAND` `#6E4526`(§7.2 五 token 对齐表在册色)× 不透明度 0.80;栏内只钥石徽章一件、右位顶格,左位不放件。呈现定值的唯一事实源=[camp](../gdd/ux/camp.md) §顶栏,本节不复述判据。验收:目检〈顶栏成一条可辨窄带,徽章落在带内〉
- **事实源边界**:解锁条件/价格/存档写入归元进度 GDD 与 `MetaProgression`;素材命名与路径归 `asset-path-map`;清单登记归 `art-assets-checklist` §3.5。
- **已裁(2026-11-01 用户裁)**:① **营地环境子板色值**——不前置拍板:量化场景实际用色后按 §4.1 场景环境子板条款登记(每屏一套、≤32 色、过 §4.2 语义检查与 RGB 三通道差 ≥8/255 两道门);② **门扇与门洞的高宽比例**——门扇贴图 240×138(锁定 240×147),随门洞 y283–367 压扁重画,坐标按门洞 x520–760 对齐;③ **帐篷的落地尺寸**——251×170 落位 (982, 335),上端遮住围墙下部。
### 6.3 三关图类型色板候选(board_type 换挡方向)
- **模板与不变槽**:换皮只换 §4.3 四槽(`tile_closed`/`tile_open`/`wall_ambient`/`variant_tint`),信息与语义层全图类型恒定——本节只给三关**方向性候选**,终值按 §4.3 现行规则重跑 §1.4 格子样张冻结。**终值的取值程序**:先跑「三关色相 × 五组类型微光」互辨抽测(抽测任务登记在待办清单),再按 §4.3 现行规则重跑 §1.4 格子样张冻结——本节不填终值色值,待该抽测与格子样张重跑后落本节修订。验收:机检〈本节三关行无 `tile_closed`/`wall_ambient` 终值色值字面量(终值未冻结前禁填)〉
- **模板与不变槽**:换皮只换 §4.3 四槽(`tile_closed`/`tile_open`/`wall_ambient`/`variant_tint`),信息与语义层全图类型恒定——本节的三关两槽终值**已冻结**。**冻结口径(本节不抄色值字面量)**:各关 `tile_closed` 终值=该关在役墙瓦片三块底型件(素砖/裂纹/碎石斑)顶面受光档的实测最亮值,`wall_ambient` 取同关同槽值(两槽同值;同属 §4.1 分层②场景环境子板,零新增色值、不占 48 额度);实测值与准入判定只在执行件里算,执行件用法登记见 [art-pipeline-tools](art-pipeline-tools.md)。**冻结凭据**=「三关色相 × 五组类型微光」互辨抽测闭合(机检 17 项 0 红+目检四条全过;抽测样图与 2026-11-05 出图件同哈希,目检面未重跑)+ §1.4 格子样张重跑机检 30/31 绿(唯一红灯=改前/改后对拍一件,因在途素材区无改前基线件、属素材区环境坑不属色板回归,沿革查 git 提交史)——砖面形态与色板维持冻结。验收:机检〈三关行两槽终值与互辨抽测执行件的实测读数同值(该件三行铺底代表值判定全过),且本节三关行不含色值字面量〉
- **三关背景变体**:§6.1 的三张背景取本节三关方向作为岩体形态、剪影语言与色族差异(dungeon 暖石棕+地精巢穴支洞/木栈道/木桩围栏;underdark 冷蓝灰+卓尔巨柱/石笋/蛛网/螺旋塔剪影/巨菇/石桥;deep_underdark 灰黑+灰矮人石笋城郭/棱角石厅/矿道口/烟囱塔);背景不复用 `wall_ambient` tint(该槽作用于棋盘/世界),背景色值归战斗屏场景环境子板。验收:目检〈三关背景可辨为不同洞窟,非同一图换色〉
- **深度轴叙事**(§2.7 下探过渡):色相层进预告「更深更险」,明度档不动——
- **dungeon 地精洞窟**(第一关):现行掩尘系暖石棕(`#C9AF83` 基)即基准档,`wall_ambient` 暖棕;
+20
View File
@@ -15,6 +15,7 @@
| UI 框材(面板、按钮、石框、底噪、图标) | `tools/quantize_ui_skin.py` | `tools/palette_check.py`、`tools/path_check.py` | 主题小件另跑 `tools/gen_theme_icons.py` |
| 地形瓦片 | `tools/recolor_terrain_tiles.py` | `tools/palette_check.py` | 只换色,不重绘 |
| 整屏样张 | `tools/render_sample_sheet_v3.py`、`tools/render_level_map_sheet.py` | `tools/shot_checker.py`;棋盘回归用 `tools/golden_board_crop.py` 加 `tools/golden_compare.py` | 样张脚本生成后自带机检 |
| 图类型色板互辨抽测(三关格底 × 五组微光) | `tools/glow_discrimination_sheet.py` | `tools/palette_check.py`(近色比对池同源)、`tools/shot_checker.py`(WCAG 公式同源) | 出并列样图+逐对读数表;格底准入的门值单点在本件 |
## 三条已知边界
@@ -291,3 +292,22 @@ python tools/path_check.py --repo . --code game
**判据或口径**:L1 查 `asset-path-map` §1 的声明目录,`design/art/candidates` 按需创建豁免。L2 查 `game/assets/art` 的四个语义子目录与现役文件清单,含主角 idle/walk/portrait、八敌、铲三帧、封印印记。L3 grep 代码里的 `res://assets/art` 引用,核对盘上存在性,并检出 `board_floor`、`board_wall`、`pillar_dungeon`、`production/mockups` 等退役命名;路径模板含 `%s` 或 `%d` 的跳过。worktree 素材未同步时,缺失项标 SKIP 不判红;主区完整素材环境才全量判。全过 0,有红灯 1。
**已知坑与适用边界**:依赖 `asset-path-map.md` 的声明清单与代码常量;新增素材目录不更新文档就不会被查。素材不入 git,worktree 空或半同步会大量 SKIP,不能据此判断主区健康。只核对路径存在与退役命名,不核对图像内容、尺寸或色板。
## tools/glow_discrimination_sheet.py
**一句话定位**:三关图类型色板 × 五组类型微光的互辨抽测执行件——出一张并列样图(54 格=三关 × 三底型 × 五组 + 基准格,含灰度副本段与图例),自带机检判定并落逐对读数表。
**归属**:仓库内 `tools/glow_discrimination_sheet.py`。
**输入与输出**:读 `game/assets/art/board/terrain_wall_center*_ <board_type>.png` 三块底型件、`game/presentation/views/ui_kit.gd` 的微光冻结常量、`game/tests/golden/palette_baseline/` 两张条带作近色比对池;合成侧逐像素复刻 `game/presentation/views/cell_type_glow.gdshader`。产物=`artwork/mockups/glow-discrimination-sheet-<日期>.png`(不入 git)+ `reports/` 下当日读数表。
**用法命令**:
```bash
python tools/glow_discrimination_sheet.py # 取准入读数 + 出样图 + 机检全道 + 落读数表
python tools/glow_discrimination_sheet.py --no-sheet # 只跑铺底准入五门,不出图不落表
```
**判据或口径**:门值单点在本件常量,文档侧不抄——`FLOOR_L`(未开格底线性亮度地板)、`CEIL_SAT`(饱和上限)、`BANNED_HUE`(禁入色相带)、`NEAR_EPS`(近色容差)、`B_MIN_CH_DIFF`(格底对微光可辨底线)、`C_MODEL_TOL`(样图与合成模型容差)。铺底代表值不查色表:取该关三块底型件像素里**线性相对亮度最大**的实测色(顶面受光档),素材换色后读数自动跟随。判定四道=A 五组明度阶梯单调且组间对比过线、B 格底对微光的三通道差禁止全部小于容差、C 样图与合成模型差落在容差内、D 准入五门;目检四条归用户,与机器判定并列取严(任一侧不过即该对不过)。退出码 0=全绿、1=有 FAIL、2=底型件缺位硬阻塞。
**已知坑与适用边界**:素材不入 git,worktree 缺三块底型件时直接 `exit(2)`——抽测的先决条件不成立,不是假红。色相吞没刻意不设机检阈值:本仓无一条文给出色相差门槛,自造一个等于把观感问题伪装成可机检问题,故色相差只进读数表列、不参与判定。带域与格心取样口径与 `game/probe/type_glow_shot_probe.gd` 同源(带域 de<格边×0.125、格心内缩 20%),改探针口径须同步本件。
+11 -11
View File
@@ -1,7 +1,7 @@
# 营地场景 · 分层生图提示词卡 v1.5
> **性质**:营地场景图(1280×800,一张可点击场景图 + 十张分层:背景层 1 + 围墙层 1 + 八个物件层)的出图执行侧文件。**场景级资产,与角色级 [art-prompts](art-prompts.md) 并列,两文件互不覆盖**。
> **依据**:色彩与氛围 = `reports/提案-营地色彩与氛围脚本-2026-10-23.md`(本卡不复述色值);坐标与层序 = 本卡 §2.3(运行期单一事实源 = `game/data/tables/camp_scene_layout.tres`);物件命名与消费链 = [asset-path-map](asset-path-map.md);画法与交付形态 = [art-bible](art-bible.md) §6.2。冲突时色彩以色彩脚本为准,画法以 art-bible 为准。
> **依据**:色彩与氛围 = [art-bible](art-bible.md) §2.8 与 §6.2(营地整体偏暖、安全温馨,与冒险中的地下城形成对比;本卡不复述色值);坐标与层序 = 本卡 §2.3(运行期单一事实源 = `game/data/tables/camp_scene_layout.tres`);物件命名与消费链 = [asset-path-map](asset-path-map.md);画法与交付形态 = [art-bible](art-bible.md) §6.2。画法与用色冲突时以 art-bible 为准。
> **边界**:只管营地场景图的分层出图。角色、图标、格面走 art-prompts.md 与脚本管线,不需要提示词。
> **建立**:2026-10-25 · 状态:十条提示词成文(背景层 1 + 围墙层 1 + 八个物件层);2026-11-05 按地平线 y367 重排,背景层与围墙层待按 §4 新判据重出,门扇随门洞压扁重画
@@ -39,7 +39,7 @@ Style reference: the attached images are this game's own finished pixel art and
## 2. 背景层与围墙层(两张,不可点)
围墙从背景层拆出、独立成层:围墙层可单独锁死桩高、门洞做成透明缺口透出背景层的小路;背景层不混入暖木色、更易守住冷色占比判据(判据见 §4)。两层都不可点;按乙工序门**先只出背景层 1 张**供作者确认风格前缀,确认后才扩产围墙层与八个物件层。
围墙从背景层拆出、独立成层:围墙层可单独锁死桩高、门洞做成透明缺口透出背景层的小路;背景层只画天、山、草地与地面,不画任何物件,远景保持淡而退后,让近景的暖调成为画面主角。两层都不可点;按乙工序门**先只出背景层 1 张**供作者确认风格前缀,确认后才扩产围墙层与八个物件层。
### 2.1 背景层(`scene_camp_bg`,不可点,不透明全幅)
@@ -64,18 +64,18 @@ Style reference: the attached images are this game's own finished pixel art and
**不画什么**:围墙、门柱、门扇、物件、角色、营地内地面(那是 §2.1.2);山与草地之间禁出现平直硬边
```
Pixel art style. True pixel art game background, chunky square pixels, strict pixel grid, hard edges, no anti-aliasing, flat colors, limited palette about 20 colors, retro 16-bit look, Aseprite style, flat side view, orthographic, ULTRA WIDE horizontal composition, aspect ratio 3.5 to 1. This is the far view seen from inside a small adventurer camp, looking out. It contains ONLY three bands stacked top to bottom, plus one path. BAND 1 — SKY, from the top edge down to 14 percent of the frame height: pale cool off-white with a faint blue-grey tint, flat and empty, at most one or two extremely simple flat clouds, no sun. BAND 2 — DISTANT HILLS, from 14 percent down to 55 percent of the frame height: two or three overlapping layers of rolling hills in pale cool blue-grey-green, each farther layer lighter, cooler and lower in contrast. THE HILLS MUST FILL THIS ENTIRE BAND — the highest ridgeline reaches up to 14 to 20 percent of the frame height and the hill mass continues down to 55 percent; a low ridge hugging the bottom of the frame is WRONG. Silhouette edges are stepped in pixel blocks, no outline, no rim light. BAND 3 — MEADOW, from 55 percent down to the bottom edge: cool pale grey-green grass, flat and low contrast, marked only by a few simple flat grass tufts, no trees, no bushes. PATH — one dirt path in the meadow only: it starts at the bottom edge of the frame in the middle, about one fifth of the frame width there, runs straight up and narrows until it meets the hill foot at 55 percent, where it disappears; it must not go above the hill foot and must not reach the sky. COLOR: everything cool, pale, desaturated, low contrast, no warm, golden, amber or yellow anywhere. DO NOT ADD: any palisade wall, fence, gate, gate posts, tent, campfire, crate, barrel, prop, character, animal; any hard straight horizontal seam between sky, hills and meadow — the hill foot dissolves into the meadow through stepped intermediate tones; any outline or rim; trees, forests or dense vegetation; antialiasing, gradients, dithering, painterly strokes, 3D, realistic shading, glossy light, bloom, vignette, dark corners, night, sunset, sun; any text, letters, number, watermark, signature, logo, frame, border, UI element, icon, badge.
Pixel art style. True pixel art game background, chunky square pixels, strict pixel grid, hard edges, no anti-aliasing, flat colors, limited palette about 20 colors, retro 16-bit look, Aseprite style, flat side view, orthographic, ULTRA WIDE horizontal composition, aspect ratio 3.5 to 1. This is the far view seen from inside a small adventurer camp, looking out. It contains ONLY three bands stacked top to bottom, plus one path. BAND 1 — SKY, from the top edge down to 14 percent of the frame height: pale warm off-white with a faint cream tint, flat and empty, at most one or two extremely simple flat clouds, no sun. BAND 2 — DISTANT HILLS, from 14 percent down to 55 percent of the frame height: two or three overlapping layers of rolling hills in pale warm grey-green, each farther layer lighter and lower in contrast. THE HILLS MUST FILL THIS ENTIRE BAND — the highest ridgeline reaches up to 14 to 20 percent of the frame height and the hill mass continues down to 55 percent; a low ridge hugging the bottom of the frame is WRONG. Silhouette edges are stepped in pixel blocks, no outline, no rim light. BAND 3 — MEADOW, from 55 percent down to the bottom edge: soft pale grey-green grass, flat and low contrast, marked only by a few simple flat grass tufts, no trees, no bushes. PATH — one dirt path in the meadow only: it starts at the bottom edge of the frame in the middle, about one fifth of the frame width there, runs straight up and narrows until it meets the hill foot at 55 percent, where it disappears; it must not go above the hill foot and must not reach the sky. COLOR: an overall warm, sunlit daytime palette — pale cream sky, warm grey-green hills, soft grey-green meadow; gentle warmth throughout, low contrast, no harsh saturation, no sunset or golden-hour cast. LIGHT: bright, dry, clear daylight from above, so the far view reads as a safe open camp in daytime. DO NOT ADD: any palisade wall, fence, gate, gate posts, tent, campfire, crate, barrel, prop, character, animal; any hard straight horizontal seam between sky, hills and meadow — the hill foot dissolves into the meadow through stepped intermediate tones; any outline or rim; trees, forests or dense vegetation; antialiasing, gradients, dithering, painterly strokes, 3D, realistic shading, glossy light, bloom, vignette, night, sunset, a visible sun disc; any text, letters, number, watermark, signature, logo, frame, border, UI element, icon, badge.
```
#### 2.1.2 地面片(`bg_floor`)
**画布比例**:3:1 超宽
**内容**:营地内的踩实裸土,铺满整幅
**硬指标**:**中线与左右两侧同色——没有任何路、小径、车辙**;明度 30%–45%;色相与远景片草地明显区分
**内容**:营地内被太阳晒着的踩实黄土地,铺满整幅
**硬指标**:**中线与左右两侧同色——没有任何路、小径、车辙**;明度读到晴天下被晒亮的干土,不暗、不闷;色相与远景片草地明显区分
**不画什么**:草、植被、大石块、物件、天空、地平线、围墙、接缝线
```
Pixel art style. True pixel art game ground texture, chunky square pixels, strict pixel grid, hard edges, no anti-aliasing, flat colors, limited palette about 12 colors, retro 16-bit look, Aseprite style, ULTRA WIDE horizontal composition, aspect ratio 3 to 1. This is the trodden floor INSIDE a small adventurer camp — bare packed dry earth filling the whole frame edge to edge, seen at a shallow angle from slightly above. MATERIAL: desaturated warm grey-brown, medium value, roughly 30 to 45 percent luminance, never dark, never muddy. TEXTURE: deliberate pixel clusters only — short paired scuff marks that read as dragged footprints, small scattered pebbles, beaten patches where feet have worn the ground smooth, occasional faint drag marks; density rises slightly toward the bottom edge and the middle stays calm and open. CRITICAL, NO PATH: there is no road, no trail, no track, no wheel rut and no worn strip anywhere in this image — the middle of the frame must look the same as the left and right edges, nothing leads anywhere. DO NOT ADD: grass, vegetation, plants, flowers, moss, lichen; rocks larger than a pebble; crates, sticks, bones, debris piles, tools; water, puddles; shadows of objects; sky, horizon, hills, wall, fence, gate, building; any line, seam, border or edge inside the frame; antialiasing, gradients, dithering, painterly strokes, 3D, realistic shading, glossy light, bloom, vignette, dark corners, warm golden cast; any text, letters, number, watermark, signature, logo, frame, border, UI element, icon, badge.
Pixel art style. True pixel art game ground texture, chunky square pixels, strict pixel grid, hard edges, no anti-aliasing, flat colors, limited palette about 12 colors, retro 16-bit look, Aseprite style, ULTRA WIDE horizontal composition, aspect ratio 3 to 1. This is the trodden floor INSIDE a small adventurer camp — bare packed dry earth filling the whole frame edge to edge, seen at a shallow angle from slightly above. MATERIAL: dry sunlit yellow earth (trodden loess), a warm ochre-tan packed soil, medium-to-bright value, dry and never muddy or grey. LIGHT: bright, dry, clear daylight from above, with broad soft sunlit patches and gentle tonal variation across the ground. TEXTURE: deliberate pixel clusters only — short paired scuff marks that read as dragged footprints, small scattered pebbles, beaten patches where feet have worn the ground smooth, occasional faint drag marks; density rises slightly toward the bottom edge and the middle stays calm and open. CRITICAL, NO PATH: there is no road, no trail, no track, no wheel rut and no worn strip anywhere in this image — the middle of the frame must look the same as the left and right edges, nothing leads anywhere. DO NOT ADD: grass, vegetation, plants, flowers, moss, lichen; rocks larger than a pebble; crates, sticks, bones, debris piles, tools; water, puddles; shadows of objects; sky, horizon, hills, wall, fence, gate, building; any line, seam, border or edge inside the frame; antialiasing, gradients, dithering, painterly strokes, 3D, realistic shading, glossy light, bloom, vignette; any text, letters, number, watermark, signature, logo, frame, border, UI element, icon, badge.
```
#### 2.1.3 拼接与后处理
@@ -193,8 +193,8 @@ A campfire made of a ring of grey stones around crossed stacked logs with a warm
## 4. 验收(机检 + 目检)
**远景片 `bg_far`**
- 黄度 =(R+G)/2−B 分区取样:天空 ≤20、远山 ≤20、草地 ≤35
- 冷色像素占比 ≥55%;禁出现任何暖色高饱和像素
- 整体读作晴天暖调、安全温馨,与冒险中的地下城形成对比;色温与色值判据按 §5 第 11 条随环境子板登记回填
- 整体暖调但保持中等饱和、低对比;远景淡而退后、近景暖调为主,高饱和只留给火焰,禁出现整屏高饱和偏色
- 机检:**山体上缘 ≤ 画面高度 15%**(这是硬指标——山体上缘落得越低,被围墙挡住后越是什么都看不见)
- 机检:**山体占画面高度 ≥40%**(山体上缘到山脚)
- 机检:草地上限 ≤ 画面高度 55%
@@ -204,10 +204,10 @@ A campfire made of a ring of grey stones around crossed stacked logs with a warm
- 目检:无围墙、门柱、门扇、物件、角色
**地面片 `bg_floor`**
- 明度 **L 0.30–0.45**(相对亮度);整片低于 0.30 即判红(旧稿偏暗、观感发闷)
- 明度读到晴天下被晒亮的干土,不暗、不闷;硬阈值随环境子板登记回填(§5 第 11 条)
- 机检:画面中线 200px 带与左右各 250px 带的平均色差 ≤6/255(**营地内无路带**)
- 机检:与远景片草地的平均色 (R−B) 差 ≥10/255(两种材质色相可辨)
- 目检:禁出现草、植被、大石块、物件、水面;细节读作拖拽脚印/碎石/踩踏斑块,不是满屏噪点
- 目检:能读出晴天下被晒过的干土与柔和日晒斑块;禁出现草、植被、大石块、物件、水面;细节读作拖拽脚印/碎石/踩踏斑块,不是满屏噪点
**整图(两片拼接后)**
- 接缝 y367 落在围墙 203–367 背后,**画面里看不见接缝**
@@ -239,7 +239,7 @@ A campfire made of a ring of grey stones around crossed stacked logs with a warm
4. **帐篷**:251×170 沿用现终稿,落位 (982, 335),遮住墙下部 32px(覆盖围墙可接受)
5. **笔记本**:随帐篷跟随,落在帐篷左下方外侧、两者不重叠——`x = 帐篷 x − 112`、`y = 帐篷底 + 10`
6. **宝箱离底**:贴图底 y744,距画面底 56px;其余物件同步上移 46px
7. **地面分两区**:营地外(y734 以上)=冷调灰绿草地+一条向远山收窄的土路;营地内(y734 至画面底)=踩实裸土、与草地异色相、无草无路无车辙;两区分界被围墙层完全遮住
7. **地面分两区**:营地外(y734 以上)=灰绿草地+一条向远山收窄的土路;营地内(y734 至画面底)=被太阳晒着的踩实黄土地、与草地异色相、无草无路无车辙;两区分界被围墙层完全遮住
8. **背景层分两片出图**:远景片 `bg_far`(2560×734,天空 14%/远山 41%/草地 45%+小路)+ 地面片 `bg_floor`(2560×866 踩实裸土),后处理按 y734 拼成一张 2560×1600;接缝落在围墙 406–734 背后。分片理由=整张出图时模型把山挤到画面下半部,实测三张里两张山体整段被围墙遮没
9. **布告板位置**:(845, 224, 130, 140),底 345 落在墙内,不再悬空
10. **描边颜色**:奶油色追认——去底脚本「奶油描边膨胀 3px 当屏障」的技术前提随现值锁定(§1 注记同源);改深灰褐须换去底方案全量重出,不裁
+5 -1
View File
@@ -20,6 +20,8 @@
凡模态覆盖层,探针必须断言:打开后自身 visible 且遮挡物已隐藏(模态被遮挡 = 功能存在但不可达)。
**UI 驱动断言须走真输入管线(2026-11-06 立)**:探针里「点按钮/点场景物件」类驱动断言,一律经引擎输入管线承载(合成 InputEvent 走 `push_input`,press+release 成对;合成滚轮同样必须 press+release 成对——只发 press 会把鼠标焦点钉死在按下点控件上,后续点击全部失效);信号直发(`pressed.emit()` 等)绕过命中测试,保不了「玩家点击可达」,只许用于真输入到不了的面并当场标〔接线断言〕(现役例:置灰态穿透钉处理器兜底)。折叠区控件先以真输入滚轮滚入可视区再点。每个动线断言钉「落点」判定(点了之后到哪个屏/哪层可见),防驱动中断后的空转假绿。真输入命中控件时自动登记控件覆盖(探针共用面登记件落 `.gates/` 观测产物),覆盖面生长不靠人记。
## 3. 缺陷修复必须沉淀回归断言
每个缺陷修复必须在探针或冒烟中留下能抓住该缺陷的断言(例:封印动作耗尽 AP → 必须进入第 2 轮;
@@ -100,6 +102,8 @@
**判据出处行锚(2026-11-05 立,自 2026-11-05 起对文件名日期不早于该日的需求卡片生效;旧卡不回头改,被编入迭代开工前由写卡段按本粒度补挂)**:对照表「需求点」列每行必须带**判据出处行锚**——该行验收判据所依据的现行文档句子,格式「`<文档相对路径>:<行号>`」。现行文档=`design/` 全域现行文档(含 `ux/` 交互规格与 art-bible 的判据行);已退役的需求卡片不是现行文档,不得充当判据出处。作用=断言预期值从规格句抄出,不从实现反推:实现行为与规格句冲突时断言红;规格缺席的行写不出锚,即停卡退回写卡段(那是需求澄清缺口,不是测试缺口)。机检=`tools/pipeline_gate.py` 判据 C5(锚格式与指向存在性,按卡文件名日期分流)+`tools/capability_audit.py` 链式锚检查(断言→套件→能力行→系统文档,见 §8.9)。
**测试函数的判据出处注记(2026-11-06 立)**:`game/tests/**` 与 `game/probe/**` 内每个**验证函数**(函数体含 `check(`/`check_quiet(`/`_problems.append(`/`push_error(` 判定调用的函数——定义行与 `_check(` 前缀不作判定计)必须带至少一条**判据出处注记**,格式「`# oracle: <design/相对路径>:<行号> <判据一句>`」——指针在前、判据摘要句在后,断言预期值的出处随断言走:断言红时读注记即达出处文档句,零查表。指针空间=`design/` 全域现行文档(行为测试锚 owner 系统文档「验收标准」句或 `ux/` 规格句;惯例类测试〔封装扫描、判定通道纪律、驱动契约〕锚规约条款行);`game/sim/` 不进扫描面(测量面直构惯例)。与链式锚的分工:链式锚(断言→套件→能力行→系统文档)不变、属义务清单层;本注记属执行证据层、写在断言文件内,非独立账本(§8.9 法则 4 不变)。写新测试函数时当场挂注记;被指规格句改行号时随批维护指针——死指针即红。局限如实声明:注记是被动文本,句内容改而行数不变时机检抓不到(与卡片判据 C5 同限)。机检=`tools/capability_audit.py` 判据出处注记道(覆盖检查+指针存在性;扫描面零文件自检红)。规格缺席写不出锚=发现规格缺口:记发现不造锚、收尾呈报,禁锚到非现行文档凑数。
**验收档申报(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 真实入口口径约束。
@@ -175,4 +179,4 @@
**文档纯度**:验收意图与能力描述只写玩家或系统可观测面,禁写代码路径(`.gd`/`.py`/`.sh`)、测试文件名、断言名;唯一例外=配置文件索引(`game/data/tables/*.tres` 这类表路径与表名)。规则正文见 doc-conventions §5「禁实现内容」。
**机器闸**:本节的结构由文档域闸「能力审计」机检——GDD 纯度(同款禁实现内容口径)、系统文档验收标准节非空、能力行四列与非空、代表用例(`文件:函数`)与真实入口可核(双通道:代码在或卡在——计划中的用例与入口由需求卡片锚定)、覆盖域表与能力行对得上;红项按新增红处理。闸件清单与域划分的唯一出处=`tools/run_all_gates.sh` 头部。
**机器闸**:本节的结构由文档域闸「能力审计」机检——GDD 纯度(同款禁实现内容口径)、系统文档验收标准节非空、能力行四列与非空、代表用例(`文件:函数`)与真实入口可核(双通道:代码在或卡在——计划中的用例与入口由需求卡片锚定)、覆盖域表与能力行对得上、判据出处注记覆盖与指针可核(§8.3 测试函数的判据出处注记);红项按新增红处理。闸件清单与域划分的唯一出处=`tools/run_all_gates.sh` 头部。
@@ -87,7 +87,7 @@ HUD框架站在玩家与全部玩法系统之间:挖掘期它是输入的翻
| toast | 非模态 | 自动消退 2.5 秒,不打断挖掘 |
| 图例面板 / 明细回看 / 胜负终局 | 模态 | 同一张登记序栈:统一入栈出栈、后开置顶、关闭逐层恢复 |
两条纪律横跨两族。其一,"第一次遇到"类提示(本作没有双击快开、挖不相邻格要先走近)走 toast,每场冒险至多弹一次。其二,拒绝反馈不属于一次性族——每次被拒都要可感知,且图标与文案双通道冗余(不靠颜色单独承载);棋盘面拒绝(动作不合棋盘规则,目标格边框闪)与资源面拒绝(行动点或储备不足,数字脉冲)必须可区分。图例面板恒可随时打开回看,不设解锁门。
三条纪律横跨两族。其一,"第一次遇到"类提示(本作没有双击快开、挖不相邻格要先走近)走 toast,每场冒险至多弹一次。其二,拒绝反馈不属于一次性族——每次被拒都要可感知,且图标与文案双通道冗余(不靠颜色单独承载);棋盘面拒绝(动作不合棋盘规则,目标格边框闪)与资源面拒绝(行动点或储备不足,数字脉冲)必须可区分。图例面板恒可随时打开回看,不设解锁门。其三,**确认弹窗的按钮位次定值**:推进操作的钮在左、维持现状的钮(取消/留下/否)在右,三钮形态的次推进钮居中;全项目确认弹窗共用同一件、同一序,四处消费方(删档确认/营地大门有断点的三出口/营地大门无断点是否开局/篝火离场)皆然。**位次不随文案变**,语义由文案承担。
### 呈现层订阅纪律:界面不自持状态、订阅面只留两条线
+2 -2
View File
@@ -1,6 +1,6 @@
# 营地场景 UX spec(camp)
> 状态:v2.7(现行)
> 状态:v2.8(现行)
> 事实源:本 spec(呈现)+ [元进度](../systems/跨局循环/元进度.md)(机制)
> 美术画法:[art-bible](../../art/art-bible.md) §6.2/§7.5;素材命名与路径:[asset-path-map](../../art/asset-path-map.md)
> 相邻:[level-map.md](level-map.md)(主菜单语境)
@@ -38,7 +38,7 @@
## 顶栏
钥石余额常驻画面顶部一行小 HUD 栏(高 48px、全宽窄条、**浮层覆盖在场景图上层,不挤压画面**)。刷新入口已实装=营地刷新单口(进档与营地内写口尾触发,现开界面与顶栏余额同步取现值)。
钥石余额常驻画面顶部一行小 HUD 栏:高 48px、全宽窄条、**浮层覆盖在场景图上层,不挤压画面**。栏体底=营地头带深棕 `#6E4526`(token `CAMP_HEADER_BAND`,色值出处 [art-bible](../../art/art-bible.md) §7.2 五 token 对齐表)× **不透明度 0.80**——半透明系定值,底下画面透出 20%。栏内**只放钥石徽章一件**,右位、顶格落座(徽章自高 48px=栏高,无顶距可言),左位不放件。验收:目检〈顶栏成一条可辨窄带、徽章落在带内〉+机检〈栏体呈色对其下方落位带底色对比 ≥3:1〉。刷新入口已实装=营地刷新单口(进档与营地内写口尾触发,现开界面与顶栏余额同步取现值)。
## 叠层与开合
+3
View File
@@ -26,6 +26,7 @@ var _cur_settlement = null # 当关 settlement(身份回调读取面——
func _ready() -> void:
# oracle: design/engineering/test-conventions.md:84 (棋盘截图取证——渲染闸采集组)
# 运行前提=窗口化真渲染面(shot_probe 同款护栏——headless 下 frame_post_draw 永不触发+视口纹理恒空)
if DisplayServer.get_name() == "headless":
print("[board_shot] FAIL — 本探针必须窗口化运行(headless 无渲染面,截图必空);现行事实源=tools/render_gate.sh 取证段")
@@ -181,6 +182,7 @@ func _ready() -> void:
# 判据:环外径像素 ÷ 格边 落 0.82±1px;封印格最外圈 1px 框最暗值与相邻未封印未开格同位框最暗值差 ≤1/255
#(halo 在 0.455×格边归零——格边框不应被压暗;相邻未开格同砖纹理同套瓦片为无封印参照)。
func _seal_evidence(tag: String, view: Node, settlement: Object, off_img: Image, out: String) -> void:
# oracle: design/engineering/test-conventions.md:84 (封印印记像素取证组)
var ms = settlement.minesweeper
var seal_cell := Vector2i(-9, -9)
var ref_cell := Vector2i(-9, -9)
@@ -288,6 +290,7 @@ func _probe_identity_lookup(c: Vector2i):
# 双帧逐像素比对:所有变化像素必须落在生效格的内缘带内
# (de < cell÷8——带外含砖心零侵入=禁整格染色/禁越圈);每个生效格须有实绘像素(不空转)。
func _check_dual_frames(tag: String, view, glow_cells: Dictionary, off_img: Image, on_img: Image) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var board = view.board()
if board == null:
_problems.append("%s 双帧比对取板失败(view.board() 为 null)" % tag)
+1
View File
@@ -8,6 +8,7 @@ extends Node
func _ready() -> void:
# oracle: design/gdd/systems/战斗循环/战斗表现.md:148 (结算演出——挖掘期零打扰组)
await get_tree().process_frame
var problems: Array = []
+3
View File
@@ -21,6 +21,7 @@ var _problems: Array = []
func _ready() -> void:
# oracle: design/engineering/test-conventions.md:84 (特效截图取证——渲染闸采集组)
var args := OS.get_cmdline_user_args()
out_dir = args[0] if args.size() >= 1 and args[0] != "" else "user://qa-fx"
DirAccess.make_dir_recursive_absolute(out_dir)
@@ -107,6 +108,7 @@ func _ready() -> void:
func _snap(tag: String) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
await RenderingServer.frame_post_draw
var img: Image = get_viewport().get_texture().get_image()
if img == null or img.is_empty():
@@ -116,6 +118,7 @@ func _snap(tag: String) -> void:
func _fail(msg: String) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
_problems.append(msg)
push_error("[fx_shot_probe] FAIL — " + msg)
get_tree().quit(1)
+2
View File
@@ -19,6 +19,7 @@ var _est_reads: Dictionary = {} # 逐档 13/14 板推定读数留痕(label
func _ready() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:264 (战斗屏三区几何与棋盘格边长——分辨率档位组)
await get_tree().process_frame
var main: Node = load("res://presentation/main.tscn").instantiate()
add_child(main)
@@ -58,6 +59,7 @@ func _ready() -> void:
# 一份不变量断言,逐档代入——代码量与档数无关
func _check_one(label: String, main: Node) -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:264 (单档几何断言组)
var vr: Rect2 = get_viewport().get_visible_rect()
if vr.size != Vector2(VIEWPORTS[0]["size"]) and vr.size.y < 1.0:
_problems.append("[%s] 逻辑视口退化 %s" % [label, vr.size])
+3
View File
@@ -11,6 +11,7 @@ extends Node
const SEED_L3 := 910212 # 深图固定种子(与 level_map_viewport_probe 同源——图确定性承载)
func _ready() -> void:
# oracle: design/engineering/test-conventions.md:84 (关卡画面截图取证——载入守卫组)
var args := OS.get_cmdline_user_args()
var out_dir := args[0] if args.size() > 0 else "user://"
await get_tree().process_frame
@@ -44,6 +45,7 @@ func _settle() -> void:
func _make_runner(entry: bool, node_id: int):
# oracle: design/engineering/test-conventions.md:84 (判定上报通道——验证函数组)
var rng = load("res://core/rng_service.gd").new()
rng.new_adventure(SEED_L3)
var runner = load("res://core/level_runner.gd").new(get_node("/root/ConfigDB"), rng)
@@ -83,6 +85,7 @@ func _deepest_with_outs(screen, runner) -> int:
func _shot(out_dir: String, fname: String) -> void:
# oracle: design/engineering/test-conventions.md:84 (判定上报通道——验证函数组)
DirAccess.make_dir_recursive_absolute(out_dir) # 落盘目录自建(save_png 对缺目录静默失败——刷新链不预建)
var img := get_viewport().get_texture().get_image()
var err := img.save_png(out_dir.path_join(fname))
+11
View File
@@ -22,12 +22,14 @@ var _runner # 当前停点的单关 runner(open_choices 驱动随记——
func _check(desc: String, ok: bool) -> void:
# oracle: design/gdd/ux/level-map.md:3 (判定上报通道——验证函数组)
_checks += 1
if not ok:
_problems.append(desc)
func _ready() -> void:
# oracle: design/gdd/ux/level-map.md:3 (关卡画面视口跟随——卷轴视口组)
await get_tree().process_frame
await get_tree().process_frame
var host := Control.new()
@@ -60,6 +62,7 @@ func _ready() -> void:
## R1 浅图整幅居中无裁切+画布入视口件承载(画布=视口首子件——setup 注入序)
func _probe_shallow_no_crop(screen, vp) -> void:
# oracle: design/gdd/ux/level-map.md:3 (浅层不裁切组)
_runner = _make_runner_entry(1, SEED_L1)
screen.open_choices(_cand_nodes(_runner), _runner)
await get_tree().process_frame
@@ -78,6 +81,7 @@ func _probe_shallow_no_crop(screen, vp) -> void:
## R5 前置+深图入口停点:锚点=入口组包围盒中心夹紧(spec :101——多入口并列取包围盒居中)
func _probe_deep_entry_clamp(screen, vp) -> void:
# oracle: design/gdd/ux/level-map.md:3 (深层入口钳制组)
_runner = _make_runner_entry(3, SEED_L3)
screen.open_choices(_cand_nodes(_runner), _runner)
await get_tree().process_frame
@@ -171,6 +175,7 @@ func _probe_clamp_bounds_and_columns(screen, vp) -> void:
## R6 拖拽不触发选路提交;阈值另一侧完整点击仍提交(提交语义零改动)
func _probe_drag_no_submit(screen, vp) -> void:
# oracle: design/gdd/ux/level-map.md:3 (判定上报通道——验证函数组)
# 重建到中段可居中停点(候选=停点出边——候选节点居屏中可拖可达)
var stops := _centerable_stops(screen, vp)
if stops.size() < 3:
@@ -209,6 +214,7 @@ func _probe_drag_no_submit(screen, vp) -> void:
## R2 停点切换后当前节点居中(视口中线投影 x=640±2——视口满屏宽即屏中线)
func _probe_follow_anchor(screen, vp) -> void:
# oracle: design/gdd/ux/level-map.md:3 (判定上报通道——验证函数组)
var stops := _centerable_stops(screen, vp)
if stops.size() < 3:
_problems.append("R2 可居中停点不足(=%d)" % stops.size())
@@ -224,6 +230,7 @@ func _probe_follow_anchor(screen, vp) -> void:
## R7 平滑滚动有 tween 中间帧;减动效开=位置即达(画布坐标域判定)
func _probe_smooth_scroll_reduced_motion(screen, vp) -> void:
# oracle: design/gdd/ux/level-map.md:3 (判定上报通道——验证函数组)
var saved_scale: float = UiKit.motion_scale
UiKit.motion_scale = 1.0
var stops := _centerable_stops(screen, vp)
@@ -263,6 +270,7 @@ func _probe_smooth_scroll_reduced_motion(screen, vp) -> void:
## R8 指引件可见性与几何(深图超窗——像素级判据②③)
func _probe_guidance_chrome(screen, vp) -> void:
# oracle: design/gdd/ux/level-map.md:3 (判定上报通道——验证函数组)
var band_l: Control = vp.get_node_or_null(NodePath("FadeL"))
var band_r: Control = vp.get_node_or_null(NodePath("FadeR"))
var chip: Control = vp.get_node_or_null(NodePath("PanHint"))
@@ -294,6 +302,7 @@ func _probe_shallow_guidance_hidden(screen, vp) -> void:
## 入口停点 runner(defer_entry——选路态挂起、候选=入口节点)
func _make_runner_entry(level_index: int, seed_val: int):
# oracle: design/gdd/ux/level-map.md:3 (判定上报通道——验证函数组)
var rng = load("res://core/rng_service.gd").new()
rng.new_adventure(seed_val)
var runner = load("res://core/level_runner.gd").new(get_node("/root/ConfigDB"), rng)
@@ -304,6 +313,7 @@ func _make_runner_entry(level_index: int, seed_val: int):
## 中段停点 runner(断点续玩重建——图由种子+参数档重生成同图,零 RNG 消耗)
func _make_runner_at_stop(level_index: int, seed_val: int, node_id: int):
# oracle: design/gdd/ux/level-map.md:3 (判定上报通道——验证函数组)
var rng = load("res://core/rng_service.gd").new()
rng.new_adventure(seed_val)
var runner = load("res://core/level_runner.gd").new(get_node("/root/ConfigDB"), rng)
@@ -378,6 +388,7 @@ func _extreme_node(pos: Dictionary, nodes: Array, first: bool) -> Dictionary:
func _finish() -> void:
# oracle: design/gdd/ux/level-map.md:3 (判定上报通道——验证函数组)
if _checks == 0:
_problems.append("零断言执行(断言数下限闸——三硬闸③)")
if _problems.is_empty():
+242
View File
@@ -0,0 +1,242 @@
# probe_coverage_recorder.gd — 真输入覆盖登记件(探针共用面)
# 是什么:探针/C 驱动件把合成 InputEvent 经 Viewport.push_input 送进引擎输入管线时,登记
# 「真输入实际命中了哪些交互控件」,探针收尾落 JSONP 式 .js 到 tests/.gates/coverage/
# (.js 供 file:// 页面 <script src> 直载——fetch 本地 json 会被 CORS 拦)。
# 成员面(两源):① 场景树全部 BaseButton——pressed 沿自动接线(重扫懒触发,见下);
# ② 自绘命中类——现役接 camp_scene_view.object_pressed(信号带物件 id,key 追加 #物件id 后缀),
# 其余自绘命中面走 note_hit() 显式口。
# 真输入旗标:push 前置位、帧末清位——只有旗标在位期间的命中入账;信号直发(〔接线断言〕保留站)
# 与产品码自发射永不入账。命中=控件真的收到事件:hitbox 错位/遮挡时零命中,缺口自然暴露——
# 这是「新控件落地→覆盖登记自动出现它」的数据源。
# key 规则(契约「场景文件路径+节点路径」的实况落地偏差,详见 qa-harness 讨论文档回写节):
# 本仓库 UI 大头由代码构建(main_ui_build/ui_kit 不设节点名),运行时节点路径全为 @类@序
# 临时名——契约原样落地产出空登记。故键取「锚链归一化」形态:自主根到控件只收集带稳定身份的
# 锚(具名节点用其名、带脚本节点用脚本文件名;@ 临时名段整体不入键),控件本体记 类#钮面文本,
# 同键控件按接线遍历序追加 #序位。键在确定构建序下可复现;UI 构建序变更会平移同键序位——
# 表现为键漂移而非静默丢失,属测量面可接受代价。
# 用法:探针 _ready 里 `_recorder = ProbeCoverageRecorder.new()` → `attach(main, "<探针名>")`;
# 收尾 `dump_js()`。探针共用面不入 S 系扫描面(code-conventions §4.13 分档)。
extends RefCounted
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
var _main: Node
var _probe_name: String
var _pending := false # 真输入在途旗标
var _camp: Control = null # 营地场景根(自绘命中单点)
var _hits: Dictionary = {} # 稳定 key(String) → 命中次数(int)
var _node_keys: Dictionary = {} # 接线时分配的节点键(instance_id → key)
var _key_seen: Dictionary = {} # 基键出现次数(同键控件按遍历序追加序位)
var _wired: Dictionary = {} # 已连 pressed 的按钮实例 id → true(防重连)
var _skipped_temp := 0 # 临时名命中笔数(不入 key,只计数)
# ---- 装载与接线 ----
func attach(main: Node, probe_name: String) -> void:
_main = main
_probe_name = probe_name
ProbeUiUtil.coverage_hook = self # 共用件真输入口统一过旗标(push_click/push_click_at/push_motion)
_wire_buttons()
_wire_camp_scene()
# 懒重扫:每次真输入 push 前接线一遍新入树的按钮(开层重建的钮行在下次点击前接上);
# 已释放按钮的连接随节点释放失效,登记表按 instance_id 清扫。
func mark_real_input() -> void:
for id in _wired.keys():
if instance_from_id(int(id)) == null:
_wired.erase(id)
_node_keys.erase(id)
_wire_buttons()
if not _pending: # 同帧多次 push 只挂一次帧末清位旗(重复 connect 会报引擎错)
_pending = true
_main.get_tree().process_frame.connect(_clear_pending, CONNECT_ONE_SHOT)
func _clear_pending() -> void:
_pending = false
func _wire_buttons() -> void:
_wire_into(_main)
func _wire_into(node: Node) -> void:
if node is BaseButton:
var id := node.get_instance_id()
if not _wired.has(id):
_wired[id] = true
_node_keys[id] = _assign_key(node)
(node as BaseButton).pressed.connect(_on_button_pressed.bind(id))
for child in node.get_children():
_wire_into(child)
func _wire_camp_scene() -> void:
_camp = _find_by_script(_main, "camp_scene_view")
if _camp != null:
_camp.object_pressed.connect(_on_camp_object)
func _find_by_script(root: Node, script_name: String) -> Control:
var s: Script = root.get_script()
if s != null and String(s.resource_path).contains(script_name) and root is Control:
return root as Control
for child in root.get_children():
var found := _find_by_script(child, script_name)
if found != null:
return found
return null
# ---- 键分配(锚链归一化,接线时一次算好存表)----
func _assign_key(n: Node) -> String:
var chain := _anchor_chain(n)
var leaf := ""
if n is BaseButton:
leaf = "%s#%s" % [n.get_class(), String((n as BaseButton).text)]
else:
leaf = n.get_class()
var base := "%s:%s/%s" % [String(_main.scene_file_path), "/".join(chain), leaf]
# 同键控件按接线遍历序加序位(#1 省略)——重复键可分账,序位随构建序漂移属已知代价
_key_seen[base] = int(_key_seen.get(base, 0)) + 1
if int(_key_seen[base]) > 1:
base += "#%d" % int(_key_seen[base])
return base
# 自主根到 n 的稳定锚链:具名节点取其名、带脚本节点取脚本文件名;@ 临时名段不入链。
func _anchor_chain(n: Node) -> Array[String]:
var chain: Array[String] = []
var cur: Node = n
while cur != null and cur != _main:
var nm := String(cur.name)
if not nm.begins_with("@"):
chain.push_front(nm)
elif cur.get_script() != null:
var sp := String((cur.get_script() as Script).resource_path)
chain.push_front(sp.get_file().trim_suffix(".gd"))
cur = cur.get_parent()
if chain.is_empty():
chain.push_front("(root)")
return chain
# ---- 显式登记口(自绘命中类无信号可连时的单点)----
func note_hit(n: Node, suffix: String = "") -> void:
if not _pending or n == null:
return
var id := n.get_instance_id()
var key := String(_node_keys.get(id, ""))
if key == "":
key = _assign_key(n)
_node_keys[id] = key
_bump(key + suffix)
# ---- 命中处理 ----
func _on_button_pressed(id: int) -> void:
if not _pending:
return
var key := String(_node_keys.get(id, ""))
if key == "":
_skipped_temp += 1
return
_bump(key)
func _on_camp_object(key: StringName) -> void:
if not _pending or _camp == null:
return
var id := _camp.get_instance_id()
var base := String(_node_keys.get(id, ""))
if base == "":
base = _assign_key(_camp)
_node_keys[id] = base
_bump(base + "#" + String(key))
func _bump(key: String) -> void:
_hits[key] = int(_hits.get(key, 0)) + 1
# ---- 场景树全量交互面枚举(控件缺口数据源:seen−hit=「存在但零真输入覆盖」)----
# 成员面与登记同源:场景树全部 BaseButton(main 建树常驻——未开过的屏的钮也算存在;
# 已接线钮复用接线键防序位错位,未接线者落盘期才补键——落盘是终态动作,序位计数此后不再消费)
# +营地物件(经 db 布局表 hotspot_key 公共数据面枚举;物件命中见 _on_camp_object 的 #物件id 后缀)。
func enumerate_seen() -> Array[String]:
var keys: Array[String] = []
_seen_into(_main, keys)
var db = _main.get("db")
if db != null and db.get("presentation") != null:
var layout = db.presentation.camp_scene_layout
if layout != null:
for spec in layout.layers:
var hk: StringName = spec.hotspot_key
var ck := "camp_object#" + String(hk)
if hk != &"" and not keys.has(ck):
keys.append(ck)
return keys
func _seen_into(node: Node, keys: Array[String]) -> void:
if node is BaseButton:
var id := node.get_instance_id()
var key := String(_node_keys.get(id, ""))
if key == "":
key = _assign_key(node)
_node_keys[id] = key
if not keys.has(key):
keys.append(key)
for child in node.get_children():
_seen_into(child, keys)
# ---- 落盘(JSONP 式 .js——file:// 页面 <script src> 直载)----
func dump_js() -> void:
var dir := DirAccess.open("res://")
if dir == null:
push_error("[coverage] res:// 打不开,登记未落盘")
return
var mk := dir.make_dir_recursive("tests/.gates/coverage")
if mk != OK and not dir.dir_exists("tests/.gates/coverage"):
push_error("[coverage] coverage 目录建不了(err=%d),登记未落盘" % mk)
return
var path := "res://tests/.gates/coverage/probe_hits_%s.js" % _probe_name
var f := FileAccess.open(path, FileAccess.WRITE)
if f == null:
push_error("[coverage] 登记文件打不开:%s" % path)
return
var total := 0
for k in _hits:
total += int(_hits[k])
var seen := enumerate_seen()
var payload := {
"meta": {
"probe": _probe_name,
"seed": OS.get_environment("PROBE_SEED"),
"scene": String(_main.scene_file_path),
"generated_at": Time.get_datetime_string_from_system(),
"driver": "InputEvent via Viewport.push_input(真输入管线)",
"key_shape": "锚链归一化(具名/脚本锚+类#文本+序位;@ 临时名不入键)",
"hits_total": total,
"seen_total": seen.size(),
"skipped_temp_names": _skipped_temp,
},
"hits": _hits,
"seen": seen,
}
var js := "/* 真输入覆盖登记——探针每次运行自动覆写,人工修订无效(豁免面走人工清单,不落本文件)。 */\n"
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
js += "window.__QA_COVERAGE__.probe_real_input = window.__QA_COVERAGE__.probe_real_input || {};\n"
js += "window.__QA_COVERAGE__.probe_real_input[%s] = %s;\n" % [
JSON.stringify(_probe_name), JSON.stringify(payload, " ")]
f.store_string(js)
f.close()
print("[coverage] 真输入命中登记已落盘 %s(控件 %d 处 / 命中 %d 次 / 临时名跳过 %d)" % [
path, _hits.size(), total, _skipped_temp])
@@ -0,0 +1 @@
uid://d1uhtlw3kmhxw
+80 -2
View File
@@ -120,13 +120,20 @@ static func isolate_save(main, tag: String, mark_first_done: bool = false, with_
# ---- 真输入点击承载(test-conventions §8.3「真实入口」判定口径)----
# 合成鼠标事件经 Viewport.push_input 进引擎输入管线(hitbox/命中测试参与),press+release 成对
#(BaseButton pressed 信号走 release 沿——ui_input_probe :717-733 / screen_flow_probe :484-499 同式)。
#(BaseButton pressed 信号走 release 沿——ui_input_probe :717-733 先例式)。
# 信号直发(pressed.emit())只许标「接线验证」,不得充当真实入口断言的驱动。
# coverage_hook = 真输入覆盖登记件(probe_coverage_recorder.gd)挂点:本件三个 push 口是探针
# 真输入的单点通道,push 前通知登记件置旗标——命中记账只认旗标在位期间(信号直发不入账)。
static var coverage_hook: Object = null
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:
if coverage_hook != null:
coverage_hook.mark_real_input()
var down := InputEventMouseButton.new()
down.position = pos
down.global_position = pos
@@ -141,6 +148,71 @@ static func push_click_at(main, pos: Vector2) -> void:
main.get_viewport().push_input(up, true)
# 悬停真输入(tooltip/悬停态断言的承载——motion 进管线触发 mouse_entered/exited 沿)
static func push_motion(main, pos: Vector2) -> void:
if coverage_hook != null:
coverage_hook.mark_real_input()
var mv := InputEventMouseMotion.new()
mv.position = pos
mv.global_position = pos
main.get_viewport().push_input(mv, true)
# 滚轮真输入(dir=+1 向下翻 / -1 向上翻——折叠区行滚入视野的唯一真输入通道)。
# wheel 合成必须 press+release 成对:硬件滚轮虽无 release 沿,但引擎 GUI 会把按键事件的
# press 记为鼠标焦点且等 release 才释放——只发 press 会把焦点钉死在按下点控件上,
# 后续左键点击全被路由给焦点控件(实证:滚轮后 fork/返回营地两钮点击全部失效)。
static func push_wheel(main, pos: Vector2, dir: int) -> void:
if coverage_hook != null:
coverage_hook.mark_real_input()
var idx := MOUSE_BUTTON_WHEEL_DOWN if dir > 0 else MOUSE_BUTTON_WHEEL_UP
var w := InputEventMouseButton.new()
w.position = pos
w.global_position = pos
w.button_index = idx
w.pressed = true
main.get_viewport().push_input(w, true)
var w2 := InputEventMouseButton.new()
w2.position = pos
w2.global_position = pos
w2.button_index = idx
w2.pressed = false
main.get_viewport().push_input(w2, true)
# 窗板定位(窗壳视图子树固定链:遮罩 ColorRect → CenterContainer → 窗板 PanelContainer——
# screen_flow_probe._window_panel 同式,探针族共用单点)
static func window_panel(view: Control) -> PanelContainer:
if view == null or view.get_child_count() == 0:
return null
var dim := view.get_child(0)
if dim.get_child_count() == 0:
return null
var center := dim.get_child(0)
if center.get_child_count() == 0 or not (center.get_child(0) is PanelContainer):
return null
return center.get_child(0) as PanelContainer
# 折叠区行滚入窗板可视区(真输入滚轮驱动;滚动不重建行,rect 随滚更新)。
# 判据:钮 rect 整体落在窗板内(上缘留 8px、下缘让开底栏 64px——返回营地钮带高约 56px)。
# 保险丝 60 次滚叶防异常内容死循环;窗板缺失或钮句柄失效即返回(调用方前置断言兜底)。
static func scroll_button_into_view(main, view: Control, btn: Control) -> void:
var panel := window_panel(view)
if panel == null or btn == null:
return
var guard := 60
while guard > 0:
var r := btn.get_global_rect()
var pr := panel.get_global_rect()
if r.position.y >= pr.position.y + 8.0 and r.end.y <= pr.end.y - 64.0:
return
var down := r.end.y > pr.end.y - 64.0
push_wheel(main, pr.get_center(), 1 if down else -1)
guard -= 1
await main.get_tree().process_frame
# 真输入拖拽沿(press→motion×steps→release 成对经 push_input 进引擎输入管线——卷轴视口批
# R3/R5/R6 驱动沿;沿 push_click_at 同式。relative 按步显式置值——合成事件的位移量由事件
# 自身承载,被测面按 relative 逐帧平移)
@@ -167,7 +239,7 @@ static func push_drag_at(main, pos: Vector2, delta: Vector2, steps: int = 4) ->
# 真输入滚轮沿(单档滚轮事件经 push_input——卷轴视口批 R4 驱动沿;方向映射在被测面,
# 本工具只发原始滚轮档)
# 本工具只发原始滚轮档)。press+release 成对(同 push_wheel 注——只发 press 钉死鼠标焦点)。
static func push_wheel_at(main, pos: Vector2, wheel_down: bool = true) -> void:
var ev := InputEventMouseButton.new()
ev.position = pos
@@ -175,6 +247,12 @@ static func push_wheel_at(main, pos: Vector2, wheel_down: bool = true) -> void:
ev.button_index = MOUSE_BUTTON_WHEEL_DOWN if wheel_down else MOUSE_BUTTON_WHEEL_UP
ev.pressed = true
main.get_viewport().push_input(ev, true)
var ev2 := InputEventMouseButton.new()
ev2.position = pos
ev2.global_position = pos
ev2.button_index = MOUSE_BUTTON_WHEEL_DOWN if wheel_down else MOUSE_BUTTON_WHEEL_UP
ev2.pressed = false
main.get_viewport().push_input(ev2, true)
# 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间,防碰真实槽
+41 -35
View File
@@ -8,9 +8,11 @@
extends Node
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
const ProbeCoverageRecorder := preload("res://probe/probe_coverage_recorder.gd")
var main: Node
var _problems: Array[String] = []
var _recorder: RefCounted = null # 真输入覆盖登记件(probe_coverage_recorder.gd——attach 后全探针真输入自动记账)
func _ready() -> void:
@@ -20,6 +22,10 @@ func _ready() -> void:
await get_tree().process_frame
await get_tree().process_frame
# 真输入覆盖登记(命中控件自动记账,收尾落 tests/.gates/coverage/——覆盖面生长不靠人记)
_recorder = ProbeCoverageRecorder.new()
_recorder.attach(main, "screen_flow")
# 隔离存档(全 worktree 共享同一份 user://save.json——探针必须换注,禁碰真实档)
ProbeUiUtil.isolate_save(main, "smoke_screen_flow", false, false, false)
await get_tree().process_frame
@@ -35,6 +41,7 @@ func _ready() -> void:
await _step_abandon_to_end()
await _step_end_back_to_menu()
_recorder.dump_js()
if _problems.is_empty():
print("[screen_flow_probe] PASS — 六步主流程+回退路径全绿(屏级互斥:任意时刻 ≤1 屏可见)")
get_tree().quit(0)
@@ -69,12 +76,14 @@ func _seals_text(f: Dictionary) -> String:
# ---- 流程状态位(与可见性是两回事,逐步单独判)----
func _expect_adventure_seal(step: String, expect: bool) -> void:
# oracle: design/engineering/architecture/architecture.md:168 (状态位判据组)
var got: bool = main.run_scope != null and main.run_scope.is_adventure
if got != expect:
_problems.append("%s:流程状态位 is_adventure 应为 %s,实测 %s" % [step, str(expect), str(got)])
func _expect_exclusive(step: String, expect: String) -> void:
# oracle: design/engineering/architecture/architecture.md:168 (屏级互斥组)
var f: Dictionary = _screen_seals()
var pairs := [["菜单", "营地"], ["菜单", "冒险"], ["营地", "冒险"], ["终局", "营地"], ["终局", "菜单"]]
for pair in pairs:
@@ -101,6 +110,7 @@ func _frames(n: int = 2) -> void:
# ---- ① boot:菜单独占 ----
func _step_boot_menu() -> void:
# oracle: design/engineering/test-conventions.md:49 (boot 组)
_expect_exclusive("①boot", "菜单")
_expect_adventure_seal("①boot", false)
if main.ui.modals.menu.debug_overlay != null and main.ui.modals.menu.debug_overlay.visible:
@@ -109,6 +119,7 @@ func _step_boot_menu() -> void:
# ---- ② 菜单 → 营地(真实按钮信号路径)----
func _step_menu_to_camp() -> void:
# oracle: design/gdd/ux/camp.md:10 (菜单→营地组)
# 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间)
if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_camp"):
_problems.append("②菜单→营地:选档层新档钮缺失(新路径驱动中断)")
@@ -119,14 +130,15 @@ func _step_menu_to_camp() -> void:
# ---- ③ 营地 → 菜单(回退路径:篝火二次确认动线——卡④ 营地唯一回菜单出口;选档层随关层收闭同收)----
func _step_camp_back_to_menu() -> void:
# oracle: design/gdd/ux/camp.md:22 (营地→菜单组)
if main.ui.modals.camp.scene == null:
_problems.append("③营地→菜单:营地场景根句柄缺失〔句柄子容器批 V1〕")
return
main.ui.modals.camp.scene.object_pressed.emit(&"campfire")
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"campfire").get_center()) # 真输入(营地场景 _gui_input 命中——大门段同式)
await _frames()
if not main.ui.modals.menu.confirm_dialog.visible:
_problems.append("③篝火:二次确认弹窗未现(camp.md v2.2 必须先确认)〔营地图批 V7〕")
main.ui.modals.menu.confirm_dialog.confirmed.emit() # 二次确认离场(confirmed 先于关层——卡④ T4)
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._confirm_btn) # 真点「确认」离场(confirmed 先于关层——卡④ T4)
await _frames()
_expect_exclusive("③营地→菜单(回退)", "菜单")
@@ -135,6 +147,7 @@ func _step_camp_back_to_menu() -> void:
# ---- 选档界面开合(卡③ sl10):函数直调开层(「进入存档」钮归卡② mn3)→屏级互斥采样→
# 返回钮真实信号路径关闭→ESC 语境不响应(卡⑤ 契约 2 范围外——零实现即断言)。----
func _step_save_select_open_back() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:61 (选档开合组)
ProbeUiUtil.isolate_save(main, "smoke_screen_flow_savesel", false, false, true)
main.save_manager.slots.attach_slot_template("user://smoke_screen_flow_savesel_slot_%d.json")
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
@@ -150,7 +163,7 @@ func _step_save_select_open_back() -> void:
if back_btn == null:
_problems.append("[savesel] sl10 返回钮缺失(页壳顶条)")
else:
back_btn.pressed.emit()
ProbeUiUtil.push_click(main, back_btn) # 真点「返回」→ 关选档层
await get_tree().process_frame
if main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("[savesel] sl10 返回后选档未隐藏或开始层未恢复(关闭链失守)")
@@ -181,6 +194,7 @@ func seals_check_single(f: Dictionary) -> bool:
func _step_menu_to_adventure() -> void:
# oracle: design/gdd/ux/level-map.md:11 (菜单→冒险组)
# 开始冒险钮已随三入口化撤出——函数直调中间态(卡② mn10;卡④ 大门两分支换代)
main._start_adventure_from_menu()
await _frames(3)
@@ -194,6 +208,7 @@ func _step_menu_to_adventure() -> void:
# ---- ⑤ 冒险内屏切换:关卡地图屏 ⇄ 战斗屏(屏内两态,屏级仍独占)----
func _step_level_to_battle_and_back() -> void:
# oracle: design/gdd/ux/battle.md:57 (冒险内切换组)
if not main.ui.modals.run.level_screen.visible:
_problems.append("⑤冒险内切换:前置不在关卡地图屏")
return
@@ -213,6 +228,7 @@ func _step_level_to_battle_and_back() -> void:
# ---- ⑥ 放弃 → 终局(放弃路径=回退路径的非正常出口)----
func _step_abandon_to_end() -> void:
# oracle: design/gdd/ux/level-map.md:42 (放弃→终局组)
if main.run_scope == null or main.run_scope.adventure == null:
_problems.append("⑥放弃→终局:前置无冒险实例")
return
@@ -225,10 +241,11 @@ func _step_abandon_to_end() -> void:
# ---- ⑦ 终局 → 菜单(终局出口返回开始界面)----
func _step_end_back_to_menu() -> void:
# oracle: design/gdd/ux/settlement-summary.md:24 (终局→菜单组)
if main.ui.modals.run.end_overlay == null or not main.ui.modals.run.end_overlay.visible:
_problems.append("⑦终局→菜单:前置不在终局")
return
main.ui.modals.run.end_overlay.continue_pressed.emit()
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 真点「继续」(_btn=continue_pressed 同源句柄)
await _frames(3)
_expect_exclusive("⑦终局→菜单", "菜单")
if main.run_scope != null and main.run_scope.is_adventure:
@@ -237,6 +254,7 @@ func _step_end_back_to_menu() -> void:
# ---- ③' 营地 Esc 浮窗+HUD(卡⑤——真实键盘合成路径 push_input;四态路由+三出口+语境分支+余额单口)----
func _step_camp_esc_and_hud() -> void:
# oracle: design/gdd/ux/camp.md:45 (Esc 段组)
ProbeUiUtil.isolate_save(main, "smoke_screen_flow_esc", false, false, true)
if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_esc"):
_problems.append("[esc] 营地入口缺失(选档层新档钮驱动中断)〔ESC批 V1〕")
@@ -254,8 +272,10 @@ func _step_camp_esc_and_hud() -> void:
await _frames()
if main.ui.modals.camp.esc_overlay.visible:
_problems.append("[esc] echo 事件唤起浮窗(触发沿 pressed 且非 echo 失守)〔ESC批 V1〕")
main.ui.modals.camp.scene.object_pressed.emit(&"dummy") # 子界面开态
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center()) # 子界面开态(真输入命中)
await _frames()
if not main.ui.modals.camp.tree.visible:
_problems.append("[esc] 子界面未随场景点选打开(后续关窗断言的前置落点)")
_push_esc()
await _frames()
if main.ui.modals.camp.tree.visible:
@@ -285,7 +305,7 @@ func _step_camp_esc_and_hud() -> void:
if not main.ui.modals.camp.esc_overlay.visible or main.ui.screens.start_overlay.visible:
_problems.append("[esc] 关设置后未回浮窗或回弹主菜单(契约 3 语境分支失守)〔ESC批 V4〕〔ESC批 B3〕")
# 返回主菜单动线(契约 4——浮窗自收+营地隐+选档收+菜单显)
main.ui.modals.camp.esc_overlay.return_requested.emit()
ProbeUiUtil.push_click(main, main.ui.modals.camp.esc_overlay._return_btn) # 真点「返回主菜单」
await _frames()
if main.ui.modals.camp.esc_overlay.visible or main.ui.modals.camp.scene.visible or main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("[esc] 返回主菜单动线失守(浮窗自收/营地隐/选档收/菜单显)〔ESC批 V3〕〔ESC批 B3〕")
@@ -300,6 +320,7 @@ func _step_camp_esc_and_hud() -> void:
# 判的是呈现形态换壳(近全屏面板→居中定尺寸窗壳 720×640):窗几何/场景根留底/遮罩 0.6/关闭钮/
# 互斥单开/ESC 优先序/窗内滚动/生涯同壳——行为面(开合路由)零改,只钉窗壳可观测提交面。
func _step_camp_window_shell() -> void:
# oracle: design/gdd/ux/camp.md:45 (窗壳 V1–V8 组)
ProbeUiUtil.isolate_save(main, "smoke_screen_flow_winshell", false, false, true)
if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_winshell"):
_problems.append("[winshell] 营地入口缺失(选档层新档钮驱动中断)〔窗口化批 V1〕")
@@ -323,11 +344,11 @@ func _step_camp_window_shell() -> void:
if prev_back == null:
_problems.append("[winshell] %s 底栏「返回营地」钮缺失〔窗口化批 V4〕" % String(opened.back()))
else:
prev_back.pressed.emit()
ProbeUiUtil.push_click(main, prev_back) # 真点「返回营地」关前窗
await _frames()
if prev.visible or not main.ui.modals.camp.scene.visible:
_problems.append("[winshell] %s 关闭钮按下未隐窗或场景根失守〔窗口化批 V4〕" % String(opened.back()))
main.ui.modals.camp.scene.object_pressed.emit(k)
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(k).get_center()) # 真输入开窗(场景 _gui_input 命中)
await _frames()
opened.append(k)
var view: Control = views[k]
@@ -362,7 +383,10 @@ func _step_camp_window_shell() -> void:
if vis_n != 1:
_problems.append("[winshell] %s 开态五窗 visible 计数=%d 应=1(互斥单开失守)〔窗口化批 V5〕" % [String(k), vis_n])
# 树窗点选遮挡(V5b——树窗开态向锻炉命中区推真实鼠标点:被遮罩挡=零新窗零响应)
main.ui.modals.camp.scene.object_pressed.emit(&"dummy")
# 真输入化订正:前窗(图鉴)先经真实关闭钮清账再开树窗——emit 直发可穿透遮罩开窗,
# 真输入下「无窗态场景命中」是唯一合法开窗路径(遮罩语义由本段正面验证)。
await _close_all_windows(views)
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center())
await _frames()
var tree_view: Control = views[&"dummy"]
if not tree_view.visible:
@@ -370,8 +394,7 @@ func _step_camp_window_shell() -> void:
else:
var hotspot: Rect2 = main.ui.modals.camp.scene.get_hotspot_rect(&"forge")
var c := hotspot.get_center()
_push_click(c)
_push_release(c)
ProbeUiUtil.push_click_at(main, c) # 遮挡试探真点击——遮罩生效则零命中零响应
await _frames()
var vis_n2 := 0
for w in five:
@@ -383,7 +406,7 @@ func _step_camp_window_shell() -> void:
# 随栈 close 恢复语义逐层清账,V6/V8 从干净单窗态起步)
await _close_all_windows(views)
# V6 ESC 先关窗(生涯窗开态 Esc→关生涯窗、浮窗不唤起)
main.ui.modals.camp.scene.object_pressed.emit(&"notebook")
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"notebook").get_center())
await _frames()
if not career.visible:
_problems.append("[winshell] 生涯窗未开(V6 前置中断)〔窗口化批 V6〕")
@@ -395,7 +418,7 @@ func _step_camp_window_shell() -> void:
if main.ui.modals.camp.esc_overlay.visible:
_problems.append("[winshell] 关窗同拍唤起浮窗(应无窗才进浮窗)〔窗口化批 V6〕")
# V7 内容超高窗内滚动(树窗+真实投影喂加长行册)
main.ui.modals.camp.scene.object_pressed.emit(&"dummy")
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center())
await _frames()
if not tree_view.visible:
_problems.append("[winshell] 树窗未开(V7 前置中断)〔窗口化批 V7〕")
@@ -420,7 +443,7 @@ func _step_camp_window_shell() -> void:
# 清窗经真实钮(同 V6 前置——干净单窗态起步)
await _close_all_windows(views)
# V8 生涯同壳(窗板同几何+返回钮在位+内容列滚动面在)
main.ui.modals.camp.scene.object_pressed.emit(&"notebook")
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"notebook").get_center())
await _frames()
if not career.visible:
_problems.append("[winshell] 生涯窗未开(V8 前置中断)〔窗口化批 V8〕")
@@ -445,7 +468,7 @@ func _step_camp_window_shell() -> void:
# 段尾收闭:生涯窗经真实「返回」钮关闭(on_camp_closed 关闭名单不含生涯窗——防残留泄段)
var career_back: Button = ProbeUiUtil.find_button(career, "返回", "prefix")
if career_back != null:
career_back.pressed.emit()
ProbeUiUtil.push_click(main, career_back) # 真点「返回」收闭生涯窗
await _frames()
if career.visible:
_problems.append("[winshell] 生涯「返回」钮未隐窗〔窗口化批 V4〕")
@@ -455,6 +478,7 @@ func _step_camp_window_shell() -> void:
# 清窗经真实钮(逐窗巡检 visible→钮 pressed——栈 close 恢复账随层清空)
func _close_all_windows(views: Dictionary) -> void:
# oracle: design/gdd/ux/camp.md:45 (清窗组)
for k: StringName in [&"forge", &"chest", &"dummy", &"board"]:
var w: Control = views[k]
if not w.visible:
@@ -463,7 +487,7 @@ func _close_all_windows(views: Dictionary) -> void:
if back == null:
_problems.append("[winshell] %s 底栏「返回营地」钮缺失〔窗口化批 V4〕" % String(k))
continue
back.pressed.emit()
ProbeUiUtil.push_click(main, back) # 真点「返回营地」逐窗清账
await _frames()
@@ -480,25 +504,7 @@ func _window_panel(view: Control) -> PanelContainer:
return center.get_child(0) as PanelContainer
# 真实鼠标合成(press+release 成对——BaseButton pressed 信号默认 release 触发,ui_input_probe :718 先例)
func _push_click(pos: Vector2) -> void:
var ev := InputEventMouseButton.new()
ev.position = pos
ev.global_position = pos
ev.button_index = MOUSE_BUTTON_LEFT
ev.pressed = true
main.get_viewport().push_input(ev, true)
func _push_release(pos: Vector2) -> void:
var ev := InputEventMouseButton.new()
ev.position = pos
ev.global_position = pos
ev.button_index = MOUSE_BUTTON_LEFT
ev.pressed = false
main.get_viewport().push_input(ev, true)
# 键盘真输入合成(鼠标点击统一走 ProbeUiUtil.push_click_at——覆盖登记单点通道)
func _push_esc(echo := false) -> void:
var ev := InputEventKey.new()
ev.keycode = KEY_ESCAPE
+18 -7
View File
@@ -46,6 +46,7 @@ func _ready() -> void:
await get_tree().process_frame
# ① 菜单(启动态)——无帮助文腿(玩法速览牌不在画面,checker 无对应断言段);
# 无副标题件,菜单文字锚只剩标题
# oracle: design/engineering/code-conventions.md:54 模态覆盖层层级规则——boot 态顶层=start_overlay、菜单文字在屏(菜单组)
# 自检一:boot 态顶层可见全屏层=start_overlay(内容层树遍历——屏壳是容器不当候选:
# camp 壳 boot 态 vis=true 但成员恒隐,裸树遍历自顶向下会先命中透明壳)
var top: Node = null
@@ -82,11 +83,12 @@ func _ready() -> void:
_meta[String(pair[0])] = ProbeUiUtil.label_rect(main.ui.screens.start_overlay, String(pair[1]))
await _snap("menu")
# ①b 设置面(settingsbtn 真实信号路径,开合同款真实钮)
# oracle: design/gdd/systems/表现与支撑/设置与本地化.md:95 最小设置面已兑现:音量/减动效/全屏/退出,载体=独立设置存储+设置界面视图(设置组)
var setbtn: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "设置", "prefix")
if setbtn == null:
_problems.append("设置入口缺失(开始层 settingsbtn)")
else:
setbtn.pressed.emit()
ProbeUiUtil.push_click(main, setbtn)
for i in 3:
await get_tree().process_frame
if not main.ui.modals.menu.settings_overlay.visible:
@@ -98,9 +100,10 @@ func _ready() -> void:
if set_close == null:
_problems.append("设置面返回钮缺失")
else:
set_close.pressed.emit()
ProbeUiUtil.push_click(main, set_close)
await get_tree().process_frame
# ①c 生涯页(函数直调中间态——卡② mn10:careerbtn 已撤,卡④ 笔记本改挂前无 UI 路径)
# oracle: design/gdd/ux/camp.md:45 生涯页与四子界面同壳同形态(生涯组)
if true:
main._open_career()
for i in 3:
@@ -114,27 +117,31 @@ func _ready() -> void:
if career_close == null:
_problems.append("生涯页返回钮缺失")
else:
career_close.pressed.emit()
ProbeUiUtil.push_click(main, career_close)
for i in 3:
await get_tree().process_frame
if main.ui.modals.menu.career_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("生涯页关闭后页未隐藏或开始层未恢复")
# ② 营地场景图(进档开层新路径——卡② mn9;截图=可点击场景图形态——卡④ 契约 9 V1 验收载体)
# oracle: design/gdd/ux/camp.md:16 营地=可点击场景图(七物件命中区表——场景图形态组)
if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_shot_probe_camp"):
_problems.append("营地入口缺失(选档层新档钮——新路径驱动中断)〔句柄子容器批 V1〕")
else:
await get_tree().process_frame
await _snap("camp")
main.ui.modals.camp.scene.object_pressed.emit(&"campfire") # 收闭改接篝火动线(营地根态无关闭钮——卡④ T5)
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"campfire").get_center()) # 收闭改接篝火动线(营地根态无关闭钮——卡④ T5)
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) # 二次确认离场(关层收闭——选档层随收)
# oracle: design/gdd/ux/camp.md:22 篝火=离开营地回主菜单、必须二次确认(篝心动线组)
await get_tree().process_frame
# ③ 关卡画面(开始冒险→停点出图——入口停点语义;函数直调中间态 mn10)
# oracle: design/gdd/ux/level-map.md:11 入口停点候选数=入口个数(关卡画面出图组)
main._start_adventure_from_menu()
for i in 4:
await get_tree().process_frame
await _snap("level")
# GP 常驻徽章断言(读数=runner.gp 现值,刷新=写路径回执后重查〔open_choices〕)
# oracle: design/gdd/ux/level-map.md:77 GP 常驻徽章=GP 图标+数字(冒险钱包读数;写路径回执后重查)
var gp_label: Label = ProbeUiUtil.find_label(main.ui.modals.run.level_screen, "◈ ")
if gp_label == null or not gp_label.is_visible_in_tree():
_problems.append("GP 徽章不可见(关卡画面顶栏)")
@@ -143,6 +150,7 @@ func _ready() -> void:
if gp_label.text != expect:
_problems.append("GP 徽章读数不符: %s != %s" % [gp_label.text, expect])
# ④ 战斗(节点一——真实选路提交路径 press_candidate,ui_click_probe :199 同款)
# oracle: design/gdd/ux/battle.md:122 B 区构建:站桩/明细带/轮次指示(战斗出图与几何锚组)
if main.ui.modals.run.level_screen != null and main.ui.modals.run.level_screen.visible:
main.ui.modals.run.level_screen.press_candidate(0)
for i in 10:
@@ -154,17 +162,19 @@ func _ready() -> void:
_meta["stage_view"] = _node_rect(main.ui.battle.stage)
_meta["board_zone"] = _node_rect(main.ui.battle.board_zone)
# draw calls 真机测量(基线 270→目标 <200;窗口化渲染帧读数,
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:264 同屏 draw calls < 200(战斗屏性能预算组)
# 取值时机=战斗截图同帧后一帧——呈现终态满负荷)
await get_tree().process_frame
var dc := int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME))
_meta["battle_draw_calls"] = dc
print("[shot_probe] battle draw_calls=%d(基线 270/目标 <200)" % dc)
# ⑤ 状态面板(战斗屏状态角标真实信号路径;层在战斗屏之上开合)
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:277 模态打开遮挡或输入锁定、关闭逐层恢复(状态面板组)
var pb: Button = main.ui.battle.board_zone.status_badge
if pb == null or not pb.visible or pb.disabled:
_problems.append("状态面板入口角标缺失/置灰(战斗屏 board_zone)")
else:
pb.pressed.emit()
ProbeUiUtil.push_click(main, pb)
for i in 3:
await get_tree().process_frame
if not main.ui.modals.run.status_panel.visible:
@@ -176,7 +186,7 @@ func _ready() -> void:
if status_close == null:
_problems.append("状态面板继续钮缺失")
else:
status_close.pressed.emit()
ProbeUiUtil.push_click(main, status_close)
await get_tree().process_frame
if main.ui.modals.run.status_panel.visible:
_problems.append("状态面板关闭后未隐藏(继续钮路径)")
@@ -201,6 +211,7 @@ func _ready() -> void:
func _snap(key: String) -> void:
# oracle: design/engineering/test-conventions.md:84 golden 测试项扩项——截图采集清单扩项、实跑截图冻结(截图采集组)
await RenderingServer.frame_post_draw
await RenderingServer.frame_post_draw
var img: Image = get_viewport().get_texture().get_image()
+1
View File
@@ -13,6 +13,7 @@ var _problems: Array = []
func _ready() -> void:
# oracle: design/gdd/ux/battle.md:122 (明细带挖掘期收起——T17 视觉扩展时序组)
await get_tree().process_frame
main = load("res://presentation/main.tscn").instantiate()
add_child(main)
+1
View File
@@ -30,6 +30,7 @@ var _hero_pos := Vector2i.ZERO
func _ready() -> void:
# oracle: design/engineering/test-conventions.md:84 (微光截图取证——渲染闸采集组)
if DisplayServer.get_name() == "headless":
print("[type_glow_shot] FAIL — 本探针必须窗口化运行(headless 无渲染面,截图必空)")
get_tree().quit(1)
+93 -42
View File
@@ -11,8 +11,10 @@ extends Node
const AutoPlayer := preload("res://sim/auto_player.gd") # O4:自动玩家策略单一实现
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点
const ProbeCoverageRecorder := preload("res://probe/probe_coverage_recorder.gd")
var main: Node
var _recorder: RefCounted = null # 真输入覆盖登记件(probe_coverage_recorder.gd——attach 后全探针真输入自动记账)
var _last_state := {}
var _stall_frames := 0
var _closed_via_abandon := false # T17——预算耗尽经放弃路径收口(断点段返回断言豁免位)
@@ -48,12 +50,16 @@ func _ready() -> void:
main = load("res://presentation/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
# 真输入覆盖登记(命中控件自动记账,收尾落 tests/.gates/coverage/——覆盖面生长不靠人记)
_recorder = ProbeCoverageRecorder.new()
_recorder.attach(main, "ui_click")
var t_ui_ready := Time.get_ticks_msec() - t0
# 调试浮窗 boot 不开(菜单优先——入口=UI 最顶层全局浮层角标,
# 任何 UI 不遮挡;浮层角标遮挡判据见设置面段)
if main.ui.modals.menu.debug_overlay.visible:
_problems.append("调试浮窗 boot 自动开(菜单优先失守)")
# ---- 营地购买合成点击路径(选档进档开层→购买→关闭全走真实按钮信号路径;
# oracle: design/gdd/systems/跨局循环/天赋树.md:198 (营地购买段——单币购买只扣钥石组)
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json);陈旧档段首清理)----
for p0 in ["user://smoke_click_probe_reset.json", "user://smoke_click_probe_reset.json.tmp"]:
if FileAccess.file_exists(p0):
@@ -67,7 +73,7 @@ func _ready() -> void:
await get_tree().process_frame
if not main.ui.modals.camp.scene.visible:
_problems.append("营地场景根未打开(进档开层新路径)〔句柄子容器批 V1〕")
main.ui.modals.camp.scene.object_pressed.emit(&"dummy") # 场景点选真实路由(木桩→天赋树子界面——卡④ T5)
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center()) # 场景点选真输入(木桩→天赋树子界面——卡④ T5;_gui_input 命中)
await get_tree().process_frame
if main.ui.modals.camp.tree._tree_buy_btns.is_empty():
_problems.append("树淬炼购买钮缺失(四柜台拆解后树=独立子界面)〔句柄子容器批 V1〕")
@@ -85,7 +91,8 @@ func _ready() -> void:
else:
var bal_before: int = iso_c.meta_state.keystone_balance()
var price: int = int(main.db.crossrun.meta_tree.row_by_id("sp_hp").get("price", 0))
buy_btn.pressed.emit() # 合成购买点击 → _on_camp_tree_buy → meta.tree_buy(写调用唯一归组合根)
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, buy_btn) # 折叠区行先滚入窗板可视区(真输入滚轮)
ProbeUiUtil.push_click(main, buy_btn) # 真点购买 → _on_camp_tree_buy → meta.tree_buy(写调用唯一归组合根)
await get_tree().process_frame
if int(iso_c.meta_state.keystone_balance()) != bal_before - price:
_problems.append("营地树购买未出账(%d→%d 应 −%d)〔句柄子容器批 V1〕〔营地图批 V4〕" % [bal_before, iso_c.meta_state.keystone_balance(), price])
@@ -99,7 +106,8 @@ func _ready() -> void:
if String(card.node_id) == "att_socket1":
socket_btn = card.btn
if socket_btn != null:
socket_btn.pressed.emit()
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, socket_btn)
ProbeUiUtil.push_click(main, socket_btn) # 真点解锁(点亮同调位前置)
await get_tree().process_frame
main.ui.modals.camp.tree.refresh_camp(main._camp_data())
await get_tree().process_frame
@@ -110,7 +118,8 @@ func _ready() -> void:
if fork_btn == null:
_problems.append("Fork 解锁钮缺失(点击路径)〔句柄子容器批 V1〕")
else:
fork_btn.pressed.emit()
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, fork_btn) # fork 行在折叠区(rect 越窗底栏——实证误中「返回营地」钮)
ProbeUiUtil.push_click(main, fork_btn) # 真点解锁(展开 a/b 双态钮)
await get_tree().process_frame
main.ui.modals.camp.tree.refresh_camp(main._camp_data())
await get_tree().process_frame
@@ -121,7 +130,8 @@ func _ready() -> void:
if side_b == null:
_problems.append("Fork 双态切换钮缺失(解锁后应展开 a/b 钮)〔句柄子容器批 V1〕")
else:
side_b.pressed.emit() # 合成切态点击 → _on_camp_fork_toggle → meta.tree_toggle_fork
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, side_b)
ProbeUiUtil.push_click(main, side_b) # 真点切态 → _on_camp_fork_toggle → meta.tree_toggle_fork
await get_tree().process_frame
var iso_fk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp_slot_1.json")
iso_fk.load_from_disk()
@@ -130,9 +140,15 @@ func _ready() -> void:
# ---- 营地灰字取色断言(迭代36 wc4——四子界面拆分后按视图分扫;卡④ T5 迁移):
# 图鉴锁文→codex 视图(锚随形态订正去『柜台』);秘宝行未解锁/同调形态/档位/刷新行→treasure 视图
# _workshop_holder 子树。font_color 逐处 == STONE_DEEP;树链段已闭树界面。
main.ui.modals.camp.tree.closed.emit() # 树界面返回(关层归 presenter 单口)
var tree_back: Button = ProbeUiUtil.find_button(main.ui.modals.camp.tree, "返回营地")
if tree_back == null:
_problems.append("树界面「返回营地」钮缺失(真输入化驱动中断)〔句柄子容器批 V1〕")
else:
ProbeUiUtil.push_click(main, tree_back) # 真点返回(关层归 presenter 单口——closed 同源钮)
await get_tree().process_frame
main.ui.modals.camp.scene.object_pressed.emit(&"chest") # 宝箱→秘宝匣(灰字四源位宿主)
if main.ui.modals.camp.tree.visible:
_problems.append("树窗「返回营地」真点击后未隐窗(真输入化补钉——落点判定)〔句柄子容器批 V1〕")
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"chest").get_center()) # 宝箱→秘宝匣(灰字四源位宿主)
await get_tree().process_frame
var ws_labels: Array = []
_collect_labels_by_text(main.ui.modals.camp.treasure._workshop_holder, ws_labels)
@@ -169,9 +185,13 @@ func _ready() -> void:
_problems.append("营地灰字:刷新行未用 STONE_DEEP(wc4 :525)〔句柄子容器批 V1〕")
if not refresh_inked:
_problems.append("营地灰字:刷新行 label 未找到(wc4 定位锚失守)〔句柄子容器批 V1〕")
main.ui.modals.camp.treasure.closed.emit()
var treasure_back: Button = ProbeUiUtil.find_button(main.ui.modals.camp.treasure, "返回营地")
if treasure_back == null:
_problems.append("秘宝匣「返回营地」钮缺失(真输入化驱动中断)〔句柄子容器批 V1〕")
else:
ProbeUiUtil.push_click(main, treasure_back) # 真点返回(closed 同源钮)
await get_tree().process_frame
main.ui.modals.camp.scene.object_pressed.emit(&"board") # 布告板→图鉴占位(锁文宿主)
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"board").get_center()) # 布告板→图鉴占位(锁文宿主)
await get_tree().process_frame
var codex_labels: Array = []
_collect_labels_by_text(main.ui.modals.camp.codex, codex_labels)
@@ -181,13 +201,18 @@ func _ready() -> void:
for l in atlas_locks:
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
_problems.append("营地灰字:图鉴锁文未用 STONE_DEEP(wc4 :167)〔句柄子容器批 V1〕〔营地图批 V6〕")
main.ui.modals.camp.codex.closed.emit()
var codex_back: Button = ProbeUiUtil.find_button(main.ui.modals.camp.codex, "返回营地")
if codex_back == null:
_problems.append("图鉴「返回营地」钮缺失(真输入化驱动中断)〔句柄子容器批 V1〕")
else:
ProbeUiUtil.push_click(main, codex_back) # 真点返回(closed 同源钮)
await get_tree().process_frame
main._on_camp_closed() # 关层收闭(营地与选档层同收+开始层恢复——卡④ T3)
await get_tree().process_frame
if main.ui.modals.camp.scene.visible or not main.ui.screens.start_overlay.visible:
_problems.append("营地关闭后未恢复开始层〔句柄子容器批 V1〕")
# ---- 选档界面行为走查(迭代 33 卡③):sl5 空槽新档链/sl6 有档槽进档链/sl7 删档二次确认双路径
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:61 (选档走查段——激活态建立组)
# /sl8 消费方分派单口/sl12 动线序不回闪。开层=函数直调(「进入存档」钮归卡② mn3——中间态在案);
# 隔离=attach_slot_template 注入命名空间,夹具按槽直写(卡① 注入口)。----
var csel_tpl := "user://smoke_click_probe_savesel_slot_%d.json"
@@ -213,7 +238,7 @@ func _ready() -> void:
_problems.append("[savesel] 槽1 新档钮缺失(sl5/sl12 驱动中断)")
else:
var samples: Array = []
new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp
ProbeUiUtil.push_click(main, new_btn) # 真点「新档」→ _on_save_select_new → create_slot+open_camp
for fi in 3:
await get_tree().process_frame
samples.append([main.ui.modals.menu.save_select_overlay.visible, main.ui.modals.camp.scene.visible, main.ui.screens.start_overlay.visible])
@@ -239,7 +264,7 @@ func _ready() -> void:
if load_btn == null:
_problems.append("[savesel] 槽2 进档钮缺失(sl6 驱动中断——夹具应有档)")
else:
load_btn.pressed.emit()
ProbeUiUtil.push_click(main, load_btn) # 真点「进档」→ 激活槽2 开营地
await get_tree().process_frame
if main.save_manager.slots.active_slot() != 2:
_problems.append("[savesel] sl6 进档链失守(active=%d 应=2——激活该槽)" % main.save_manager.slots.active_slot())
@@ -254,7 +279,7 @@ func _ready() -> void:
if del_btn == null:
_problems.append("[savesel] 槽2 删档钮缺失(sl7 驱动中断)")
else:
del_btn.pressed.emit()
ProbeUiUtil.push_click(main, del_btn) # 真点「删档」一击只开窗
await get_tree().process_frame
if not main.ui.modals.menu.confirm_dialog.is_open():
_problems.append("[savesel] sl7 一击未只开确认窗(直删形态)")
@@ -265,7 +290,7 @@ func _ready() -> void:
_problems.append("[savesel] sl7 确认窗未经模态栈开(直开旁路——wr2 单口失守)")
elif not main.ui.flow.locks_input():
_problems.append("[savesel] sl7 确认窗开时输入锁未生效(锁域失守——wr2)")
main.ui.modals.menu.confirm_dialog.cancelled.emit() # 取消路径(真实信号——cancelled 钮同源)
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._cancel_btn) # 真点「取消」(cancelled 钮同源)
await get_tree().process_frame
if main.ui.modals.menu.confirm_dialog.is_open() or not FileAccess.file_exists(csel_tpl % 2):
_problems.append("[savesel] sl7 取消路径失守(应只关窗、文件仍在)")
@@ -273,9 +298,9 @@ func _ready() -> void:
_problems.append("[savesel] sl8 取消后消费方状态量未清(分派面残留)")
if main.ui.flow.stack._occluded.has(main.ui.modals.menu.confirm_dialog):
_problems.append("[savesel] sl7 取消后确认窗遮挡账未清(关侧未走模态栈——wr2)")
del_btn.pressed.emit() # 再开窗走确认路径
ProbeUiUtil.push_click(main, del_btn) # 真点再开窗走确认路径
await get_tree().process_frame
main.ui.modals.menu.confirm_dialog.confirmed.emit() # 确认路径(真实信号——confirmed 钮同源)
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._confirm_btn) # 真点「确认」(confirmed 钮同源)
await get_tree().process_frame
if FileAccess.file_exists(csel_tpl % 2) or main.ui.modals.menu.confirm_dialog.is_open():
_problems.append("[savesel] sl7 确认路径失守(应删档+关窗)")
@@ -291,6 +316,7 @@ func _ready() -> void:
_problems.append("[savesel] 关闭后层未隐藏或开始层未恢复(模态栈单口失守)")
# ---- 设置面段(隔离 store 换注禁碰真实 user://settings.cfg;真实按钮信号路径)----
# oracle: design/gdd/systems/表现与支撑/设置与本地化.md:4 (设置面段——最小设置面组)
for p0 in ["user://smoke_click_probe_settings.cfg"]:
if FileAccess.file_exists(p0):
DirAccess.remove_absolute(p0)
@@ -343,7 +369,7 @@ func _ready() -> void:
if settings_close == null:
_problems.append("设置面返回按钮缺失")
else:
settings_close.pressed.emit()
ProbeUiUtil.push_click(main, settings_close) # 真点「返回」收设置面
await get_tree().process_frame
if main.ui.modals.menu.settings_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("设置面关闭后未恢复开始层")
@@ -357,6 +383,7 @@ func _ready() -> void:
if ProbeUiUtil.find_button(main.ui.modals.menu.settings_overlay, "退出游戏", "prefix") != null:
_problems.append("设置面仍含退出游戏按钮(二轮 D1——只在一级菜单出现)")
# ---- 生涯页段(入口点击链+关闭回返,真实按钮信号路径;本页只读零写,
# oracle: design/gdd/ux/camp.md:45 (生涯页段——同壳同形态组)
# 值断言归冒烟投影段与 ui_struct 结构族,此处只验开合路径与零跳转)----
# 生涯页驱动=函数直调中间态(卡② mn10——careerbtn 已撤,卡④ 笔记本改挂前无 UI 路径)
if true:
@@ -377,7 +404,7 @@ func _ready() -> void:
if career_close == null:
_problems.append("生涯页返回按钮缺失")
else:
career_close.pressed.emit()
ProbeUiUtil.push_click(main, career_close) # 真点「返回」收生涯页
await get_tree().process_frame
if main.ui.modals.menu.career_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("生涯页关闭后未恢复开始层")
@@ -463,7 +490,11 @@ func _ready() -> void:
# 分派链序:商店层=选路页上的模态覆盖(停点呈现为
# level_screen+shop 双 visible)——必须先于 level_screen 分派,否则下层候选点击
# 空转烧尽预算(实证:lv2 商店停点双 visible 循环 12000 帧 done=false)。
main.ui.modals.menu.shop_overlay.leave_pressed.emit()
var shop_leave: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店")
if shop_leave == null:
_problems.append("商店「离开商店」钮缺失(真输入化驱动中断)")
else:
ProbeUiUtil.push_click(main, shop_leave) # 真点离店(leave_pressed 同源钮)放行选路
_seals_placed.clear()
continue
if main.ui.modals.run.level_screen.visible:
@@ -476,12 +507,13 @@ func _ready() -> void:
if mpb == null or not mpb.visible or mpb.disabled:
_problems.append("地图屏状态角标缺失/未点亮/被置灰(P1 丁)")
else:
mpb.pressed.emit()
ProbeUiUtil.push_click(main, mpb) # 真点地图屏角标开状态面板
if not main.ui.modals.run.status_panel.visible or not main.ui.modals.run.level_screen.visible:
_problems.append("地图屏角标开状态面板失败或地图屏被误藏(P2 甲层级失守)")
await get_tree().process_frame # 面板当帧开——容器排版帧后钮 rect 才有效(真点击依赖排版)
var rbtn2: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix")
if rbtn2 != null:
rbtn2.pressed.emit()
ProbeUiUtil.push_click(main, rbtn2) # 真点「继续」关面板
if main.ui.modals.run.status_panel.visible or not main.ui.modals.run.level_screen.visible:
_problems.append("状态面板关闭后地图屏态失守(恢复语义破)")
# 地图屏加成帽换代断言(迭代36 卡A wa2——「特性」→「属性值提升」;帽 Label 常在树;
@@ -519,7 +551,8 @@ func _ready() -> void:
if next_btn == null:
_problems.append("大奖励屏确认按钮缺失")
else:
next_btn.pressed.emit()
await get_tree().process_frame # 停点层当帧开——排版帧后真点击
ProbeUiUtil.push_click(main, next_btn) # 真点「进入下一层」推进 L2(落点判定见下)
await get_tree().process_frame
await get_tree().process_frame
if main.run_scope.runner == null or int(main.run_scope.runner.level_row.get("level_index", -1)) != 2:
@@ -540,7 +573,7 @@ func _ready() -> void:
if main.ui.modals.run.end_overlay.verdict_text() == "":
_problems.append("胜利结算框判定行缺失")
_settlement_confirms += 1
main.ui.modals.run.end_overlay._btn.pressed.emit() # 「下一节点」真实信号路径
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 真点「下一节点」(结算框已过帧排版)
await get_tree().process_frame
await get_tree().process_frame
if main.run_scope.settlement_stop_timer.is_armed() or not main.ui.modals.run.level_screen.visible:
@@ -555,7 +588,7 @@ func _ready() -> void:
# (_adventure_mode)在真终局仍 true——会把真终局当中途面点掉返回键(实证)。
var _adv_running: bool = main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running"
if main.run_scope.is_adventure and _adv_running:
main.ui.modals.run.end_overlay._btn.pressed.emit()
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 中途结算面真点「继续」推进
continue
if not main.ui.flow.locks_input():
_problems.append("终局 overlay 打开时输入未锁定")
@@ -601,6 +634,7 @@ func _ready() -> void:
_run_modal_probes_once()
_play_one_action()
# ---- 预算耗尽收口(T17 硬锁批适配):选件变强后 probe 可能活过预算不再战败——断点段前提
# oracle: design/gdd/ux/level-map.md:11 (主循环选路段——入口候选组)
# (冒险终局面在案)须主动满足。收口驱动:任何停点层逐个推进(复用主循环分支语义)至
# level_screen 选路停点→真实放弃二击→终局面。战斗中则继续 autoplay 打完。
if not _done and not main.ui.modals.run.end_overlay.is_open():
@@ -612,7 +646,7 @@ func _ready() -> void:
if main.ui.modals.run.end_overlay.is_open():
# 中途结算面(state=running)=点继续推进;真终局(settling)=断点段前提满足,收口。
if main.run_scope.is_adventure and main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running":
main.ui.modals.run.end_overlay._btn.pressed.emit()
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 中途结算面真点「继续」推进
else:
break
elif main.ui.modals.run.curio_overlay.visible:
@@ -626,18 +660,20 @@ func _ready() -> void:
elif main.ui.modals.run.node_overlay.visible:
main.ui.modals.run.node_overlay.press_continue()
elif main.ui.modals.menu.shop_overlay.visible:
main.ui.modals.menu.shop_overlay.leave_pressed.emit()
var shop_leave2: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店")
if shop_leave2 != null:
ProbeUiUtil.push_click(main, shop_leave2) # 真点离店(收口段同主循环分派)
elif main.ui.screens.inter_overlay.visible:
var next_btn2: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix")
if next_btn2 != null:
next_btn2.pressed.emit()
ProbeUiUtil.push_click(main, next_btn2) # 真点「进入下一层」(收口段)
elif main.ui.modals.run.level_screen.visible:
var abandon_btn: Button = ProbeUiUtil.find_button(main.ui.modals.run.level_screen, "放弃", "prefix")
if abandon_btn != null:
_closed_via_abandon = true
abandon_btn.pressed.emit()
ProbeUiUtil.push_click(main, abandon_btn) # 真点「放弃」一击
await get_tree().process_frame
abandon_btn.pressed.emit() # 二击确认(3s 窗内)
ProbeUiUtil.push_click(main, abandon_btn) # 真点二击确认(3s 窗内)
for i in 10:
await get_tree().process_frame
break
@@ -646,6 +682,7 @@ func _ready() -> void:
_play_one_action()
# 排播期/Waiting 等待态=自然流逝(演播秒级、锁随队列空解除)
# T17 适配:战败或放弃二途皆达终局面(放弃=预算耗尽收口路径——见上),断点段前提不变。
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:180 (断点段——断点域与续玩组)
await get_tree().process_frame
var bp_file := "user://smoke_click_probe_reset.json"
var iso_chk = load("res://core/save_manager.gd").new(bp_file)
@@ -665,7 +702,7 @@ func _ready() -> void:
if main.ui.modals.run.end_overlay._btn == null:
_problems.append("断点段:终局面返回按钮缺失")
else:
main.ui.modals.run.end_overlay._btn.pressed.emit() # 真实按钮信号路径(_btn=继续/返回共用句柄)
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 真点「返回」(_btn=继续/返回共用句柄)
await get_tree().process_frame
if not main.ui.screens.start_overlay.visible:
# 豁免:放弃路径收口时终局「返回」后仍挂三选一/关卡推进残留态(停点链),
@@ -677,6 +714,7 @@ func _ready() -> void:
# ① 战斗收口断点写盘断言保留(上方——冒险流直调中间态 mn10)。
# ---- 大门两分支断言段(卡④ T5 接收——卡② 移交落点;V3 断言面)----
# oracle: design/gdd/ux/camp.md:16 (大门段——点大门先出确认组)
# 驱动=营地场景根点选信号(真实路由)→组合根 _on_camp_object_pressed→确认弹窗。
# 无断点=两出口(是/否);有断点=三出口(set_breakpoint 注入选路停点形——同下方开场行③先例)。
# 判据绑可观测提交面:点大门不直接进冒险(is_adventure 翻转只在 confirmed 后);cancelled 恒=
@@ -693,7 +731,7 @@ func _ready() -> void:
_problems.append("大门无断点:未确认即进冒险〔营地图批 V3〕")
if main.ui.modals.menu.confirm_dialog._secondary_btn.visible:
_problems.append("大门无断点:次钮应隐藏(两出口形态)〔营地图批 V3〕")
main.ui.modals.menu.confirm_dialog.cancelled.emit()
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._cancel_btn) # 真点「取消」(大门取消出口)
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〕")
@@ -795,12 +833,15 @@ func _ready() -> void:
exit_code = 1
if not _problems.is_empty():
exit_code = 1
_recorder.dump_js() # 收尾落盘(大门段/开场行段点击在循环退出后——落盘必须在 quit 前)
get_tree().quit(exit_code)
# ---- 模态/tooltip/刷新链一次性覆盖(真实信号路径驱动) ----
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:277 (模态/tooltip 段——遮挡与输入锁组)
func _run_modal_probes_once() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:277 (判定上报通道——验证函数组)
# ⓪ 玩家面文案真实入口断言(迭代36 卡A wa1——C 区两段头随战斗常驻)
if not _tested_copy and main.ui.battle.info_panel != null and main.ui.battle.info_panel.visible:
_tested_copy = true
@@ -821,7 +862,7 @@ func _run_modal_probes_once() -> void:
# ① 图例模态(pattern 6):打开 → 遮挡物隐藏 + 输入锁定;关闭 → 恢复
if not _tested_legend:
_tested_legend = true
main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit()
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.ap_bar._btn_legend) # 真点图例钮开层
if not main.ui.modals.run.legend_overlay.is_open():
_problems.append("图例打开失败")
if main.ui.modals.run.end_overlay.is_open() or main.ui.screens.start_overlay.visible:
@@ -851,7 +892,7 @@ func _run_modal_probes_once() -> void:
_problems.append("战斗屏状态角标缺失/未点亮/被置灰(P1 丁入口失守 visible=%s disabled=%s)"
% [str(pbtn != null and pbtn.visible), str(pbtn != null and pbtn.disabled)])
else:
pbtn.pressed.emit() # 真实按钮信号路径 → 组合根 _on_status_badge_pressed
ProbeUiUtil.push_click(main, pbtn) # 真点状态角标 → 组合根 _on_status_badge_pressed
if not main.ui.modals.run.status_panel.visible:
_problems.append("状态面板未随角标打开(组合根接线失守)")
if not main.ui.flow.locks_input():
@@ -903,30 +944,32 @@ func _run_modal_probes_once() -> void:
if resume_btn == null:
_problems.append("状态面板关闭钮缺失(「继续」)")
else:
resume_btn.pressed.emit()
await get_tree().process_frame # 面板当帧开——排版帧后真点击
ProbeUiUtil.push_click(main, resume_btn) # 真点「继续」关面板恢复
if main.ui.modals.run.status_panel.visible or main.ui.flow.locks_input():
_problems.append("状态面板关闭后未恢复(visible=%s locked=%s——R3 残留)"
% [str(main.ui.modals.run.status_panel.visible), str(main.ui.flow.locks_input())])
# P1 守卫:图例开着 → 两处入口角标置灰;置灰态点击被处理器兜住(探针 emit 绕过 disabled)
main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit()
# P1 守卫:图例开着 → 两处入口角标置灰;置灰态点击被处理器兜住
#(置灰态穿透判定保留 emit 直发——真输入到不了置灰钮,处理器级防线只能以〔接线断言〕钉住)
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.ap_bar._btn_legend) # 真点图例钮复开
if not main.ui.battle.board_zone.status_badge.disabled:
_problems.append("图例开着时战斗屏状态角标未置灰(P1 守卫失守)")
main.ui.battle.board_zone.status_badge.pressed.emit()
main.ui.battle.board_zone.status_badge.pressed.emit() # 〔接线断言〕置灰态 emit 穿透——钉处理器兜底(真输入被 disabled 拦,测不到此层)
if main.ui.modals.run.status_panel.visible:
_problems.append("置灰态仍可开状态面板(守卫兜底失守)")
main.ui.modals.run.legend_overlay.close()
# ③ tooltip(状态效果槽悬停——来源+语义+剩余):有槽可见时触发
# ③ tooltip(状态效果槽悬停——来源+语义+剩余):有槽可见时触发(真输入 motion 进管线触发 entered/exited 沿)
if not _tested_tooltip:
var row = main.ui.battle.stage.hero_standee.slot_row # 状态 tooltip 载体(B 区站桩)
for child in row.get_children():
if child is PanelContainer and child.visible and child.has_meta("eff"):
_tested_tooltip = true
child.mouse_entered.emit()
ProbeUiUtil.push_motion(main, (child as PanelContainer).get_global_rect().get_center())
if not row._tip.visible:
_problems.append("状态槽 tooltip 未显示")
elif not row._tip_label.text.contains("来源") or not row._tip_label.text.contains("剩余"):
_problems.append("状态槽 tooltip 缺来源/剩余(%s)" % row._tip_label.text.replace("\n", "|"))
child.mouse_exited.emit()
ProbeUiUtil.push_motion(main, Vector2(-40, -40)) # 移出一切控件(视口外沿触发 exited)
if row._tip.visible:
_problems.append("状态槽 tooltip 未随移出隐藏")
break
@@ -972,6 +1015,7 @@ func _labels_with_text(labels: Array, needle: String) -> Array:
func _play_one_action() -> void:
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:236 (判定上报通道——验证函数组)
var settlement = main.run_scope.settlement
if settlement == null:
return # 结算竞态防抖:battle_finished 与本轮动作调度同帧到达时 settlement 可能已被置空(基线既有噪声——加固)
@@ -1012,6 +1056,7 @@ func _play_one_action() -> void:
func _random_unknown_adjacent(minesweeper) -> Vector2i:
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:236 (判定上报通道——验证函数组)
var hero: Vector2i = minesweeper.hero_pos()
var cands: Array = []
for dy in range(-1, 2):
@@ -1027,11 +1072,13 @@ func _random_unknown_adjacent(minesweeper) -> Vector2i:
func _click_stop() -> void:
# 停手走真实按钮信号路径(AP 条 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体)
main.ui.battle.board_zone.ap_bar._btn_stop.pressed.emit()
# oracle: design/gdd/systems/排雷循环/行动经济.md:262 (判定上报通道——验证函数组)
# 停手走真实点击路径(AP 条「停手」钮 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体)
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.ap_bar._btn_stop)
# ---- T1 D1:锁期/重放断言(每 run 一次,脚本化场景——演播期豁免的断言化承载) ----
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:236 (锁期/重放段——拒绝与锁组)
# 锁期断言(排播队列活跃=锁面):注入封印击 → 直落被冻结不受理(封印未置)+ `_locked_click`
# 缓存命中且为本注入(「最后一次」语义——注入帧内无并发点击)。
# 重放断言(演播完边沿 pump_ended→_replay_locked_click 重入唯一动作通道):缓存清空 +
@@ -1039,6 +1086,7 @@ func _click_stop() -> void:
# 前提消失守卫:等重放期间出现停点层/战斗收场(注入时点恰逢终结演播)→ 静默弃测不计
# problem(封印动作不杀伤,正常局不触发;断言实现形态取舍在代拍白名单内)。
func _locktest_tick() -> void:
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:236 (判定上报通道——验证函数组)
if _locktest_state == 2:
return
var settlement = main.run_scope.settlement
@@ -1095,6 +1143,7 @@ func _locktest_tick() -> void:
func _locktest_pick_cell(minesweeper) -> Vector2i:
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:236 (判定上报通道——验证函数组)
var hero: Vector2i = minesweeper.hero_pos()
for dy in range(-1, 2):
for dx in range(-1, 2):
@@ -1107,6 +1156,7 @@ func _locktest_pick_cell(minesweeper) -> Vector2i:
func _report_stall(st: Dictionary) -> void:
# oracle: design/engineering/test-conventions.md:30 (判定上报通道——验证函数组)
var why := ""
if st.phase == "Waiting":
why = "结算停滞:Waiting 相位无推进(结算应同步走完——窗口段已删除)"
@@ -1122,6 +1172,7 @@ func _report_stall(st: Dictionary) -> void:
func _dump_state() -> void:
# oracle: design/engineering/test-conventions.md:30 (判定上报通道——验证函数组)
var diag: Dictionary = main.run_scope.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
print(" node_index=%s round=%s phase=%s ap=%s rap=%s" % [
str(main.run_scope.runner.node_index), str(main.run_scope.settlement.round_index),
+1
View File
@@ -88,6 +88,7 @@ func _ratio(a: Color, b: Color) -> float:
func _ready() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:281 (UI 文字对比度机检 ≥4.5:1 无区域例外——对比度组)
var problems: Array = []
var pairs := _pairs()
for p in pairs:
+15 -1
View File
@@ -242,6 +242,7 @@ func _ready() -> void:
_pre_fail("目标格选取失败 seal=%s dig=%s forbid=%s reveal=%s" % [seal_t, dig_t, forbid_t, reveal_t])
return
# ---- case① 悬停可挖域:motion → 悬停态到达(金框 + 十字光标)----
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:269 悬停光标三语义随目标切换、已开格路径预览、可挖域常驻微光(case①-③ 输入面)
_push_motion(_pos(bv, seal_t))
await get_tree().process_frame
if bv.hover_probe() != seal_t:
@@ -270,6 +271,7 @@ func _ready() -> void:
if bv.mouse_default_cursor_shape != Input.CURSOR_ARROW:
_problems.append("case③ 移动光标缺失(%d)" % bv.mouse_default_cursor_shape)
# ---- case④ 右键封印:seal_place 到达组合根 + 核心封印态落位 ----
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:231 封印受理与印记随挖消失(case④/④b)
_push_click(_pos(bv, seal_t), MOUSE_BUTTON_RIGHT)
await get_tree().process_frame
if not _has_action(seal_t, "seal_place"):
@@ -283,12 +285,14 @@ func _ready() -> void:
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 ----
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:236 拒绝三源可辨、任何拒绝零副作用(case⑤ chord)
_push_click(_pos(bv, forbid_t), MOUSE_BUTTON_LEFT)
_push_click(_pos(bv, forbid_t), MOUSE_BUTTON_LEFT)
await get_tree().process_frame
if _chords.is_empty() or _chords[_chords.size() - 1] != forbid_t:
_problems.append("case⑤ chord_attempted 未达(chords=%s)" % [str(_chords)])
# ---- case⑥ 左键挖:dig 到达组合根 + 核心揭格落位 ----
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:231 挖掘受理与揭格落位(case⑥)
_push_click(_pos(bv, dig_t), MOUSE_BUTTON_LEFT)
await get_tree().process_frame
if not _has_action(dig_t, "dig"):
@@ -297,6 +301,7 @@ func _ready() -> void:
if not ms.is_revealed(dig_t):
_problems.append("case⑥ 核心揭格未落位(目标=%s)" % dig_t)
# ---- case⑦ 英雄精灵世界落位(回归闸):精灵在 SubViewport 世界内,
# oracle: design/gdd/ux/battle.md:154 主角站桩立绘带落影、贴脚与缩放档(case⑦-⑩ 呈现落位)
# 落位 = 格×步距 + 脚底锚(横=格中线/纵=格底往上 25% 线=HERO_GROUND_FRAC 自底边口径——「脚踩
# 格子 1/4 处·从下往上数」;禁带壳层 _svp_container.position 居中偏移——上批误用壳层
# cell_origin 实测右偏 ~7 格+偏离格心,实机截图用户报缺陷)。素材缺位树(hero==null,字形
@@ -363,6 +368,7 @@ func _ready() -> void:
if bv._world.hero.position.distance_to(expect10b) > 0.5:
_problems.append("case⑩ 还原后英雄未复位(pos=%s expect=%s)" % [str(bv._world.hero.position), str(expect10b)])
# ---- case⑪ 贴图缺位回退(字形「人」回归闸):texture 置 null + refresh → 英雄格数字 0 时字形
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:264 棋盘格子与数字完整可见——贴图缺位回退字形保可读(case⑪)
# 必须回退「人」(数字格仍显数字——回退判据与抑制判据同源);验毕还原纹理重绘。 ----
if bv._world.hero != null:
var hp11: Vector2i = ms.hero_pos()
@@ -379,6 +385,7 @@ func _ready() -> void:
bv._hero_sprite.tex_idle = tex_saved
bv.refresh()
# ---- case⑭ 变异角标(flat_damage 未开格视觉标记;resolver 注入面+落位+复位) ----
# oracle: design/gdd/systems/排雷循环/地形变异.md:154 呈现按 effect/trigger/placement 数据路由、零特判(case⑭ 角标)
var mark_t: Vector2i = _pick_forbidden(bv, ms) # 任一未开格(禁入=未开非相邻)
if mark_t.x >= 0:
bv.set_variant_mark_resolver(func(_c): return "flat_damage")
@@ -392,6 +399,7 @@ func _ready() -> void:
else:
_problems.append("case⑭ 无未开格可验(板面异常)")
# ---- case⑮ 视野迷雾(三态呈态/半径圈/移动跟随/开关两态零扰动/提灯插值位;
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:268 视野迷雾:灯光区随移动跟随、开关两态结算逐位一致(case⑮)
# 纯呈现零逻辑——迷雾开关切换前后 minesweeper 状态摘要逐位一致) ----
var fogp: Dictionary = bv.fog_params_probe()
if int(bv.FOG_RADIUS) != 8 or int(fogp["lantern"]) != 0 or not fogp["enabled"]:
@@ -487,6 +495,7 @@ func _ready() -> void:
# attune_param_sum 只读出口→set_fog_glow_bonus〕→视野 +4 生效与卸装复原。
# settlement.attunement 手动注入=模拟 begin_battle enc 注入后的运行态〔注入面本体归
# 冒烟+单测覆盖〕;不重启冒险——case⑯ 起既有板面继续用)
# oracle: design/gdd/systems/跨局循环/元进度.md:156 解锁可见:装配强化下一局生效(case⑮ 装配链)
main.save_manager.meta_state.set_attunement_equipped(["wide_scout_band"])
var p_lantern: Dictionary = main.build_adventure_params(main.save_manager, main.meta)
if not (p_lantern.get("attunement", []) as Array).has("wide_scout_band"):
@@ -505,6 +514,7 @@ func _ready() -> void:
_problems.append("case⑮ 卸装后 dist==R+1 格未复原遮蔽")
main.save_manager.meta_state.set_attunement_equipped([])
# ---- case⑯ 状态面板输入锁语义(R3——开层锁棋盘、关层恢复无残留;真实 push_input
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:277 模态打开输入锁定、关闭逐层恢复(case⑯ R3)
# 路径:入口角标本体 press+release 真点击 → 状态面板开;锁期挖掘点击零穿透;「继续」关层后
# 同格挖掘重新到达——锁语义与既有模态同款,无状态残留。置于全 case 之尾:取格若需前沿移位
#(真实点击 move 扩大可挖域),其板面扰动不再波及任何既有断言——域恰 2 枚时 seal_t 带封印+
@@ -547,7 +557,9 @@ func _ready() -> void:
if resume_btn == null:
_problems.append("case⑯ 状态面板关闭钮缺失")
else:
resume_btn.pressed.emit()
var rpos16: Vector2 = resume_btn.get_global_rect().get_center()
_push_click(rpos16, MOUSE_BUTTON_LEFT)
_push_release(rpos16, MOUSE_BUTTON_LEFT) # Button pressed 信号走 release 沿——按压+释放成对
await get_tree().process_frame
if main.ui.modals.run.status_panel.visible or main.ui.flow.locks_input():
_problems.append("case⑯ 关闭后未恢复(visible=%s locked=%s——R3 残留)"
@@ -558,6 +570,7 @@ func _ready() -> void:
if not _has_action(status_dig, "dig") or not ms.is_revealed(status_dig):
_problems.append("case⑯ 关闭后挖掘未到达(恢复无残留验证失守)")
# ---- case⑰ 未开格类型微光(活板面走查:作用集恒等/开格灭/移动复亮
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:267 类型微光出圈即灭入圈即亮、已开与封锁格恒无微光(case⑰)
# 与出圈灭/相位门控/开关两态零扰动。与冒烟段数据面断言分工:此处走真实点击路径) ----
if not bool(bv.type_glow_probe().get("enabled", false)):
_problems.append("case⑰ 微光默认态非开(生产默认开——Q4 丙案)")
@@ -746,6 +759,7 @@ func _push_release(pos: Vector2, btn: int) -> void:
func _pre_fail(why: String) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
push_error("[ui_input_probe] FAIL — " + why)
print("[ui_input_probe] FAIL — " + why)
get_tree().quit(1)
+20 -6
View File
@@ -16,6 +16,7 @@ func _ready() -> void:
await get_tree().process_frame
var problems: Array = []
# ---- 放弃入口(台账 119):默认隐藏(非冒险模式无冒险可放弃——开始层/调试两层)----
# oracle: design/gdd/ux/level-map.md:59 放弃冒险入口=边缘动作、任意选路停点承载——非冒险模式无入口(默认隐藏)
if main.ui.modals.run.level_screen.abandon_btn == null or main.ui.modals.run.level_screen.abandon_btn.visible:
print("[ui_probe] FAIL — 放弃按钮默认应隐藏(非冒险模式关)")
get_tree().quit(1)
@@ -23,6 +24,7 @@ func _ready() -> void:
# ---- 营地覆盖层(主菜单入口/开合/余额文案/购买禁用态;
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json——SaveManager 路径注入纪律;
# 营地先行段只换 meta 不换 save_manager——冒险段换注零档在前)----
# oracle: design/gdd/ux/camp.md:45 五功能窗统一窗壳、五窗互斥单开、各窗自带关闭钮(营地开合组)
ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", false, false, false)
if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_probe_camp"):
problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕")
@@ -34,10 +36,12 @@ func _ready() -> void:
problems.append("营地层打开时开始层未隐藏(规范 2.3 遮挡纪律)〔句柄子容器批 V1〕")
else:
# 天赋树子界面(场景点选真实路由——木桩命中区进树界面;卡④ T5 深员改指)
main.ui.modals.camp.scene.object_pressed.emit(&"dummy")
# oracle: design/gdd/ux/camp.md:20 木桩命中区→天赋树界面入口(场景点选路由)
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center())
await get_tree().process_frame
if not main.ui.modals.camp.tree.visible:
problems.append("天赋树子界面未打开(场景点选路由中断)〔营地图批 V2〕")
# oracle: design/gdd/ux/camp.md:41 钥石余额常驻画面顶部 HUD 栏(余额文案组)
if not String(main.ui.modals.camp.tree._balance_label.text).begins_with("钥石"):
problems.append("营地余额文案缺失(%s)〔句柄子容器批 V1〕" % main.ui.modals.camp.tree._balance_label.text)
if main.ui.modals.camp.tree._tree_buy_btns.is_empty():
@@ -50,14 +54,15 @@ func _ready() -> void:
problems.append("脊柱生命 L1 白板可购钮未启用(无门槛——can_buy 不含余额面语义)〔句柄子容器批 V1〕")
if not btn_by_id.has("att_socket2") or not bool(btn_by_id["att_socket2"].disabled):
problems.append("同调位 2 前置未亮应禁用(点亮前置语义)〔句柄子容器批 V1〕")
main.ui.modals.camp.tree.closed.emit()
ProbeUiUtil.push_click(main, ProbeUiUtil.find_button(main.ui.modals.camp.tree, "返回营地")) # 真点底栏返回钮→_emit_closed(camp_sub_view_base.gd:66-84)
await get_tree().process_frame
# 秘宝匣子界面(场景点选真实路由——宝箱命中区)
main.ui.modals.camp.scene.object_pressed.emit(&"chest")
# oracle: design/gdd/ux/camp.md:18 宝箱命中区→秘宝匣界面入口
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"chest").get_center())
await get_tree().process_frame
if main.ui.modals.camp.treasure._workshop_rows.size() != 12:
problems.append("秘宝匣行数错误(%d ≠ 12——迭代 22 秘宝批换代)〔句柄子容器批 V1〕" % main.ui.modals.camp.treasure._workshop_rows.size())
main.ui.modals.camp.treasure.closed.emit()
ProbeUiUtil.push_click(main, ProbeUiUtil.find_button(main.ui.modals.camp.treasure, "返回营地"))
await get_tree().process_frame
main._on_camp_closed() # 关层收闭(营地与选档层同收+开始层恢复——卡④ T3)
await get_tree().process_frame
@@ -71,6 +76,7 @@ func _ready() -> void:
await get_tree().process_frame
# ---- 入口选路停点(关卡开始即出节点图——level_started 后、首战前弹选路层;
# press_candidate 图上命中路径 pick 入口单候选进首战)----
# oracle: design/gdd/ux/level-map.md:11 入口停点候选数=入口个数(入口单候选断言)
# ---- 独立性断言(独立屏激活期战斗屏宿主整体隐藏,零透出)----
if not main.ui.modals.run.level_screen.visible:
problems.append("关卡开始未弹入口选路停点(level_screen 应可见)")
@@ -86,14 +92,16 @@ func _ready() -> void:
if not main.ui.screens.battle_root.visible or main.ui.modals.run.level_screen.visible:
problems.append("选路提交战斗节点后未切回战斗屏(屏级互斥恢复失守)")
# ---- 放弃入口·冒险模式可见 + 战斗中按下被拒(真实按钮路径——入口挂选路停点,战斗中拒绝为防御分支)----
# oracle: design/gdd/ux/level-map.md:42 放弃冒险(任意选路停点)→ 冒险落幕——战斗域单事务在飞时拒绝
if main.ui.modals.run.level_screen.abandon_btn == null or not main.ui.modals.run.level_screen.abandon_btn.visible:
problems.append("冒险模式放弃按钮未开放(选路停点入口在案)")
else:
main.ui.modals.run.level_screen.abandon_btn.pressed.emit()
ProbeUiUtil.push_click(main, main.ui.modals.run.level_screen.abandon_btn)
await get_tree().process_frame
if main.run_scope.adventure == null or String(main.run_scope.adventure.state) != "running":
problems.append("首战中放弃未被拒(战斗域单事务在飞——state 应保持 running)")
# ---- 既有断言(回归基线;A 区无进度条——节点链进度由关卡画面独立屏承载)----
# oracle: design/gdd/ux/battle.md:122 B 区构建组:主角与敌人站桩、明细带挖掘期收起、轮次指示、棋盘与 C 区/秘宝槽、首战进行中
if main.ui.battle.board_zone.board_view.cells_probe().is_empty():
problems.append("棋盘格未生成(cells=0)")
if main.ui.battle.board_zone.board_view.board() == null:
@@ -110,7 +118,8 @@ func _ready() -> void:
if main.ui.battle.stage.enemy_box.get_child_count() < 1:
problems.append("B 区敌方站桩未构建(%d 个)" % main.ui.battle.stage.enemy_box.get_child_count())
if main.ui.battle.board_zone.ap_bar._dots.get_child_count() != int(main.db.run.action_economy.ap_per_round):
problems.append("D 区 AP 点阵数量错误(%d)" % main.ui.battle.board_zone.ap_bar._dots.get_child_count())
problems.append("D 区 AP 点阵数量错误(%d)" % main.ui.battle.board_zone.ap_bar._dots.get_child_count())
# oracle: design/gdd/systems/排雷循环/行动经济.md:258 开轮硬重置:行动点回到表值(AP 点阵数=ap_per_round)
if main.ui.battle.board_zone.ap_bar.refresh_remaining_text() == "":
problems.append("D 区弃图刷新按钮文案缺失")
if main.ui.battle.board_zone.toast == null:
@@ -127,6 +136,7 @@ func _ready() -> void:
# E 区无道具面板——四区构建由其余断言覆盖
# ---- 敌方站桩投影(顺位徽标/意图三档动作动画占位——敌人系统×4,全站桩)----
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:275 敌人呈现:意图占位三档可见性——普通攻击零表示、类型级提示(站桩投影组)
# B 区站桩子件伴随件化(壳层 tooltip 载体)——断言口径见台账 171:
# 伴随件化(壳层 tooltip 载体),穿透明(子件 _name_label/_armor_icon/_intent_badge)改本体
# 探针出口 stage.enemy_body_at(i).name_line_probe()/intent_probe()——呈现口径逐字保持
@@ -153,6 +163,7 @@ func _ready() -> void:
elif not bool(main.ui.battle.stage.hero_standee.portrait_probe().get("has_tex", false)):
problems.append("主角立绘纹理未落位(显示档 hero=144——显示档批)")
# ---- BUG-0001 回归(tooltip 入树后 set_effects 槽位不错位——结算软锁修复沉淀)----
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:274 状态槽图标+层数+剩余次数、悬停见来源(tooltip 回归组)
var slots_row = main.ui.battle.stage.hero_standee.slot_row # BUG-0001 回归随组件迁入站桩(tooltip/槽位错位同源)
var probe_eff: Array = [{"effect_id": "attack_up", "display_name": "增伤", "layers": 1,
"remaining_settlements": 1, "source_class": "safe_cell", "round_index": 1, "effective_value": 50}]
@@ -178,6 +189,7 @@ func _ready() -> void:
# ---- 图标键回归(HUD框架 图标键契约:状态表九行全映射〔定稿 PNG 纹理〕、
# 格面资源 cell_type 九型全映射〔含 gp 键〕;查不到的键 fail-fast + ? 占位
# ——此处钉死映射不残缺,未知键断言由 _icon_tex/_badge_glyph 承担)----
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:274 极性按键级表呈现——状态图标键映射不残缺(图标键回归组)
for eid in ["attack_up", "ac_up", "alert", "weaken", "enrage", "off_guard", "sapped", "empower", "fortify"]:
if not StatusSlotRow.EFFECT_ICON.has(eid) or StatusSlotRow.EFFECT_ICON[eid] == null:
problems.append("状态图标九键纹理映射缺失(%s)" % eid)
@@ -202,6 +214,7 @@ func _ready() -> void:
# ---- L2 单场段(debug_session 单场原语 + 去饱和接口可用性;雾格机制已整体移除,不再断言变体呈现)----
# 单场无用户入口——探针直调 debug_session 原语(battle_created/ready 仍经组合根接线;
# _reset_run_scope 复现 _start_single 的提示去重复位)
# oracle: design/gdd/systems/排雷循环/地形变异.md:156 布置域:变异格全在初始展开区外(L2 布置域组)
main.begin_session(false)
main._debug.start_single(2)
await get_tree().process_frame
@@ -224,6 +237,7 @@ func _ready() -> void:
# ---- GP 终局行:summary gp_final 键取数+胜利面板 GP 行呈现(C5:GP 行只上冒险终局面板;
# 断言面 = summary 键,无影子账清零转存)----
# (置于战斗态断言之后——合成收束会灰化站桩并清 settlement 引用,具污染性)
# oracle: design/gdd/ux/settlement-summary.md:20 战利品行=GP 增量+总量(冒险终局面板 GP 行组)
var meta_saved = main.meta.save
for p2 in ["user://smoke_ui_probe_settle_scratch.json", "user://smoke_ui_probe_settle_scratch.json.tmp"]:
if FileAccess.file_exists(p2):
+32 -7
View File
@@ -68,7 +68,7 @@ func _ready() -> void:
problems.append("[camp] 场景子节点序≠layout 表 z 序(逐层相等失守)〔营地图批 V1〕")
if not rect_ok:
problems.append("[camp] 层贴图矩形≠layout 表(逐值一致失守)〔营地图批 V1〕")
main.ui.modals.camp.scene.object_pressed.emit(&"forge")
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"forge").get_center())
_settle()
if not main.ui.modals.camp.forge.visible:
problems.append("[camp] 点选锻炉→锻造子界面未开(场景点选路由中断)〔营地图批 V2〕")
@@ -81,7 +81,7 @@ func _ready() -> void:
scene_buttons += 1
if scene_buttons != 0:
problems.append("[camp] 场景根态存在文字按钮(反参照纪律——禁文字按钮列表形态)〔营地图批 V9〕")
main.ui.modals.camp.forge.closed.emit()
ProbeUiUtil.push_click(main, ProbeUiUtil.find_button(main.ui.modals.camp.forge, "返回营地"))
_settle()
main._on_camp_closed() # 关层收闭(营地与选档层同收+开始层恢复——卡④ T3)
_settle()
@@ -89,10 +89,11 @@ func _ready() -> void:
# ---- 生涯页:入口=帐篷旁笔记本(卡④ T3 改挂——场景点选信号真实路由→组合根 _open_career)→
# 结构断言族→只读巡检(契约 3)+零动效契约+零跳转契约(提纲 P4 甲)→组合根真实路径关闭。
#(登记序/树序契约统一在全模态 _modal_registry_contract——boot 段跑)----
# oracle: design/gdd/ux/camp.md:19 帐篷旁笔记本→生涯记录;生涯页返回钮口径由结构校验兜底(生涯段)
# 营地语境:开始层全程隐藏(笔记本入口在营地场景根——关生涯回营地不回开始层,卡④ T3 语境分支)
main._camp_pres.open_camp() # 重开营地场景根(笔记本入口语境)
_settle()
main.ui.modals.camp.scene.object_pressed.emit(&"notebook")
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"notebook").get_center())
_settle()
if not main.ui.modals.menu.career_overlay.visible or main.ui.screens.start_overlay.visible:
problems.append("[career] 生涯页未打开或开始层未隐藏(笔记本改挂路径断裂)〔营地图批 V5〕")
@@ -105,11 +106,26 @@ func _ready() -> void:
if main.ui.modals.menu.career_overlay.visible or main.ui.screens.start_overlay.visible:
problems.append("[career] 关闭后页未隐藏或开始层回弹(营地语境分支失守)〔营地图批 V5〕")
# ---- Esc 浮窗结构段(卡⑤——三钮在位且仅三+零动效契约+登记序在册+debug_badge 不波及)----
# oracle: design/gdd/ux/camp.md:45 ESC 优先序:有窗先关最上层窗、无窗才进浮窗;各窗自带关闭钮(Esc 段)
main._camp_pres.open_camp()
_settle()
main.ui.modals.camp.scene.object_pressed.emit(&"gate")
var gpos_gate: Vector2 = main.ui.modals.camp.scene.get_hotspot_rect(&"gate").get_center()
var gdown := InputEventMouseButton.new()
gdown.button_index = MOUSE_BUTTON_LEFT
gdown.pressed = true
gdown.position = gpos_gate
gdown.global_position = gpos_gate
main.get_viewport().push_input(gdown, true) # 按下在 gate 命中区
await get_tree().process_frame
await get_tree().process_frame
var gup := InputEventMouseButton.new()
gup.button_index = MOUSE_BUTTON_LEFT
gup.pressed = false
gup.position = Vector2(2, 2) # 抬杆落点避开弹窗钮域——场景命中沿=抬杆,弹窗随抬杆同帧开;抬杆落原位会经事件传播误触确认钮(实证:冒险误启)。抬杆离场=不经历弹窗开关,段断言(Esc 路由)在净态成立
gup.global_position = Vector2(2, 2)
main.get_viewport().push_input(gup, true)
_settle()
main.ui.modals.menu.confirm_dialog.cancelled.emit() # 关大门弹窗(清场——浮窗段前提)
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._cancel_btn) # 关大门弹窗(清场——浮窗段前提;无弹窗态=无害空点)
_settle()
var push_esc := func() -> void:
var ev := InputEventKey.new()
@@ -197,6 +213,7 @@ func _ready() -> void:
_settle()
# ---- 冒险簇(真流推进:开冒险→地图选路态→选战斗节点→战斗 HUD)----
# oracle: design/gdd/ux/level-map.md:3 图上点击选路·候选按钮退役(冒险簇选路真点组)
ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_adv", true, false, true)
main.ui.modals.menu.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,结构采样确定性禁依赖默认分支)
main._start_adventure_from_menu()
@@ -205,7 +222,7 @@ func _ready() -> void:
var battle_id := _pick_battle_node(main)
if battle_id >= 0:
main.ui.modals.run.level_screen.route_chosen.emit(battle_id)
main.ui.modals.run.level_screen.route_chosen.emit(battle_id) # 前提注入(保留 emit):本段被测面=战斗 HUD 结构审计,选路机制非其判据面——图上命中真入口由 ui_input_probe case⑫ 承载。附发现呈报:emit 路径下审计采样时 battle_root 尚未显示(审计长期空转采样);真点路径采样变实后暴露停手(64)/图例(56)同容器宽度不等——两项均待裁(收尾呈报)
_settle(6)
_audit("battle_hud", [main.ui.battle.board_zone, main.ui.battle.info_panel, main.ui.battle.stage])
_standeefit_contract(main)
@@ -226,7 +243,8 @@ func _ready() -> void:
# ---- 图例层(旧模态、首过结构族):ap_bar legend_toggled
# 信号真路径开(board_zone 信号→组合根 _on_legend_toggled→_open_modal)→结构断言族→
# closed 信号真路径关(close()→组合根 _on_modal_closed)。
main.ui.battle.board_zone.legend_toggled.emit()
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:88 图例面板=模态族:统一登记序栈、入栈出栈、关闭逐层恢复(图例组)
main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit() # 前提注入(保留 emit):emit 选路路径下 battle_root 未及显示且输入锁在(实证在案),真点不可达——图例开启的真输入承载随「route 信号路径屏幕切换时序」发现一并呈报待裁
_settle()
if not main.ui.modals.run.legend_overlay.visible:
problems.append("[legend] 图例层未打开(legend_toggled 路径中断)")
@@ -388,6 +406,7 @@ const _PAUSE_READONLY_SIGNALS: Array[String] = [
func _status_readonly_contract(main: Node) -> void:
# oracle: design/engineering/code-conventions.md:21 视图零核心写调用、依赖单向(status 只读巡检组)
var overlay: Control = main.ui.modals.run.status_panel
var stack: Array = [overlay]
var conn_count := 0
@@ -434,6 +453,7 @@ func _status_zero_motion_contract(main: Node) -> void:
# 清单外零连接。白名单={视图自身 _emit_closed〔返回钮〕, 组合根 _on_career_closed};页内任何
# 购买/导航/写入方向的新连接一律红,须先入册审查。
func _career_readonly_contract(main: Node) -> void:
# oracle: design/engineering/code-conventions.md:21 视图零核心写调用、依赖单向(career 只读巡检组)
var overlay: Control = main.ui.modals.menu.career_overlay
var stack: Array = [overlay]
var conn_count := 0
@@ -480,6 +500,7 @@ func _career_zero_motion_contract(main: Node) -> void:
# _career_registry_contract 的断言(career=状态面板后/确认弹窗前、树序同步)由本全量合同覆盖)
# ⑭选档界面契约(卡③ sl1/sl2/sl3/sl4/sl9——呈现面全部来自喂入数据,视图零自判)
func _save_select_contract(main: Node, tpl: String) -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:61 三存档位独立文件、激活态由选档界面建立(选档契约组)
var sso: Node = main.ui.modals.menu.save_select_overlay
# sl1 三槽呈现:槽卡计数==3+零滚动容器(禁滚动长列表形态——契约 1)
var rows: VBoxContainer = sso._rows_box
@@ -559,6 +580,7 @@ func _collect_labels(node: Node, out: Array[String]) -> void:
func _modal_registry_contract(main: Node) -> void:
# oracle: design/engineering/code-conventions.md:54 模态覆盖层层级规则——新增覆盖层必须登记 z 序位置(登记序契约组)
var list: Array = main.ui.flow.stack._list
# ① 登记册序==合同模态序(逐位点名——合同是登记序唯一事实源)
var contract_modals: Array = []
@@ -626,6 +648,7 @@ func _modal_registry_contract(main: Node) -> void:
# ⑬career 零跳转契约(提纲 P4 甲——页内零「前往他处」入口:除「返回」外任何导航钮=红)
func _career_nojump_contract(main: Node) -> void:
# oracle: design/gdd/ux/camp.md:19 生涯记录入口与返回钮口径由结构校验兜底——页内零导航入口(零跳转契约组)
for nav in ["营地", "商店", "设置", "冒险", "继续上局", "退出", "前往"]:
if ProbeUiUtil.find_button(main.ui.modals.menu.career_overlay, nav, "contains") != null:
problems.append("[career][nojump] 页内出现导航入口「%s」(零跳转违约——生涯纯只读)" % nav)
@@ -635,6 +658,7 @@ func _career_nojump_contract(main: Node) -> void:
# (名行基线离盒底 1px 同型高危写法返修回归闸;
# 内容矩形=StandeeBody.content_rect(),最低绘制元素=底座下缘)
func _standeefit_contract(main: Node) -> void:
# oracle: design/gdd/ux/battle.md:154 主角站桩立绘(无框立绘+落影)——站桩内容矩形⊆舞台(站桩适配组)
var st: Node = main.ui.battle.stage
if st == null or st.hero_standee == null:
problems.append("[standee] 舞台/主角站桩缺失(断言中断)")
@@ -719,6 +743,7 @@ func _in_scroll(c: Control) -> bool:
func _audit(screen: String, roots: Array) -> void:
# oracle: design/engineering/architecture/architecture.md:168 组合根交接面与呈现装配——屏结构审计宿主面(结构审计组)
for r in roots:
if r != null and is_instance_valid(r):
_force_layout(r)
+8 -2
View File
@@ -43,7 +43,8 @@ func _ready() -> void:
# ① 全局浮层角标开窗(调试入口=UI 最顶层浮层,任何 UI 不遮挡——
# main 直接子层、LAYER_CONTRACT 末位 float 类;boot 菜单优先不开浮窗,角标真实点击路径开窗;
# 悬浮于菜单之上——开始层不隐藏;树序新基准=登记册末位(确认弹窗之后=绘制序最上))
main.ui.screens.debug_badge.pressed.emit()
# oracle: design/engineering/test-conventions.md:49 调试面=DebugOverlay/DS_DEBUG——发布态恒隐无 UI 开启口、开发期临时启用(①开窗层级组)
main._show_debug_overlay() # 发布态角标恒隐(main.gd:138 _debug_entry 缺省 false)——角标信号路径已退役(:123 同款先例),开窗经组合根直调承载
await get_tree().process_frame
if not main.ui.modals.menu.debug_overlay.visible:
problems.append("调试浮层角标点击后浮窗未开(全局浮层入口失守)")
@@ -56,12 +57,14 @@ func _ready() -> void:
if str(boosted_atk) not in main.ui.modals.menu.debug_overlay.effect_text() or str(boosted_hp) not in main.ui.modals.menu.debug_overlay.effect_text():
problems.append("生效预览未联动(%s)" % main.ui.modals.menu.debug_overlay.effect_text())
# ③ 应用(真实信号路径:apply_requested → 组合根)——E4 后加成零表写,生效断言移至 ⑧ 战斗快照
# oracle: design/engineering/test-conventions.md:49 调试面加成链路:滑条联动/应用/重置、hero 表运行期只读(②③④⑦组)
main.ui.modals.menu.debug_overlay.press_apply_for_probe()
# ④ 重置(重开浮窗 → 重置按钮真实路径;面板百分比归零——下一场战斗生效值回到基线)
main._show_debug_overlay()
main.ui.modals.menu.debug_overlay.press_reset_for_probe()
# ⑤ 批量模拟全链路(N=10——走 level_autoplay 与 headless 扫描同口径;表基线由组合根复位;
# 菜单态上下文未锁定=按钮可点,is_running 语义=模拟在跑〔18卡⑦ 与对局禁用态拆分〕)
# oracle: design/engineering/test-conventions.md:49 调试面批量模拟链路:N=10 输出关卡完成率/剩余 HP/败北节点(⑤组)
main.ui.modals.menu.debug_overlay.set_sliders(100, 0)
main.ui.modals.menu.debug_overlay.set_sim_count(10)
main.ui.modals.menu.debug_overlay.press_simulate_for_probe()
@@ -116,6 +119,7 @@ func _ready() -> void:
# ⑨ 对局中可开(18卡⑦ P2 甲/T2 防护——S1+ 停机点回归闸):战斗屏角标真实信号路径开窗;
# 棋盘输入不锁、状态角标不置灰(「默认开×遮挡复活」下必锁死的破约回归面);
# 对局中批量模拟/两层调试两钮禁用;当局数据六项与只读访问器逐项一致;关闭钮真实路径收窗恢复
# oracle: design/engineering/test-conventions.md:49 调试面对局态:不锁棋盘输入/两钮禁用/当局数据只读一致(⑨组)
var dbg_badge: Button = main.ui.screens.debug_badge
if dbg_badge == null or dbg_badge.visible:
problems.append("对局中调试浮层角标非恒隐(发布态语义失守——卡⑦ 删写死行批)")
@@ -150,7 +154,7 @@ func _ready() -> void:
if close_btn == null:
problems.append("浮窗关闭钮缺失(真实收窗路径中断)")
else:
close_btn.pressed.emit()
ProbeUiUtil.push_click(main, close_btn)
if main.ui.modals.menu.debug_overlay.visible or main.ui.flow.locks_input():
problems.append("对局中关闭浮窗后态失守(visible=%s locked=%s)"
% [str(main.ui.modals.menu.debug_overlay.visible), str(main.ui.flow.locks_input())])
@@ -159,6 +163,7 @@ func _ready() -> void:
_overlay_zero_motion_contract(main, problems)
# ⑩ 种子三分单点断言(18卡⑦ C1——判定只落 _seed_value() 单点;对局态不影响本判定面):
# 显式合法整数全场景优先(含首局)→ 首局(存档布尔 false)恒 42 → 非首局空框引擎熵连抽不全等 42
# oracle: design/engineering/architecture/architecture.md:168 seed_value=组合根公开服务面(⑩种子三分判定单点组)
main.ui.modals.menu.debug_overlay.seed_edit.text = "777"
if main.seed_value() != 777:
problems.append("显式种子输入未优先(_seed_value != 777——C1 ①失守)")
@@ -199,6 +204,7 @@ func _ready() -> void:
# 全连接巡检——白名单={视图自身八方法 + 注册面模态编排口 ui.flow.close(closed 外发消费——
# 注册面批把该连接目标从组合根转发壳改为 flow 真方法,机检白名单随批登记同一连接)};
# 当局数据区块=纯标签零连接在此被一并约束(新增任何连接=红须入册审查)。
# oracle: design/engineering/code-conventions.md:21 视图零核心写调用、依赖单向(白名单外零连接巡检组)
func _overlay_readonly_contract(main: Node, problems: Array) -> void:
var overlay: Control = main.ui.modals.menu.debug_overlay
var stack: Array = [overlay]
+587
View File
@@ -0,0 +1,587 @@
# journey_player.gd — 全动线机器自玩驱动(C·发现环节去人化)
# 是什么:headless 场景驱动件——实例化 main、隔离存档,按旅程段驱动全动线(开机→设置→存档三操作
# 新档/删档/进档→营地功能窗逐窗开合→Esc 回退→大门开冒险→关卡推进→战斗→各停点→终局收束→回菜单),
# 每屏到达记账+段间异常上报,收尾落 JSONP 报告并入探针覆盖登记(真输入事件同源记账)。
# 判据(契约 §5):每个屏到达过+段间零 fatal。
# 异常上报三类(其余静默):①诊断收集器 fatal 级(settlement battle_result=fatal_error/fatal 屏)
# ②段间超时(单段 >30s 未达预期停点;战斗段另设状态无变化挂死阈值)③屏状态与预期不符。
# RID 泄漏等已知日志噪音静默。
# 归因字段:每条上报机器自填——段名/期望屏/实测屏/上一动作/通道标签(真实输入-按钮/
# 真实输入-场景热点/真实输入-键盘/方法-地图命中口/方法-棋盘口)。原归因制度残骸=本结构内字段。
# 误报收敛:误报指纹(类+段+归因摘要)落 .gates/coverage/journey_misfire_ledger.json——同一指纹
# 第 2 次起自动登记静默(计数照记、不再进上报清单);连 3 批零真缺陷判据收紧归用户复核轮。
# 驱动纪律:按钮/场景物件全走真输入(push_input 管线,同探针判据——ProbeUiUtil 单点通道)。
# 两处方法口如实标注通道:地图候选=press_candidate(自绘布局命中路径,探针族既定入口);
# 棋盘格=click_cell(BoardView 公共点击口,ui_input_probe 先例——棋盘管线面由该探针承载)。
# 停点分发的视图内部读数(_continue_btn/_node_pending 等)沿用探针族直读受控例外(iter53 反哺点 9 同款在册)。
# 运行:Godot_console.exe --headless --path . res://sim/journey_player.tscn
extends Node
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
const ProbeCoverageRecorder := preload("res://probe/probe_coverage_recorder.gd")
const AutoPlayer := preload("res://sim/auto_player.gd")
const SEGMENT_TIMEOUT_MS := 30000 # 段间超时判据(契约 §5——单段 30s 未达预期停点)
const BATTLE_STALL_FRAMES := 90 # 战斗段状态无变化挂死阈值(演播期豁免同探针口径)
const ADVENTURE_BUDGET_FRAMES := 60000 # 冒险段帧预算(全停点循环兜底)
const MISFIRE_PATH := "res://tests/.gates/coverage/journey_misfire_ledger.json"
const REPORT_PATH := "res://tests/.gates/coverage/journey_report.js"
var main: Node
var _recorder: RefCounted = null
var _problems: Array = [] # 异常上报(三类;指纹二次起静默不入此列)
var _silent_count := 0 # 误报静默笔数(回执可见)
var _screens_seen: Dictionary = {} # 屏到达记账(判据面)
var _screen_trace: Array = [] # 屏到达变迁轨迹(诊断面——新屏首现时点+上一动作)
var _misfire: Dictionary = {} # 指纹 → 次数(跨跑落盘)
var _last_state := {} # 战斗段状态哈希(挂死检测)
var _stall := 0
var _last_action := "(无)" # 归因初判:上一动作描述
var _map_status_done := false # 地图屏状态角标一次性
var _legend_done := false # 图例开合一次性(每战斗面一次)
var _status_done := false # 战斗屏状态面板一次性
var _refresh_done := false # 刷新确认链一次性
func _ready() -> void:
await get_tree().process_frame
main = load("res://presentation/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
await get_tree().process_frame
_load_misfire()
_recorder = ProbeCoverageRecorder.new()
_recorder.attach(main, "journey")
ProbeUiUtil.isolate_save(main, "journey_player", false, false, true)
main.save_manager.slots.attach_slot_template("user://journey_player_slot_%d.json")
for i in range(1, 4): # 夹具清场——陈旧槽文件会翻有档态、新档钮缺席(open_camp_via_savesel 同款坑注)
for suf in ["", ".tmp", ".corrupt.bak"]:
var sp: String = main.save_manager.slots.slot_path(i) + suf
if FileAccess.file_exists(sp):
DirAccess.remove_absolute(sp)
main.ui.modals.menu.debug_overlay.set_sliders(400, 200) # 高加成保 L1 可完成(探针先例——夹具注)
main.ui.modals.menu.debug_overlay.seed_edit.text = "42"
await _phase_expect("开机落菜单", ["菜单"])
await _phase_menu_settings()
await _phase_new_slot(1)
await _phase_camp_windows()
await _phase_camp_esc_return()
await _phase_new_and_delete(2)
await _phase_load_slot(1)
await _phase_gate_start()
await _phase_adventure()
await _phase_end_back_menu()
_recorder.dump_js()
_dump_report()
_save_misfire()
var fatal := _fatal_count()
if _problems.is_empty() and fatal == 0 and _screens_seen.has("菜单") and _screens_seen.has("终局"):
print("[journey] PASS — 全动线走查完成(屏到达 %d 类,异常上报 0,fatal 0,误报静默 %d)" % [
_screens_seen.size(), _silent_count])
get_tree().quit(0)
return
print("[journey] FAIL — 异常 %d(静默 %d)/ fatal %d / 屏到达 %d 类" % [
_problems.size(), _silent_count, fatal, _screens_seen.size()])
for p in _problems:
print("[journey] - " + JSON.stringify(p, " "))
get_tree().quit(1)
# ---- 屏状态读取与记账 ----
func _screen_state() -> Dictionary:
var st := {
"菜单": main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible,
"选档": main.ui.modals.menu.save_select_overlay.visible,
"设置": main.ui.modals.menu.settings_overlay.visible,
"生涯": main.ui.modals.menu.career_overlay.visible,
"确认弹窗": main.ui.modals.menu.confirm_dialog.is_open(),
"营地": main.ui.modals.camp.scene.visible,
"锻造窗": main.ui.modals.camp.forge.visible,
"树窗": main.ui.modals.camp.tree.visible,
"秘宝窗": main.ui.modals.camp.treasure.visible,
"图鉴窗": main.ui.modals.camp.codex.visible,
"Esc浮窗": main.ui.modals.camp.esc_overlay.visible,
"冒险": main.run_scope != null and main.run_scope.is_adventure,
"关卡屏": main.ui.modals.run.level_screen != null and main.ui.modals.run.level_screen.visible,
"三选一": main.ui.modals.run.curio_overlay.visible,
"商店": main.ui.modals.menu.shop_overlay.visible,
"大奖励": main.ui.screens.inter_overlay != null and main.ui.screens.inter_overlay.visible,
"节点": main.ui.modals.run.node_overlay.visible,
"终局": main.ui.modals.run.end_overlay != null and main.ui.modals.run.end_overlay.is_open(),
}
var in_battle: bool = bool(st["冒险"]) and not bool(st["关卡屏"]) and not bool(st["终局"]) \
and main.run_scope != null and main.run_scope.settlement != null \
and String(main.run_scope.settlement.battle_result) == ""
st["战斗"] = in_battle
return st
func _record_screens() -> void:
var st := _screen_state()
for k in st:
if bool(st[k]) and not _screens_seen.has(k):
_screens_seen[k] = true
_screen_trace.append({"t": Time.get_datetime_string_from_system(), "screen": k, "last_action": _last_action})
_check_exclusion(st)
var _excl_reported: Dictionary = {} # 屏互斥违约对(本跑一次性上报——跨跑由误报指纹静默)
# 屏级互斥(探针同款判据对):合法叠加仅「终局盖冒险」;违约=③屏状态与预期不符
func _check_exclusion(st: Dictionary) -> void:
var pairs := [["菜单", "营地"], ["菜单", "冒险"], ["营地", "冒险"], ["终局", "营地"], ["终局", "菜单"], ["菜单", "战斗"]]
for pair in pairs:
if bool(st.get(pair[0], false)) and bool(st.get(pair[1], false)):
var fk: String = str(pair[0]) + "×" + str(pair[1])
if not _excl_reported.has(fk):
_excl_reported[fk] = true
_report("screen_mismatch", "屏级互斥", "%s 与 %s 不应同场" % [pair[0], pair[1]])
func _active_screens() -> Array:
var on: Array = []
var st := _screen_state()
for k in st:
if bool(st[k]):
on.append(k)
return on
func _fatal_count() -> int:
var n := 0
if main.run_scope != null and main.run_scope.settlement != null \
and String(main.run_scope.settlement.battle_result) == "fatal_error":
n += 1
return n
# ---- 异常上报(三类+归因+误报收敛) ----
func _report(kind: String, seg: String, expect: String) -> void:
var finger_src := "%s|%s|%s|%s" % [kind, seg, expect, ",".join(_active_screens())]
var finger := str(finger_src.hash())
_misfire[finger] = int(_misfire.get(finger, 0)) + 1
if int(_misfire[finger]) >= 2:
_silent_count += 1 # 同一误报源第 2 次自动登记静默(契约 §5 收敛口径)
return
_problems.append({
"kind": kind, "segment": seg, "expect": expect,
"actual_screens": _active_screens(), "last_action": _last_action,
"fatal": _fatal_count(), "at": Time.get_datetime_string_from_system(),
})
func _load_misfire() -> void:
if FileAccess.file_exists(MISFIRE_PATH):
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(MISFIRE_PATH))
if parsed is Dictionary:
_misfire = parsed
func _save_misfire() -> void:
var f := FileAccess.open(MISFIRE_PATH, FileAccess.WRITE)
if f != null:
f.store_string(JSON.stringify(_misfire, " "))
f.close()
# ---- 段间等待(超时判据承载) ----
func _frames(n: int = 2) -> void:
for _i in n:
await get_tree().process_frame
_record_screens()
# 期望屏集合在超时内逐帧到达;到达即记账返回。超时=异常②超时+③屏状态不符双记(同源指纹静默收敛)。
func _phase_expect(seg: String, expect_keys: Array) -> void:
_last_action = "等待段「%s」落点(期望 %s)" % [seg, ",".join(expect_keys)]
var t0 := Time.get_ticks_msec()
while Time.get_ticks_msec() - t0 < SEGMENT_TIMEOUT_MS:
await get_tree().process_frame
_record_screens()
var st := _screen_state()
var all_on := true
for k in expect_keys:
all_on = all_on and bool(st.get(k, false))
if all_on:
await _frames(2) # 开层排版帧——当帧可见时容器未排,紧随的控件真点击会落空(探针件同款坑)
return
_report("timeout", seg, "屏 %s 应在场" % ",".join(expect_keys))
_report("screen_mismatch", seg, "屏 %s 应在场" % ",".join(expect_keys))
_save_misfire()
# ---- 旅程段 ----
func _phase_menu_settings() -> void:
# 设置开合(开始层真实入口——功能界面全覆盖义务)
var btn: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "设置", "prefix")
if btn == null:
_report("screen_mismatch", "设置入口", "开始层「设置」钮在场")
return
_last_action = "真点「设置」钮"
ProbeUiUtil.push_click(main, btn)
await _phase_expect("设置开", ["设置"])
var back: Button = ProbeUiUtil.find_button(main.ui.modals.menu.settings_overlay, "返回", "prefix")
if back != null:
_last_action = "真点设置「返回」钮"
ProbeUiUtil.push_click(main, back)
await _phase_expect("设置收", ["菜单"])
func _phase_new_slot(slot: int) -> void:
# 开机→存档·新档(选档层真实入口:进入存档→槽位新档→落营地)
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
if esb == null:
_report("screen_mismatch", "选档入口", "开始层「进入存档」钮在场")
return
_last_action = "真点「进入存档」钮"
ProbeUiUtil.push_click(main, esb)
await _phase_expect("选档层开", ["选档"])
var nb: Button = main.ui.modals.menu.save_select_overlay._new_btns.get(slot)
if nb == null:
_report("screen_mismatch", "新档槽%d" % slot, "槽%d 新档钮在场" % slot)
return
_last_action = "真点槽%d「新档」钮" % slot
ProbeUiUtil.push_click(main, nb)
await _phase_expect("新档落营地", ["营地"])
func _phase_load_slot(slot: int) -> void:
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
if esb == null:
_report("screen_mismatch", "选档入口", "开始层「进入存档」钮在场")
return
_last_action = "真点「进入存档」钮(进档链)"
ProbeUiUtil.push_click(main, esb)
await _phase_expect("选档层开", ["选档"])
var lb: Button = main.ui.modals.menu.save_select_overlay._load_btns.get(slot)
if lb == null:
_report("screen_mismatch", "进档槽%d" % slot, "槽%d 进档钮在场" % slot)
return
_last_action = "真点槽%d「进档」钮" % slot
ProbeUiUtil.push_click(main, lb)
await _phase_expect("进档落营地", ["营地"])
func _phase_new_and_delete(slot: int) -> void:
# 新档→篝火回菜单→选档删档(二次确认双路径真点击)——存档三操作收口
await _phase_new_slot(slot)
var camp: Control = main.ui.modals.camp.scene
_last_action = "真点篝火热点(回退动线)"
ProbeUiUtil.push_click_at(main, camp.get_hotspot_rect(&"campfire").get_center())
await _phase_expect("篝火确认弹窗", ["确认弹窗"])
var ok: Button = main.ui.modals.menu.confirm_dialog._confirm_btn
if ok != null:
_last_action = "真点篝火确认「确认」钮"
ProbeUiUtil.push_click(main, ok)
await _phase_expect("回菜单", ["菜单"])
# 删档:进选档→槽位删档一击开窗→真点取消→再删→真点确认(文件三态走查归探针既有套件——C 只验到达)
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
if esb != null:
_last_action = "真点「进入存档」钮(删档链)"
ProbeUiUtil.push_click(main, esb)
await _phase_expect("选档层开", ["选档"])
var db: Button = main.ui.modals.menu.save_select_overlay._delete_btns.get(slot)
if db != null:
_last_action = "真点槽%d「删档」钮一击" % slot
ProbeUiUtil.push_click(main, db)
await _phase_expect("删档确认窗", ["确认弹窗"])
var cc: Button = main.ui.modals.menu.confirm_dialog._cancel_btn
if cc != null:
_last_action = "真点删档「取消」钮"
ProbeUiUtil.push_click(main, cc)
await _frames(2)
_last_action = "真点槽%d「删档」钮二击" % slot
ProbeUiUtil.push_click(main, db)
await _phase_expect("删档确认窗", ["确认弹窗"])
var ok2: Button = main.ui.modals.menu.confirm_dialog._confirm_btn
if ok2 != null:
_last_action = "真点删档「确认」钮"
ProbeUiUtil.push_click(main, ok2)
await _frames(3)
var back: Button = ProbeUiUtil.find_button(main.ui.modals.menu.save_select_overlay, "返回", "prefix")
if back != null:
_last_action = "真点选档「返回」钮"
ProbeUiUtil.push_click(main, back)
await _phase_expect("选档收", ["菜单"])
func _phase_camp_windows() -> void:
# 营地功能窗逐窗真点开合(锻造/秘宝/树/图鉴/生涯——campfire/gate 归动线段)
var views := {
&"forge": ["锻造窗", main.ui.modals.camp.forge],
&"chest": ["秘宝窗", main.ui.modals.camp.treasure],
&"dummy": ["树窗", main.ui.modals.camp.tree],
&"board": ["图鉴窗", main.ui.modals.camp.codex],
&"notebook": ["生涯", main.ui.modals.menu.career_overlay],
}
for key: StringName in [&"forge", &"chest", &"dummy", &"board", &"notebook"]:
var camp: Control = main.ui.modals.camp.scene
_last_action = "真点营地物件热点 %s" % String(key)
ProbeUiUtil.push_click_at(main, camp.get_hotspot_rect(key).get_center())
await _phase_expect("%s窗开" % String(key), [views[key][0]])
var back: Button = ProbeUiUtil.find_button(views[key][1], "返回营地")
if back == null:
back = ProbeUiUtil.find_button(views[key][1], "返回", "prefix")
if back != null:
_last_action = "真点 %s 窗返回钮" % String(key)
ProbeUiUtil.push_click(main, back)
await _frames(3)
func _phase_camp_esc_return() -> void:
# 营地 Esc 浮窗→「返回主菜单」真点(回退动线+浮窗功能面到达)
_push_esc()
await _phase_expect("Esc浮窗开", ["Esc浮窗"])
var rb: Button = main.ui.modals.camp.esc_overlay._return_btn
if rb != null:
_last_action = "真点浮窗「返回主菜单」钮"
ProbeUiUtil.push_click(main, rb)
await _phase_expect("浮窗回菜单", ["菜单"])
func _phase_gate_start() -> void:
# 大门确认→开冒险(动线收口——确认后落点=冒险,非回菜单)
var camp: Control = main.ui.modals.camp.scene
_last_action = "真点大门热点"
ProbeUiUtil.push_click_at(main, camp.get_hotspot_rect(&"gate").get_center())
await _phase_expect("大门确认弹窗", ["确认弹窗"])
var ok: Button = main.ui.modals.menu.confirm_dialog._confirm_btn
if ok != null:
_last_action = "真点大门「确认」钮"
ProbeUiUtil.push_click(main, ok)
await _phase_expect("冒险发起", ["冒险"])
# 已知缺陷现场(发现环义务已尽):大门开局后开始层滞留盖屏=「开局被开始层盖住」
# (营地交互修复批承接中、合回前 master 在案)——开始层可见直入 locks_input 谓词,战斗全程锁死。
# C 上报后以夹具面隐藏开始层继续走查(通道=夹具修整,非真输入;缺陷本体不因此掩盖)。
await _frames(2)
if main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible:
_report("screen_mismatch", "大门开局", "开始层滞留盖屏(已知缺陷——营地交互修复批承接中)")
_last_action = "夹具修整:隐藏滞留开始层以解锁战斗走查(缺陷已上报在案)"
main.ui.screens.start_overlay.visible = false
await _frames(2)
func _phase_adventure() -> void:
# 关卡推进段:入口选路→战斗→各停点分发→真终局收口(帧预算兜底)
var budget := ADVENTURE_BUDGET_FRAMES
while budget > 0:
budget -= 1
await get_tree().process_frame
_record_screens()
var st := _screen_state()
if bool(st["节点"]):
_last_action = "节点层「继续」(方法-视图口)"
main.ui.modals.run.node_overlay.press_continue()
continue
if bool(st["三选一"]):
_drive_curio()
continue
if bool(st["商店"]):
var lv: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店")
if lv != null:
_last_action = "真点「离开商店」钮"
ProbeUiUtil.push_click(main, lv)
continue
if bool(st["大奖励"]):
var nb: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix")
if nb != null:
_last_action = "真点大奖励「进入下一层」钮"
ProbeUiUtil.push_click(main, nb)
await _frames(2)
continue
if bool(st["关卡屏"]):
_drive_map_once()
continue
if bool(st["战斗"]):
await _drive_battle_once()
continue
var endo: Control = main.ui.modals.run.end_overlay
if endo != null and endo.is_open():
var armed: bool = main.run_scope.settlement_stop_timer.is_armed()
var running: bool = main.run_scope.is_adventure and main.run_scope.adventure != null \
and String(main.run_scope.adventure.state) == "running"
if armed or running:
_last_action = "真点结算面「继续/下一节点」钮"
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn)
await _frames(2)
continue
break # 真终局(失败/放弃/打穿)——收口段交棒
if budget <= 0:
_last_action = "冒险段超时(诊断:%s)" % JSON.stringify({
"pacer_pending": main.ui.screens.row_pacer.pending_rows().size(),
"locked": main.ui.flow.locks_input(),
"battle_result": String(main.run_scope.settlement.battle_result) if main.run_scope.settlement != null else "?",
"level": int(main.run_scope.runner.level_row.get("level_index", -1)) if main.run_scope.runner != null else -1,
"round": int(main.run_scope.settlement.round_index) if main.run_scope.settlement != null else -1,
"ap": int(main.run_scope.settlement.action_economy.ap) if main.run_scope.settlement != null else -1,
})
_report("timeout", "冒险段", "帧预算耗尽未达真终局")
_save_misfire()
func _drive_map_once() -> void:
# 地图屏:状态角标真点开合一次+候选命中(方法-地图命中口——自绘布局路径,探针族同款入口)
if not _map_status_done and main.ui.modals.run.level_screen.status_badge != null \
and not main.ui.modals.run.level_screen.status_badge.disabled:
_map_status_done = true
var badge: Button = main.ui.modals.run.level_screen.status_badge
_last_action = "真点地图屏状态角标"
ProbeUiUtil.push_click(main, badge)
await _frames(3) # 面板开层排版帧——同帧点「继续」会落空、输入锁滞留全战役(实证)
var resumed: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix")
if resumed != null and main.ui.modals.run.status_panel.visible:
ProbeUiUtil.push_click(main, resumed)
_last_action = "地图候选命中(方法-地图命中口)"
main.ui.modals.run.level_screen.press_candidate(0)
func _drive_curio() -> void:
# 三选一停点:待选态点首件+确认;兑付/已消费态点继续(方法-视图口——curio 停点既定驱动面)
var co: Control = main.ui.modals.run.curio_overlay
_last_action = "三选一停点分发(方法-视图口)"
if co._continue_btn.visible:
co.press_continue()
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty():
co.press_card(0)
co.press_confirm()
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
co.press_card(0)
co.press_confirm()
else:
co.press_continue()
func _drive_battle_once() -> void:
# 战斗单步:挂死检测+功能钮真点(图例/状态/刷新确认链/停手)+棋盘挖格(方法-棋盘口)
var settlement = main.run_scope.settlement
var diag: Dictionary = settlement.diagnostic_snapshot()
var st := {
"round": diag.round, "phase": diag.action_economy.phase, "ap": diag.action_economy.ap,
"revealed": diag.minesweeper_revealed,
}
var pacer_idle: bool = main.ui.screens.row_pacer.pending_rows().is_empty()
if JSON.stringify(st) == JSON.stringify(_last_state) and pacer_idle \
and not main.ui.flow.locks_input():
_stall += 1
if _stall >= BATTLE_STALL_FRAMES:
_report("timeout", "战斗段", "状态无变化达 %d 帧" % BATTLE_STALL_FRAMES)
_stall = 0
_save_misfire()
_last_state = {} # 重置基线防同帧连报
else:
_stall = 0
_last_state = st
if not pacer_idle or main.ui.flow.locks_input():
return
var ap_bar: Control = main.ui.battle.board_zone.ap_bar
# 功能钮真点(一次性——图例开合/状态面板开合/刷新确认链)
if not _legend_done:
_legend_done = true
_last_action = "真点图例钮"
ProbeUiUtil.push_click(main, ap_bar._btn_legend)
await _frames(2)
var lclose: Button = ProbeUiUtil.find_button(main.ui.modals.run.legend_overlay, "关闭")
if lclose != null and main.ui.modals.run.legend_overlay.is_open():
ProbeUiUtil.push_click(main, lclose)
await _frames(2)
return
if not _status_done and not main.ui.battle.board_zone.status_badge.disabled:
_status_done = true
_last_action = "真点战斗屏状态角标"
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.status_badge)
await _frames(3) # 面板开层排版帧(同地图屏实证坑)
var resumed: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix")
if resumed != null and main.ui.modals.run.status_panel.visible:
ProbeUiUtil.push_click(main, resumed)
await _frames(2)
return
if not _refresh_done and ap_bar.is_refresh_ready():
_refresh_done = true
_last_action = "真点「弃图刷新」钮+确认链"
ProbeUiUtil.push_click(main, ap_bar._btn_refresh)
await _frames(2)
var cbtn: Button = ProbeUiUtil.find_button(ap_bar, "确认弃图")
if cbtn != null:
ProbeUiUtil.push_click(main, cbtn)
await _frames(2)
return
# 棋盘动作(方法-棋盘口——BoardView 公共点击口,ui_input_probe 先例;C 的按钮面全走真输入)
var minesweeper = settlement.minesweeper
var plan: Dictionary = AutoPlayer.choose_dig(minesweeper)
if plan.is_empty():
plan = AutoPlayer.gamble_pick(minesweeper)
if plan.is_empty():
_last_action = "真点「停手」钮(无计划)"
ProbeUiUtil.push_click(main, ap_bar._btn_stop)
await _frames(2)
return
_last_action = "棋盘挖格 %s(方法-棋盘口)" % str(plan.dig_cell)
if plan.hero_cell != minesweeper.hero_pos():
main.ui.battle.board_zone.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT)
main.ui.battle.board_zone.board_view.click_cell(plan.dig_cell, MOUSE_BUTTON_LEFT)
await _frames(1)
func _phase_end_back_menu() -> void:
# 终局收束:真点「返回」→ 回菜单;结算框/中途面已在冒险段分发
var t0 := Time.get_ticks_msec()
while Time.get_ticks_msec() - t0 < SEGMENT_TIMEOUT_MS:
await get_tree().process_frame
_record_screens()
var endo: Control = main.ui.modals.run.end_overlay
if endo != null and endo.is_open() and not main.run_scope.settlement_stop_timer.is_armed():
break
if main.ui.modals.run.end_overlay == null or not main.ui.modals.run.end_overlay.is_open():
_report("timeout", "终局收束", "真终局面未在超时内到达")
_save_misfire()
return
var btn: Button = main.ui.modals.run.end_overlay._btn
if btn != null:
_last_action = "真点终局「返回」钮"
ProbeUiUtil.push_click(main, btn)
await _phase_expect("终局回菜单", ["菜单"])
func _push_esc() -> void:
var ev := InputEventKey.new()
ev.keycode = KEY_ESCAPE
ev.physical_keycode = KEY_ESCAPE
ev.pressed = true
main.get_viewport().push_input(ev, true) # 键盘真输入(通道=真实输入-键盘)
# ---- 报告落盘(JSONP) ----
func _dump_report() -> void:
var dir := DirAccess.open("res://")
if dir != null:
dir.make_dir_recursive("tests/.gates/coverage")
var payload := {
"meta": {
"generated_at": Time.get_datetime_string_from_system(),
"seed": OS.get_environment("PROBE_SEED"),
"screens_seen_total": _screens_seen.size(),
"anomalies_reported": _problems.size(),
"anomalies_silent": _silent_count,
"fatal_count": _fatal_count(),
"anomaly_classes": "fatal/timeout/screen_mismatch(其余静默;误报指纹二次起静默)",
},
"screens_seen": _screens_seen.keys(),
"screen_trace": _screen_trace,
"anomalies": _problems,
}
var f := FileAccess.open(REPORT_PATH, FileAccess.WRITE)
if f != null:
var js := "/* 全动线自玩报告——journey_player.gd 随跑覆写;误报核销归用户复核轮。 */\n"
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
js += "window.__QA_COVERAGE__.journey = %s;\n" % JSON.stringify(payload, " ")
f.store_string(js)
f.close()
print("[journey] 报告已落盘 %s(屏到达 %d 类 / 上报 %d / 静默 %d)" % [
REPORT_PATH, _screens_seen.size(), _problems.size(), _silent_count])
+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://cjourneyplayer1"]
[ext_resource type="Script" path="res://sim/journey_player.gd" id="1"]
[node name="JourneyPlayer" type="Node"]
script = ExtResource("1")
+3 -1
View File
@@ -1,9 +1,11 @@
# 本目录只入库 baseline.json(存量基线,需随代码走以保证各 worktree 判定口径一致)。
# receipt.json 是每跑批覆写的当次回执、ci-status.json/ci-history.jsonl 是已退役的 CI 回执判定与流水账(判定只在远端,本机残留)、
# ci.lock/ci.rerun 是跑批件锁与补跑标记、*.log 是跑批当场日志——均为中间产物,不入库。
# ci.lock/ci.rerun 是跑批件锁与补跑标记、*.log 是跑批当场日志、coverage/ 是探针真输入覆盖登记
#(JSONP 式 .js,每次探针运行自动覆写——观测产物非事实源,不入库)——均为中间产物,不入库。
receipt.json
ci-status.json
ci-history.jsonl
ci.lock
ci.rerun
*.log
coverage/
+6 -4
View File
@@ -155,19 +155,21 @@ 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` 热点中心)→ `CampSceneView._gui_input()`(命中发 `object_pressed` 信号) → 组合根开界面(2026-11-06 真输入化,残险登记撤) | `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 真输入化);`presentation_encapsulation_sweep_test.gd:_init`(迭代52卡③组合根交接面结构性主证——pres封装 V1–V4 四组私取零命中+新公开面正锚,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`(篝火/开合段) |
| 营地篝火回主菜单与笔记本改挂动线 | 营地交互流 | 笔记本命中区真点(`push_click_at`)→ `CareerOverlay.open()`;篝火命中区真点 → ConfirmDialog 真钮二次确认 → 主菜单(2026-11-06 真输入化) | `ui_struct_probe.gd:_ready`(career 段);`screen_flow_probe.gd:_ready`(篝火/开合段——真输入驱动) |
| 确认弹窗按钮位次与语义同钉 | HUD框架 | `ConfirmDialog.open()`/`ConfirmDialog.open_three()` 开态读按钮行子节点序(定值=推进钮在左/次推进居中/维持钮在右,四处共用件同一序);探针在弹窗开态取按钮行 child(0) 的全局矩形中心经 `ProbeUiUtil.push_click_at` 真点 | `confirm_dialog_test.gd:_test_two_button_row_order`;`confirm_dialog_test.gd:_test_three_button_row_order`;`ui_click_probe.gd:_ready`(大门无断点段推进钮坐标真点) |
| 营地顶栏栏体定值与落座 | 营地场景视图 | 真实工程 boot 后经装配链 `MainUiBuild`→`CampSceneView.set_hud_band()` 读 `ui.modals.camp.scene.hud_band`(定值=`CAMP_HEADER_BAND`×α0.80、48px 全宽浮层、徽章右位顶格);单元经 `camp_scene_layout_table.gd` 的 `hud_band_h` 出口读背景贴图落位带像素算对比 ≥3:1 | `system_test.gd:_smoke_camp_hud_band`;`camp_scene_layout_test.gd:_test_hud_band_contrast_over_bg` |
| 关卡画面视口跟随 | 关卡画面(选路枢纽) | 停点切换与兑付回图经真实屏流程;读面 `LevelMapViewport.viewport_center_canvas_pos()` | `level_map_viewport_probe.gd:_probe_follow_anchor`;`level_map_viewport_probe.gd:_probe_smooth_scroll_reduced_motion` |
| 关卡画面视口平移 | 关卡画面(选路枢纽) | 合成拖拽沿与滚轮沿经 `push_input`(`ProbeUiUtil.push_drag_at()`/`push_wheel_at()`) | `level_map_viewport_probe.gd:_probe_drag_pan`;`level_map_viewport_probe.gd:_probe_wheel_pan`;`level_map_viewport_probe.gd:_probe_clamp_bounds_and_columns`;`level_map_viewport_probe.gd:_probe_drag_no_submit` |
| 关卡画面视口指引 | 关卡画面(选路枢纽) | 深图经 `LevelMapScreen.open_choices()` 真实停点 | `level_map_viewport_probe.gd:_probe_guidance_chrome` |
| 营地图鉴占位挂起态与反参照结构纪律 | 营地场景视图 | 布告板命中区 → `CampSceneView._gui_input()`(object_pressed 信号) → 图鉴占位视图;营地根态场景树审计 | `ui_struct_probe.gd:_ready`(占位禁词/禁行段);`ui_struct_probe.gd:_ready`(结构禁词段) |
| 营地图鉴占位挂起态与反参照结构纪律 | 营地场景视图 | 布告板命中区真点(`push_click_at`)→ `CampSceneView._gui_input()`(object_pressed 信号) → 图鉴占位视图;营地根态场景树审计(2026-11-06 真输入化) | `ui_struct_probe.gd:_ready`(占位禁词/禁行段);`ui_struct_probe.gd:_ready`(结构禁词段) |
| 营地刷新单口与场景根 HUD 同步 | 营地刷新总线 | 写口尾 → `CampRefreshBus.refresh()` → 现开界面刷新+场景根 HUD 刷新 | `ui_struct_probe.gd:_ready`(顶栏位段);`ui_click_probe.gd:_ready`(写口尾刷新段) |
| Esc 浮窗唤起与三出口动线 | 营地 Esc 流 | 营地场景根态合成 Esc 键事件→ `main_esc_flow.on_esc_input()` → `ui.flow.open/close`;浮窗三钮 `pressed` → 三出口 | `screen_flow_probe.gd:_ready`(Esc 唤起/子界面开态不唤起/再按关窗/非营地不唤起段);`screen_flow_probe.gd:_ready`(返回动线/设置双路径段) |
| 减动效开关存储契约(持久化回读/缺省值/引擎动效档应用) | 设置与本地化 | `SettingsStore.load_settings()`/`save_settings()`/`apply()` | `settings_store_test.gd:_test_settings_roundtrip`;`settings_store_test.gd:_test_settings_default`;`settings_store_test.gd:_test_settings_apply_chain` |
| 营地顶栏 HUD 钥石读数与刷新单口同步 | 营地 HUD 栏 | 隔离存档实例 `meta_state._add_keystone()`→ 显式触发 `CampRefreshBus.refresh(camp_data)` → `camp_hud_band.refresh()` → `keystone_badge` 读数 | `screen_flow_probe.gd:_ready`(HUD 余额读数/刷新段);`ui_struct_probe.gd:_ready`(顶栏位结构段) |
| 营地五窗统一窗壳形态 | 营地五界面 | 七物件命中区/笔记本 → `CampSceneView._gui_input()`(`object_pressed` 信号)→ 组合根分派开窗;窗板几何(居中定尺寸 720×640 起点)/遮罩/窗内滚动面为呈现提交面 | `screen_flow_probe.gd:_step_camp_window_shell`(窗壳形态段) |
| 营地五窗统一窗壳形态 | 营地五界面 | 七物件命中区/笔记本真点(`push_click_at`,2026-11-06 真输入化)→ `CampSceneView._gui_input()`(`object_pressed` 信号)→ 组合根分派开窗;窗板几何(居中定尺寸 720×640 起点)/遮罩/窗内滚动面为呈现提交面 | `screen_flow_probe.gd:_step_camp_window_shell`(窗壳形态段) |
**旅程段七:数据配置与跨系统契约**
@@ -20,6 +20,7 @@ var _db
func _init() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
_db = ConfigDBScript.new()
# 表加载失败不中止(承 G2–G5 同款:表损坏的红落在具体断言上)
var load_ok: bool = _db.load_tables("res://data/tables", true)
@@ -46,6 +47,7 @@ func _fresh_rng(seed_val: int):
# ---- ① RNG 事件日志会话落盘(源 `_initialize` :70,主会话裁决一改判集成——
# 驱动沿源 :60–70 取号序列自持:master 种子 42 → 前置取号 → new_adventure 开日志会话 → 会话内取号)----
func _rng_event_log_session() -> void:
# oracle: design/engineering/architecture/architecture.md:90 (事件日志落盘会话组)
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(42)
rng.open_stream("board_gen")
@@ -60,6 +62,7 @@ func _rng_event_log_session() -> void:
# _on_level_finished(victory)+breakpoint_snapshot 取读+clear_breakpoint;
# 起局与兑付两步的受理断言留单元宿主(adventure_feats_test),此处纯驱动——前置失败自然落红)----
func _feats_breakpoint_roundtrip() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:180 (专长断点附随件往返组)
var rt := FeatTable.new()
rt.rows = [
{"feat_id": "x1", "display_name": "试一", "rarity": "common", "desc": "测试揭示", "cap": 1,
@@ -105,6 +108,7 @@ func _feats_breakpoint_roundtrip() -> void:
# level_autoplay 真局摘要 → apply 原语 → 域读数 → 写盘读回保持;临时档 user://smoke_save_integration.json
# 段首清场+段尾清理(源 _smoke_save 隔离纪律),绝不碰 user://save.json(红线))----
func _save_summary_integration() -> void:
# oracle: design/gdd/systems/跨局循环/元进度.md:150 (摘要集成与钥石只增不减组)
var sm_script = load("res://core/save_manager.gd")
var path9 := "user://smoke_save_integration.json" # 源 paths[8]
if FileAccess.file_exists(path9):
@@ -137,6 +141,7 @@ func _save_summary_integration() -> void:
# ---- ④ 试玩记录仪(源 `_smoke_playtest_logger` 整套八条,主会话裁决七改判集成——
# PlaytestLogger 挂真实结算流:会话文件/逐结算快照/字段齐备/spawn_count 回归/诊断转储/JSONL 回读)----
func _smoke_playtest_logger() -> void:
# oracle: design/engineering/code-conventions.md:40 (试玩日志器跨系统接线组)
print("--- PlaytestLogger(结算快照 JSONL + 诊断转储)---")
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260912)
@@ -19,6 +19,7 @@ var _bp_path := "user://smoke_node_map_save.json"
func _init() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
_db = load("res://core/config_db.gd").new()
# 表加载失败不中止(承 G2/G3/G4/G5 同款:表损坏的红落在具体断言上)
var load_ok: bool = _db.load_tables("res://data/tables", true)
@@ -33,6 +34,7 @@ func _init() -> void:
# ---- 关卡节点图生成·确定性承载断言面 ----
func _smoke_generated_map_determinism() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点图生成.md:134 (起关/选路/断点重生成同图组)
# ① 纯函数复现:图 = f(参数档, 种子)——同档同种子两次产图逐位同图,换种子必变图
var gen = NodeMapGenerator.new(_fresh_rng(910001))
var params2: Dictionary = _db.run.level_gen_params.row_by_level_index(2)
@@ -20,6 +20,7 @@ var _disk: String
func _init() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
_disk = ProjectSettings.globalize_path(SAVE_PATH)
_clear_disk()
_db = load("res://core/config_db.gd").new() as ConfigDb
@@ -35,6 +36,7 @@ func _init() -> void:
# ---- 用例一:盘上往返保真——写档再读回,断点逐键零漂移 ----
func _test_disk_roundtrip_preserves_breakpoint() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:180 (存档⇄RNG 快照往返——断点域逐位组)
var rng := RngServiceScript.new()
var sm := SaveManagerScript.new(SAVE_PATH)
var adr := AdventureRunnerScript.new(_db, rng)
@@ -65,6 +67,7 @@ func _test_disk_roundtrip_preserves_breakpoint() -> void:
# ---- 用例二:旧版本档经迁移链后仍可续玩(单测不覆盖的跨系统集成面)----
func _test_old_version_save_migrates_and_resumes() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:179 (旧档迁移链读回续玩组)
var rng := RngServiceScript.new()
var sm := SaveManagerScript.new(SAVE_PATH)
var adr := AdventureRunnerScript.new(_db, rng)
+3
View File
@@ -16,6 +16,7 @@ var _checks: int = 0
func check(cond: bool, label: String) -> void:
# oracle: design/engineering/architecture/architecture.md:10 (测试层级四级——冒烟存活检查组)
_checks += 1
if cond:
print(" [PASS] " + label)
@@ -34,6 +35,7 @@ func _ready() -> void:
# 开机前置面:autoload 注册与资源可加载(挂主屏之前先查,失败时主屏断言整段跳过)
func _check_autoloads() -> void:
# oracle: design/engineering/architecture/architecture.md:10 (测试层级四级——冒烟存活检查组)
for autoload_name in ["ConfigDB", "RngService", "SaveManager", "MetaProgression"]:
var node: Node = get_node_or_null(NodePath("/root/" + autoload_name))
check(node != null, "autoload %s 在位" % autoload_name)
@@ -62,6 +64,7 @@ func _boot_main_scene() -> Node:
# 主屏存活面:组合根装配句柄在位(main.X 一级句柄契约,探针挂载假设的启动侧证明)
func _check_main_scene(main: Node) -> void:
# oracle: design/engineering/architecture/architecture.md:10 (测试层级四级——冒烟存活检查组)
check(main != null and main.is_inside_tree(), "主屏场景已实例化并在树")
if main == null:
return
+7
View File
@@ -33,6 +33,7 @@ func _initialize() -> void:
# (自 game/smoke/smoke_all.gd 剪切迁入——断言 label 逐字原样;G12 两套调用不随迁,
# 归 source_guard_test.gd,断言迁移批 B G1)
func _split_tail() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
await process_frame
await process_frame
var autoload_ok: bool = get_root().get_node_or_null("/root/ConfigDB") != null and get_root().get_node_or_null("/root/SaveManager") != null
@@ -51,6 +52,7 @@ func _split_tail() -> void:
# ---- 三屏场景化断言(V2——独立实例化+关键节点在位+组合根树序/z 序保序)----
# (自 game/smoke/smoke_all.gd 剪切迁入——label 与 cond 逐字原样,断言迁移批 B G1)
func _smoke_scene_split() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
print("--- 迭代27 T1 三屏场景化(V2:屏壳独立实例化+组合根装配保序)---")
# ① 三屏壳独立实例化(F6 等价——headless 直挂;视图代码构建在屏壳 _ready)
var menu: Node = load("res://presentation/scenes/menu_screen.tscn").instantiate()
@@ -147,6 +149,7 @@ func _count_visible_buttons(node: Node) -> int:
# ---- boot 链路时序后置条件(启动态六断言;只立行为可判断言,不造假时序)----
func _smoke_boot_chain() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
print("--- 迭代28 T2 boot 链路时序后置条件(V2:六断言)---")
var main: Node = load("res://presentation/main.tscn").instantiate()
root.add_child(main)
@@ -211,6 +214,7 @@ func _smoke_boot_chain() -> void:
func _smoke_run_lifecycle() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
print("--- 迭代27 T1 会话物化(V3:两连局复位+脏状态不残留)---")
var main: Node = load("res://presentation/main.tscn").instantiate()
root.add_child(main)
@@ -268,6 +272,7 @@ func _smoke_run_lifecycle() -> void:
# ---- d6/d7 批量模拟互斥守卫(wd6/wd7——白盒置态直调入口,以守卫早退的可见后果为断言面)----
func _smoke_sim_guards() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
print("--- 卡① T2:批量模拟互斥两守卫(d6/d7)---")
var main: Node = load("res://presentation/main.tscn").instantiate()
root.add_child(main)
@@ -341,6 +346,7 @@ func _face_snapshot(main: Node) -> Dictionary:
# 阶段面断言:adv/run/set=alive|null|both(会话三面存活态);恒活面每阶段必查;
# 现值回调与 run_scope 成员必须同源(回调链无悬挂铁证);extra=阶段附加判据。
func _stage_faces(main: Node, tag: String, adv: String, run: String, set: String, extra: Dictionary = {}) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var snap := _face_snapshot(main)
var bad := ""
for k in ["bonus_meta", "bonus_save", "camp_meta", "camp_save", "rp_log", "rp_stage", "rp_board_zone", "rp_fx", "rp_board"]:
@@ -409,6 +415,7 @@ func _face_liveness(name: String, v, want: String) -> String:
#(level_started→choices_presented 同步到达,无可观测中间态)——合并为 V3b 一处断言。
# (自 game/smoke/smoke_all.gd 剪切迁入——label 与 cond 逐字原样,断言迁移批 B G1)
func _smoke_lifecycle_matrix() -> void:
# oracle: design/gdd/systems/冒险循环/冒险结构.md:284 (生命周期矩阵——三关序列组)
print("--- 迭代28 T3 生命周期存活性矩阵(V3:九阶段×五引用面)---")
var main: Node = load("res://presentation/main.tscn").instantiate()
root.add_child(main)
@@ -8,11 +8,13 @@ const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:293 Boss 节点开战行集宿主(装载组)
_test_boss_node_spawns_boss_rowset()
quit(Kit.finish("boss_gate_test"))
func _test_boss_node_spawns_boss_rowset() -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:295 开战生成按遭遇坐标取行集(Boss 行集组)
var db = load("res://core/config_db.gd").new()
var load_ok: bool = db.load_tables("res://data/tables", true) and db.diagnostics_count("error") == 0
Kit.check("前置:表加载零 error", load_ok)
@@ -23,6 +23,7 @@ var _rows: Array = [] # 当前 run 的行协议收集(_on_row 绑定法—
func _init() -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:360 伤害单据编排宿主(装载组)
var db = ConfigDBScript.new()
var load_ok: bool = db.load_tables("res://data/tables", true)
Kit.check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0)
@@ -70,6 +71,7 @@ func _row_by_display_prefix(prefix: String) -> Dictionary:
# ---- 五配方一次全流程 + 双跑逐位复现 ----
func _run_recipe_sequence(db) -> Array:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:362 (五段全序——配方序列组)
_rows = []
var h = _harness(db)
var units = h.units
@@ -176,6 +178,7 @@ func _run_recipe_sequence(db) -> Array:
func _test_five_recipes_and_replay(db) -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:363 (同种子回放逐行相同组)
var run1: Array = _run_recipe_sequence(db)
Kit.check("五配方序列产出 5 行伤害行", run1.size() == 5)
var run2: Array = _run_recipe_sequence(db)
@@ -185,6 +188,7 @@ func _test_five_recipes_and_replay(db) -> void:
# ---- A 模式(开单即结——回切位) ----
func _test_policy_immediate_mode(db) -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:362 (结算策略即时面组)
_rows = []
var h = _harness(db)
if not h.spawn_ok:
@@ -200,6 +204,7 @@ func _test_policy_immediate_mode(db) -> void:
# ---- 来源敌死亡:队列中未结单据作废(死亡即不动手) ----
func _test_source_death_void(db) -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:133 (摇定后兑现前被杀本拍作废组)
_rows = []
var h = _harness(db)
if not h.spawn_ok:
@@ -233,6 +238,7 @@ func _unit_fixture() -> Dictionary:
func _test_identical_content_dual_enemy(db: ConfigDb) -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:303 (双敌同内容顺位组)
var a := _unit_fixture()
var b := _unit_fixture()
Kit.check("前置:双敌字典内容全同且非同一实例", a == b and not is_same(a, b))
@@ -262,6 +268,7 @@ func _test_identical_content_dual_enemy(db: ConfigDb) -> void:
# d12 产发端增键:目标身份随行/随载荷下发(同名敌按 id 精确解析的产发半)
func _test_target_id_payload(db: ConfigDb) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
_rows = []
var h = _harness(db)
if not h.spawn_ok:
@@ -284,6 +291,7 @@ func _test_target_id_payload(db: ConfigDb) -> void:
func _test_requires_gate_and_zero(db) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
_rows = []
var h = _harness(db)
if not h.spawn_ok:
@@ -310,6 +318,7 @@ func _test_requires_gate_and_zero(db) -> void:
# ---- 主角倒下:flush 余单全部作废(_halted 终止) ----
func _test_hero_death_halt(db) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
_rows = []
var h = _harness(db)
if not h.spawn_ok:
@@ -327,6 +336,7 @@ func _test_hero_death_halt(db) -> void:
# ---- AC 合流:override 入参 / 表值兜底 / ac 型状态叠层(不入池) ----
func _test_ac_merge(db) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
_rows = []
var h = _harness(db)
if not h.spawn_ok:
@@ -354,6 +364,7 @@ func _test_ac_merge(db) -> void:
# ---- 池乘区收口:池内加算 → clamp;ac 型不入池 ----
func _test_pool_clamp(db) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
_rows = []
var h = _harness(db)
if not h.spawn_ok:
@@ -411,6 +422,7 @@ func _test_pool_clamp(db) -> void:
# ---- 个体池读数 / 个体 AC 合流(增益状态 v1.5——敌方状态个体化批)----
func _test_pool_consumption_per_unit(db) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var h = _harness(db, 3) # depth 3 = 混编行集(熊地精 1 + 地精 1–2,最少 2 只——个体对照面需 ≥2 敌)
if not h.spawn_ok:
return
@@ -430,6 +442,7 @@ func _test_pool_consumption_per_unit(db) -> void:
func _test_defender_ac_per_unit(db) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var h = _harness(db, 3)
if not h.spawn_ok:
return
@@ -12,6 +12,7 @@ var db
func _init() -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:360 (伤害公式宿主——装载组)
db = ConfigDBScript.new()
var load_ok: bool = db.load_tables("res://data/tables", true)
Kit.check("前置:settlement 表加载零 error", load_ok and db.diagnostics_count("error") == 0)
@@ -30,6 +31,7 @@ func _floor_val() -> int:
# ---- F2 乘区:池内加算 → 池间乘算 → floor(单一取整规则)----
func _test_pool_damage() -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:360 (池伤害公式组)
var d := _floor_val() + 9 # 表值推导的任意正探针量(非玩法行值)
Kit.check("中性双池 (0,0) 原样透传", DamageMathScript.pool_damage(d, 0, 0) == d)
Kit.check("负敌方池压低不增高", DamageMathScript.pool_damage(d, 0, int(db.combat.settlement.core.pool_clamp_lo)) <= d)
@@ -42,6 +44,7 @@ func _test_pool_damage() -> void:
# ---- 克制分量:空桶不吃修正恒 0;落地下限保底([1,+∞) 全来源)----
func _test_counter_component() -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:360 (计数器分量组)
var floor_v := _floor_val()
Kit.check("空桶(点数≤0)恒 0 不吃修正", DamageMathScript.counter_component(0, floor_v, floor_v) == 0
and DamageMathScript.counter_component(-1, 5, floor_v) == 0)
@@ -53,6 +56,7 @@ func _test_counter_component() -> void:
# ---- 掷点折扣查档(纯函数):满阈值恒全额;档位边界/末档语义与表数组一致 ----
func _test_roll_discount_percent() -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:360 (乘区折扣组)
var threshold := 10 # 查档探针阈值(夹具值——档位参数本身全部来自表)
var bounds: Array = db.combat.settlement.damage_roll.roll_tier_bounds
var discounts: Array = db.combat.settlement.damage_roll.roll_tier_discounts
@@ -77,6 +81,7 @@ func _test_roll_discount_percent() -> void:
# ---- 吃掷点分量算术:折扣→(暴击)→floor→+克制修正(单一取整链)----
func _test_rolled_component() -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:360 (掷点分量组)
var base := float(_floor_val() + 7) # 表值推导探针基线
var crit_mult: int = int(db.combat.settlement.damage_roll.crit_multiplier)
Kit.check("满额折扣无暴击 = floor(基线) + 修正 0", DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) == int(floor(base)))
@@ -19,6 +19,7 @@ var _db
func _init() -> void:
# oracle: design/engineering/architecture/architecture.md:144 (效果端口宿主——装载组)
_db = ConfigDBScript.new()
var load_ok: bool = _db.load_tables("res://data/tables", true)
Kit.check("前置:表加载零 error", load_ok and _db.diagnostics_count("error") == 0)
@@ -53,6 +54,7 @@ func _hero_attack_value(rows: Array) -> int:
# ---- V1 端口单实例与真实发射路由 ----
func _smoke_effect_port_wiring() -> void:
# oracle: design/engineering/architecture/architecture.md:144 (端口单实例与六 Callable 路由组)
print("--- 效果端口:装配链单实例+真实发射路由 ---")
# 充能跑(蓄力周转 charge_rotation 在册——attack_commit 订阅行 attack_bonus per 蓄力周转计数器)
var orch_c = _orch(TEST_SEED)
@@ -86,6 +88,7 @@ func _smoke_effect_port_wiring() -> void:
# ---- V2 业务类零引擎直连(源码契约锚——theme_projection_test 源码锚惯例)----
func _test_business_classes_engine_free() -> void:
# oracle: design/engineering/architecture/architecture.md:144 (业务类零引擎直连组)
print("--- 效果端口:业务类零引擎直连源码锚 ---")
for path in ["res://core/damage_controller.gd", "res://core/action_economy.gd"]:
var src := FileAccess.get_file_as_string(path)
@@ -97,6 +100,7 @@ func _test_business_classes_engine_free() -> void:
# ---- V3 伤害域事实面闭集(合成行两对照——damage_taken 真实单据流)----
func _test_biz_fact_face_closed() -> void:
# oracle: design/engineering/architecture/architecture.md:144 (闭面四键 biz_fact 组)
print("--- 效果端口:业务发射事实面闭集 ---")
var orch = _orch(TEST_SEED)
Kit.check("V3 begin_battle 受理", orch.begin_battle(_enc([])))
@@ -11,6 +11,7 @@ const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 (扩展行必载 effect_id——加载致命组)
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
push_error("[extended_row_violation_test] 表加载失败")
@@ -154,6 +154,7 @@ func _seal_direct_with_expansion(orch) -> Vector2i:
# 四条真实路径(敌方攻击/危险格/错误封印/地形)逐路驱动:每条至少一张主角承伤单据在案,
# 且承伤计数器 == 主角承伤单据行数(每单据恰发射一次的全量判据——重复发射即计数器>行数)。
func test_all_hero_damage_paths_emit_once() -> void:
# oracle: design/gdd/systems/冒险循环/奇物.md:465 (换图生存:全局固定减伤全伤害路径恰一次组)
print("--- V5 全局 damage_taken:四路径逐路恰一次 ---")
# ① 敌方攻击路径(Boss 战零挖掘连停三轮——干净样本:零地形零资源单据;多轮保证伤害技在案)
var orch1 = _orch(46001)
@@ -223,6 +224,7 @@ func _plan_seal(orch, target: Vector2i) -> Dictionary:
# 同一伤害不得出现两次消费(重复发射=计数器>行数):敌方路径与地形路径两样本逐位核对——
# 地形单据带 hero_damage_consumed 标记,战斗段末 _resolve_board_flat_hero 跳过二次发射由此钉住。
func test_no_duplicate_damage_taken_event() -> void:
# oracle: design/gdd/systems/冒险循环/奇物.md:465 (受击事件零重复组)
print("--- V5 全局 damage_taken:无重复消费 ---")
var orch1 = _orch(46011)
var rows1 := _collect_hero_rows(orch1)
@@ -264,6 +266,7 @@ func _auto_dig(orch) -> void:
# V2 矿脉储能:受理的付费挖格 RAP 恰 +1、AP 按表扣、板资源不变
func test_reserve_on_accepted_dig() -> void:
# oracle: design/gdd/systems/排雷循环/行动经济.md:256 (换图基座:挖格产 RAP 组)
print("--- V2 矿脉储能:付费挖格产 RAP ---")
var orch = _orch(46021)
Kit.check("V2 前置:战斗受理", orch.begin_battle(_enc(["vein_reserve"])))
@@ -301,6 +304,7 @@ func _drain_round_rap_accounting(orch) -> bool:
# V3 省力周转:折价门槛与扣费(can 与执行同源;base 8→扣 4)
func test_refresh_discount_gate_and_charge() -> void:
# oracle: design/gdd/systems/排雷循环/行动经济.md:256 (刷新折扣门控与充能组)
print("--- V3 省力周转:折价门槛与扣费 ---")
var orch = _orch(46022)
Kit.check("V3 前置:战斗受理", orch.begin_battle(_enc(["easy_swap"])))
@@ -322,6 +326,7 @@ func test_refresh_discount_gate_and_charge() -> void:
# V3 双基座叠加:挖格产 RAP 与刷新折价同场生效
func test_double_base_stack() -> void:
# oracle: design/gdd/systems/冒险循环/奇物.md:465 (双基座叠加组)
print("--- V3 双基座叠加 ---")
var orch = _orch(46023)
Kit.check("V3 前置:战斗受理", orch.begin_battle(_enc(["vein_reserve", "easy_swap"])))
@@ -338,6 +343,7 @@ func test_double_base_stack() -> void:
# V12(refresh_feats 侧):无专长基线——挖格零产 RAP、刷新按表基线扣、AP 保留、上限照旧
func test_no_feats_baseline() -> void:
# oracle: design/gdd/systems/冒险循环/奇物.md:465 (零专长基线组)
print("--- V12 无专长基线(refresh_feats 侧) ---")
var orch = _orch(46024)
Kit.check("V12 前置:战斗受理", orch.begin_battle(_enc([])))
@@ -373,6 +379,7 @@ func _br_count(orch) -> int:
# V4 成功事实只在新板就位后发射:成功恰一次;非 Active/每轮上限/RAP 不足三拒面全零事实零换图
func test_board_refresh_fact_only_on_success() -> void:
# oracle: design/gdd/systems/排雷循环/行动经济.md:256 (弃图刷新事实面仅成功态组)
print("--- V4 成功刷新事实:拒绝零事实 ---")
_install_br_probe()
var orch = _orch(46031)
@@ -407,6 +414,7 @@ func test_board_refresh_fact_only_on_success() -> void:
# V10 矿道红利:每次成功刷新 escrow 恰 +1 且 fx_grant_gp 行在案;拒绝零变化
func test_mine_profit_gp() -> void:
# oracle: design/gdd/systems/冒险循环/奇物.md:465 (换图资源:刷新固定 GP 收益组)
print("--- V10 矿道红利:刷新固定 GP ---")
var orch = _orch(46032)
var gp_rows := {"n": 0}
@@ -453,6 +461,7 @@ func _damage_rows_by_source(rows: Array, source: String) -> Array:
# V6 石层缓冲:全局固定减伤(危险格 pierce 8→6 两档钉死:单刷减 2·同轮多刷不叠加·未刷新轮不减;
# 地形路径减后值=max(下限, effect_value−2)——含下限保持;敌方路径照常单次发射)
func test_stone_mitigation_all_damage() -> void:
# oracle: design/gdd/systems/冒险循环/奇物.md:465 (全局减伤全覆盖组)
print("--- V6 石层缓冲:全局固定减伤 ---")
# S1 同轮双刷 → 危险格 pierce 8 减 2=6(叠加即 4——多刷不增钉死)
var orch1 = _orch(46041)
@@ -508,6 +517,7 @@ func test_stone_mitigation_all_damage() -> void:
# V9 坑道小憩:轮末合计回复(白盒预置承载上限 3——计数器直改属数据面注入;
# 自然累计 2 与满血未生效两形态分战场钉死)
func test_rest_rotation_cap_and_clear() -> void:
# oracle: design/gdd/systems/冒险循环/奇物.md:465 (轮末恢复 cap 与清账组)
print("--- V9 坑道小憩:轮末恢复 ---")
# R1 白盒预置 3 层 + 刷新闸(when<3 拒第 4 层)→ 停手回复 min(3, 缺血)——护甲降档保缺血 ≥3
var orch1 = _orch(46051)
@@ -565,6 +575,7 @@ func test_rest_rotation_cap_and_clear() -> void:
# V8 蓄力周转:当轮攻击消费全部层数(上限 3 闸=白盒预置;战斗新建自然清空)
func test_charge_layers_consumed_same_round() -> void:
# oracle: design/gdd/systems/冒险循环/奇物.md:465 (充能当轮消耗组)
print("--- V8 蓄力周转:攻击消费层 ---")
var orch1 = _orch(46061)
var rows1 := _collect_damage_rows(orch1)
@@ -591,6 +602,7 @@ func test_charge_layers_consumed_same_round() -> void:
# V7 崩壁冲击:延迟群伤单(刷新时点零伤害行——战斗段末按存活目标结账)
func test_blast_defers_to_combat_segment() -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:362 (刷新群伤让位战斗段组)
print("--- V7 崩壁冲击:延迟群伤 ---")
var orch = _orch(46071)
var rows := _collect_damage_rows(orch)
@@ -609,6 +621,7 @@ func test_blast_defers_to_combat_segment() -> void:
# 七件 validate/在册/稀有度 cap 对表/跨表计数器引用闭合/desc 禁词
func test_seven_feats_validate_and_pool() -> void:
# oracle: design/gdd/systems/冒险循环/奇物.md:465 (换图系七件校验与池组)
print("--- V11 七件数据与池 ---")
var seven := {"vein_reserve": "excellent", "easy_swap": "excellent", "wall_blast": "excellent",
"charge_rotation": "excellent", "stone_mitigation": "common", "tunnel_rest": "common", "mine_profit": "rare"}
@@ -19,6 +19,7 @@ var _db
func _init() -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:360 (掷点与乘区宿主——装载组)
_db = ConfigDBScript.new()
# 表加载失败不中止(承 G2/G3 同款:表损坏的红落在具体断言上——
# 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评)
@@ -43,6 +44,7 @@ func _init() -> void:
# 受理后不变量(encounter 受理后只读)。攻击单 base_value 贯通由 _smoke_damage_types 的
# hero_atk_val=100 全链路行值承载(同夹具既有覆盖——派生读在攻击链上工作即行值随 override 走)----
func _smoke_hero_atk_derived() -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:360 (英雄攻击派生组)
var rng_a = load("res://core/rng_service.gd").new()
rng_a.set_master_seed(4242)
var s_a = load("res://core/battle_orchestrator.gd").new(_db, rng_a)
@@ -65,6 +67,7 @@ func _smoke_hero_atk_derived() -> void:
# → heavy 伤害 == light −1、ward == light +1(要求基线伤害 ≥ 2——地板 1 会吞 −1 位移)。
# 承伤位移红线(卡片 §8 L3):全局位移只允许 ±1 量级——非此量级 = 矩阵消费面有未盘点的第二落点(停机)。
func _smoke_enemy_attack_type() -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:295 (敌攻击类型行集组)
print("--- 敌方攻击类型轴(迭代 24 N3)---")
# ① 表列:20 行全填 normal(N3 全填不做差异化——Q9)
var at_ok := true
@@ -123,6 +126,7 @@ func _smoke_enemy_attack_type() -> void:
# ---- 护甲等级掷点·行为面(折扣档分布 / nat20 暴击 / 伤害下限 / 棋盘来源必中)----
func _smoke_armor_roll() -> void:
# oracle: design/gdd/systems/战斗循环/增益状态.md:254 (护甲掷点与叠层合并组)
print("--- 护甲等级掷点(查档纯函数 + 暴击 + 伤害下限 + AC12 分布 + 棋盘必中)---")
# ① 查档纯函数:全掷点域逐位对照(AC10 / AC12)+ EV 锚(AC10 期望减免 21.25% —— registry 参考数)
var expect_ac10 := {100: 11, 75: 3, 50: 4, 25: 2} # 掷 10-20 全额 / 7-9 / 3-6 / 1-2
@@ -341,6 +345,7 @@ func _auto_play_round(settlement, out: Dictionary) -> void:
# 基线构成:基线 100——hero 攻击类型=normal(锻造工坊配置默认值,
# hero.tres 无两轴字段)对轻甲中性 0 修正(目标 = L1 地精轻甲;AC 0 恒全额非暴击种子)。
func _smoke_pool_wiring() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
print("--- 主角乘区接线 + 硬化折半方向 ---")
var base_row := _hero_attack_row_with({}, 100, 20260918)
var up_row := _hero_attack_row_with({"effects": ["attack_up"]}, 100, 20260918)
@@ -382,6 +387,7 @@ func _hero_attack_row_with(opts: Dictionary, hero_atk_val: int, seed_val: int) -
# ---- 攻击类型 × 护甲克制 + 标错惩罚冒烟(护甲快照/克制矩阵/充能/反噬三选一)----
func _smoke_damage_types() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
print("--- 攻击类型 × 护甲克制 + 标错惩罚 ---")
# ① 护甲类型快照传播(spawn 行值 → 单位字典)
var enemy_system = load("res://core/unit_container.gd").new(_db)
@@ -476,6 +482,7 @@ func _find_seal_target(minesweeper, want_mine: bool) -> Dictionary:
# d_tick = max(damage_floor, pool_damage(floor(基线×档位%), 0, 0)),档位读数随行重建可归因)----
func _smoke_attack_chain_fresh_pool() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
print("--- 战斗结算·fresh 池攻击链回归(Bug#2·掷点口径)---")
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260913)
@@ -22,6 +22,7 @@ var _db
func _init() -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:360 (轮内流与技能池宿主——装载组)
_db = ConfigDBScript.new()
# 表加载失败不中止(承 G2/G3/G4/G5 同款:表损坏的红落在具体断言上——
# 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评)
@@ -47,6 +48,7 @@ func _init() -> void:
# ---- 完整战斗回放(自动玩家,确定性)——回放断言:同种子两遍步骤流逐行相同 ----
func _smoke_full_battle(L: int, D: int, label: String) -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:362 (全战斗回放——五段全序组)
print("--- 完整战斗回放:%s ---" % label)
var run1 = _run_battle(L, D)
var run2 = _run_battle(L, D)
@@ -81,6 +83,7 @@ func _smoke_full_battle(L: int, D: int, label: String) -> void:
func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:362 (战斗驱动组)
# 每遍全新 RngService(同 master 种子)——回放断言的隔离前提(地图战斗资源 AC"两套全新 RngService+地图战斗资源"同款)
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(42)
@@ -145,6 +148,7 @@ func _auto_play_round(settlement, out: Dictionary) -> void:
# ---- 行动经济·轮内刷新冒烟(方案 A:Active 相位当场换图继续挖)----
func _smoke_active_refresh() -> void:
# oracle: design/gdd/systems/排雷循环/行动经济.md:262 (停手转存储备与轮内刷新组)
print("--- 行动经济·轮内刷新(方案 A)---")
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260912)
@@ -251,6 +255,7 @@ func _active_refresh_replay() -> Array:
# ---- 遭遇随机化冒烟(难度曲线与Boss Core Rule 8:区间骰/encounter 流/可回放/空表定值路径)----
func _smoke_encounter_roll() -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:296 (遭遇区间骰——摇号确定性组)
print("--- 遭遇随机化(unit_count 区间骰)---")
var counts: Array = []
for i in range(20):
@@ -385,6 +390,7 @@ func _encounter_replay(seed_val: int) -> Array:
# 五段全序断言(战斗结算 验收"五段全序"+ 行型封闭 10 类):逐轮行相位序 = 授予 → 增益/减益 → 战斗 → 衰减 → 判定。
func _smoke_settlement_segments() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
print("--- 五段全序 + 行型封闭 ---")
Kit.check("segment_order 五段冻结在位(表值 = 拍板序恒等)",
Array(_db.combat.settlement.segment_order) == SettlementTable.FROZEN_SEGMENT_ORDER
@@ -503,6 +509,7 @@ func _smoke_skill_pool() -> void:
# ① 数据面:技能表行集 + 普攻锁死 + 建议池行值 + 空池不可能(结构断言)
func _smoke_skill_pool_data() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
Kit.check("技能表十六行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动 + 震地怒击/遏毒/岩崩 + 毒吐/盾击——前三批 + Boss 半血解锁技三行〔迭代14④ T5〕+ 普通敌错峰两行〔迭代 24 N2〕+ 触发条件批示范两行〔孤注守势/趁隙偷袭〕)", _db.combat.skill.rows.size() == 16)
var basic: Dictionary = _db.combat.skill.row_by_id("attack")
Kit.check("普攻行:倍率恒 1 · 冷却恒 0(域校验锁死——池恒非空的结构前提)",
@@ -717,6 +724,7 @@ func _fresh_rng(seed_val: int):
# 单敌轨迹不独立复放——轨迹取数与其它流交错,无法脱离全序重建;
# 同种子全战斗逐位复现由 ② 全对拍承载,单点定位归事件日志。
func _smoke_skill_pool_replay_independence() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var seed_val := -1
for s in range(1, 60):
var seqs := _record_pool_trajectories(s, 1)
@@ -770,6 +778,7 @@ func _record_pool_trajectories(seed_val: int, rounds: int) -> Dictionary:
# 2026-10-28 重锚 [0.82, 1.02](照酋长先例 ±0.10 绝对幅度,中值取理论稳态 0.92;旧带 [1.00,1.20] 系触发器前
# 唯一特殊技=突刺的池结构下登记,随池构成换代作废;撤销证明=带宽回退旧值本断言复红)→ 宽带登记。
func _smoke_skill_pool_ev_anchor() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var bands := {"goblin": [0.82, 1.02], "bugbear": [0.97, 1.03], "ogre_chief": [1.10, 1.30]} # 地精重锚 2026-10-28(触发条件批孤注守势入池稀释,无情境口径实测 0.9148;EV 锚注 enemy.tres)· 酋长重锚(震地怒击半血解锁入池,含闸长期速率上移:实测 1.1948;EV 锚注 enemy.tres)
var species := ["goblin", "bugbear", "ogre_chief"]
var rng = load("res://core/rng_service.gd").new()
@@ -825,6 +834,7 @@ func _smoke_skill_pool_ev_anchor() -> void:
# ⑤ 死亡打断作废(摇定后、兑现前死亡 → 本拍技能作废、不动手、不进账)
func _smoke_skill_pool_death_void() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var void_seed := -1
for s in range(1, 400):
var rg = load("res://core/rng_service.gd").new()
@@ -864,6 +874,7 @@ func _smoke_skill_pool_death_void() -> void:
# ⑥ 加载校验拒绝(单表域校验 + 跨表池构成纪律)
func _smoke_skill_pool_validation_reject() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var skill_table = load("res://data/schemas/skill_table.gd").new()
var enemy_table = load("res://data/schemas/enemy_table.gd").new()
enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "ghost", "weight": 1}]}])
@@ -922,6 +933,7 @@ func _has_err_category(errs: Array, category: String) -> bool:
# 停手轮驱动 (2,9)——零挖掘零封印 → 零池,伤害行可逐拍精确复算;种子 hunt 至少各命一粒毒爆伤害行
# 与蛛网束缚施放行;毒分量随开战掷定的攻击基线缩放(atk 区间 2–4 → rider 1–3)。
func _smoke_debuff_rider() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var venom_seed := -1
var venom_check_ok := true
var venom_rows_seen := 0
@@ -992,6 +1004,7 @@ func _smoke_debuff_rider() -> void:
# ---- 天赋树六键行为冒烟(V10:开局姿态窗/危险直觉限次/财富之道聚散/守侧失误补偿;
# 引擎直构 + 编排授予段直注(_grant_seg._judge_seal=结算授予段封印判定单口))----
func _smoke_counter_buckets() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
print("--- 效果引擎无状态化四桶正向验收(卡② §9 V1/V6)---")
# V1——adventure 桶跨战持久:同一宿主字典下两战各真实 request_dig 一次,
# 断付费计数经 ctx 就地写入宿主字典并跨战累积(副本搬运态:读不到上一战值→第 2 战仍 1,此断言会红)。
@@ -1054,6 +1067,7 @@ func _smoke_counter_buckets() -> void:
# V6 形态先例同源:直入真实管线缝(_judge_seal 直调同款口径)。
func _smoke_extended_rows() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
print("--- 扩展行收敛:fx_reveal 兑付/落空 ---")
var RigS = load("res://core/battle_orchestrator.gd") # 局部装配别名(_smoke_counter_buckets 同款——跨函数不共享局部量)
var enc_fx = {"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
@@ -1099,6 +1113,7 @@ func _smoke_extended_rows() -> void:
# 蓄力表值 2 拍衰减后剩 1 拍仍在池)→ 逐敌读敌方输出池。判据=施放者与被他敌的池读数差。
func _smoke_enemy_buff_targets_self() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var emp_mag: int = int(_db.combat.status_effects.row_by_id("empower").magnitude)
var sc_script = load("res://core/status_container.gd")
var found_seed := -1
@@ -22,6 +22,7 @@ var _db
func _init() -> void:
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:231 (封印与极性宿主——装载组)
_db = ConfigDBScript.new()
# 表加载失败不中止(承 G2/G3/G4 同款:表损坏的红落在具体断言上——
# 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评)
@@ -45,6 +46,7 @@ func _init() -> void:
# ---- 末点 AP 封印轮末结算(playtest 缺陷②回归:末点 AP 花在封印必须终结轮次进入下一轮)----
func _smoke_seal_ap_exhausted() -> void:
# oracle: design/gdd/systems/排雷循环/行动经济.md:260 (末点 AP 耗尽即收轮组)
print("--- 排雷玩法·封印动作耗尽末点 AP 回归 ---")
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260911)
@@ -322,6 +324,8 @@ func _rows_at(rows: Array, source_class: String, cell) -> Array:
func _smoke_clear_reward_chain() -> void:
# oracle: design/gdd/systems/战斗循环/地图战斗资源.md:235 (清图奖励链——清图槽对象身份组)
# oracle: design/gdd/systems/战斗循环/地图战斗资源.md:235 (清图奖励链——清图槽对象身份组)
print("--- 清图奖励·双份兑付链(完美封印 / 零兑付 / 第二份被拒 / 高光记实)---")
# ① 真实流完美封印:标中一面真雷封印 + 零误标 + 清全图 → grant_clear 身份(clear_blast)双份真实兑付
var rows: Array = []
@@ -350,6 +354,8 @@ func _smoke_clear_reward_chain() -> void:
# 弱形式登记:正常战斗流下"授段时刻无存活敌"不可达——敌全灭当轮判定段即判胜并终止战斗,而清图批在判定段之前,
# 恒有存活敌;故本组直调编排私有面构造状态,只断言"路径存在 + 行为符合契约"(构造手法见各行注释)。
func _smoke_clear_reward_zero_paths() -> void:
# oracle: design/gdd/systems/战斗循环/地图战斗资源.md:240 (无存活敌群伤记「奖励未生效」组)
# oracle: design/gdd/systems/战斗循环/地图战斗资源.md:240 (无存活敌群伤记「奖励未生效」组)
# ②-a 首份零兑付判定(无存活敌)→ first_ok=false + 未生效行
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260915)
@@ -404,6 +410,8 @@ func _smoke_clear_reward_zero_paths() -> void:
# 轮内刷新 provenance 守卫:刷新换来的图同轮清图不发清图奖励(_board_from_midround_refresh 守卫)
func _smoke_refresh_provenance_guard() -> void:
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:237 (换图重置:旧封印印记先判定后消失组)
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:237 (换图重置:旧封印印记先判定后消失组)
print("--- 轮内刷新 provenance 守卫(刷新图清图不发清图奖励)---")
var rows: Array = []
var run := _run_full_clear(20260916, true, rows)
@@ -430,6 +438,8 @@ func _smoke_refresh_provenance_guard() -> void:
func _smoke_polarity_routing() -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:366 (极性路由:正向只兑正向、负向只兑负向组)
# oracle: design/gdd/systems/战斗循环/战斗结算.md:366 (极性路由:正向只兑正向、负向只兑负向组)
print("--- 极性路由(挖安全格正向 / 挖危险格负向 / 标中正向 / 标空负向)---")
# 战斗随机值面随全局序漂移——构造种子按行为面扫描(谓词见 _polarity_probe_accepts)
var probe := {}
@@ -567,6 +577,8 @@ func _smoke_polarity_routing() -> void:
# 场景 B(②):标危险格 → 直接挖开 → 收轮。
# 场景 C(①③):标安全格 + 标危险格 → 不挖收轮 → 标空/标中判定照常。
func _smoke_seal_irreversible_quadrants() -> void:
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:231 (封印不可逆四情况组)
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:231 (封印不可逆四情况组)
print("--- 封印不可逆四情况(禁撤封印 + 挖封印格兑付改判)---")
# 场景 A:安全格直挖(情况④)+ seal_pull/重复封印拒绝面
var rng_a = load("res://core/rng_service.gd").new()
@@ -675,6 +687,8 @@ func _smoke_seal_irreversible_quadrants() -> void:
# 负向字典逐型直验(类型化兑付行协议):找真实板上各型非封锁格,直调 grant_routed 负向路由(att 归因束形态),
# 断言每型的兑付形态(enrage/off_guard/sapped 效果行、heal 直伤行、blast 警觉行、action 行动点排队)。
func _smoke_negative_dictionary() -> void:
# oracle: design/gdd/systems/排雷循环/行动经济.md:256 (下轮行动点修正排队与兑现组)
# oracle: design/gdd/systems/排雷循环/行动经济.md:256 (下轮行动点修正排队与兑现组)
print("--- 负向字典逐型直验(sapped 在位 / heal 直伤 / blast 警觉 / action 行动点)---")
# 数据面:sapped 行在位(状态表七行 + effect_pool_map 映射在位)
var sapped: Dictionary = _db.combat.status_effects.row_by_id("sapped")
@@ -768,6 +782,8 @@ func _smoke_negative_dictionary() -> void:
# 资源面断言:chain/dig 事件恒携带 resource=true(危险格同权);initial 恒 false(封锁格);
# 封锁格查询 = null、危险格未开恒可查。
func _smoke_resource_surface() -> void:
# oracle: design/gdd/systems/战斗循环/地图战斗资源.md:235 (资源面只读查询组)
# oracle: design/gdd/systems/战斗循环/地图战斗资源.md:235 (资源面只读查询组)
print("--- 格面资源资格面(危险格携带 / 封锁格无资源)---")
var run1 = _run_battle(1, 0)
var run2 = _run_battle(2, 0)
@@ -419,6 +419,7 @@ var _db
func _init() -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:362 (结算接线同种子逐项 golden 对拍组)
_db = ConfigDBScript.new()
var load_ok: bool = _db.load_tables("res://data/tables", true)
Kit.check("前置:表加载零 error", load_ok and _db.diagnostics_count("error") == 0)
@@ -13,6 +13,7 @@ const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:293 Boss 半血解锁技宿主(装载组)
var db = ConfigDBScript.new()
var load_ok: bool = db.load_tables("res://data/tables", true)
Kit.check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0)
@@ -25,6 +26,7 @@ func _init() -> void:
# 门谓词:卓尔 (2,9) hp 130(×mult 1.0)→ venom_curb unlock 50 → 阈值 round(130×0.5)=65
func _boss(db, L: int, D: int, kind := "boss"):
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var ec = UnitContainerScript.new(db)
var ok: bool = ec.spawn({"level_index": L, "node_depth": D, "hero_hp": int(db.combat.hero.max_hp), "row_kind": kind})
Kit.check("前置:spawn (%d,%d) 受理" % [L, D], ok)
@@ -43,6 +45,7 @@ func _has_sid(arr: Array, sid: String) -> bool:
func _test_gate_predicate(db) -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:143 (门谓词:条件不满足不进可摇集合组)
var ec = _boss(db, 2, 9)
var u: Dictionary = _boss_u(ec)
var mhp: int = int(u.max_hp)
@@ -71,6 +74,7 @@ func _test_gate_predicate(db) -> void:
# 闩不可逆:解锁后治疗抬回满血 → 复查零新解锁、可摇集合不回退
func _test_latch_irreversible(db) -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:143 (闩型条件首次为真整场保持组)
var ec = _boss(db, 2, 9)
var u: Dictionary = _boss_u(ec)
ec.apply_damage(u, int(u.max_hp) / 2) # → 半血触发
@@ -82,6 +86,7 @@ func _test_latch_irreversible(db) -> void:
# 普通行无字段逐位零扰动:地精 (1,0) 池快照零 unlock 键、过滤恒空转、可摇=全池
func _test_plain_rows_zero_drift(db) -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:301 (普攻倍率 1 特例——零漂移组)
var ec = load("res://core/unit_container.gd").new(db)
Kit.check("零扰动前置:巨蜘蛛首遇带 spawn(池三行全无 trigger——快照键策略钉死载体)", ec.spawn({"level_index": 2, "node_depth": 1, "hero_hp": int(db.combat.hero.max_hp)}))
var u: Dictionary = ec.all_enemies()[0]
@@ -9,6 +9,7 @@ const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:143 (触发条件机制 V1–V10 组)
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
print("[trigger_cond_test] FAIL — 数值表加载失败")
@@ -15,6 +15,7 @@ var _db
func _init() -> void:
# oracle: design/gdd/systems/排雷循环/地形变异.md:151 变异布置宿主(装载组)
_db = ConfigDBScript.new()
# 表加载失败不中止(承 G2/G3/G4/G5 同款:表损坏的红落在具体断言上——
# 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评)
@@ -53,6 +54,7 @@ class _CountVariantRng extends RngServiceCore:
# 本段经 begin_battle 走完整初始化序后断言:门控一致性 / 布置域 / 数量公式 / 连通片上限 / 确定性。
func _smoke_variant_placement(L: int) -> void:
# oracle: design/gdd/systems/排雷循环/地形变异.md:63 (战内布置段门控组)
print("--- 地形变异 布置段(L%d)---" % L)
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(42)
@@ -146,6 +148,7 @@ func _variant_dump(b) -> String:
func _smoke_global_variant() -> void:
# oracle: design/gdd/systems/排雷循环/地形变异.md:74 (全局替换制与回落组)
print("--- 全局作用域变异(门控三维 + 全局池空兜底回落 + 资源面偏斜)---")
# ① 跨字段互锁负样本(resource_face ⟺ scope=global 三条 + effect_value 新域 [0,50])
var vt = load("res://data/schemas/variation_table.gd").new()
@@ -340,6 +343,7 @@ func _errs_has_cross_field(errs: Array) -> bool:
# 全称不可断)。可达判据=①静态(布置面零字面量下界)+②同种子改值产物互异(真实入口=begin_battle 布置段)。
func _test_pack_min_from_table() -> void:
# oracle: design/gdd/systems/排雷循环/地形变异.md:63 (连通片格数 [pack_min,pack_max] 读表组)
# ① 静态:布置面零字面量下界(randi_range(2, 退役——下界读 pack_min 表头单点)
var src := FileAccess.get_file_as_string("res://core/board_installer.gd")
Kit.check("布置面零字面量下界(randi_range(2, 退役、下界读表单点在位)〔架构整改 V10·静态〕",
@@ -21,6 +21,7 @@ func _init() -> void:
# ---- 类型闸断言(V7——project.godot warning 配置在位〔源码断言〕+审计器脚本在位;
# 审计器归零判定走 static_scan S9 闸机,此处兜配置面)----
func _smoke_type_gate() -> void:
# oracle: design/engineering/code-conventions.md:41 (封装边界——类型闸源码扫描组)
var pj: String = FileAccess.get_file_as_string("res://project.godot")
Kit.check("V7 类型闸 warning 配置在位(untyped_declaration=1 显形)〔迭代27 T5〕",
pj.contains("gdscript/warnings/untyped_declaration=1"))
@@ -31,6 +32,7 @@ func _smoke_type_gate() -> void:
# ---- 卫生件 grep 闸(V8——死代码/字符串 Callable/影子账残留零命中;源码扫描面)----
func _smoke_hygiene_gates() -> void:
# oracle: design/engineering/code-conventions.md:54 (模态覆盖层层级与登记纪律——卫生闸组)
var callables := 0
for d in ["res://core", "res://presentation", "res://data"]:
var dir := DirAccess.open(d)
@@ -18,6 +18,7 @@ const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇
func _init() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var db = ConfigDBScript.new()
var load_ok: bool = db.load_tables("res://data/tables", true)
Kit.check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0)
@@ -40,6 +41,8 @@ func _init() -> void:
# ---- B1 兑付配方词表:五配方/三目标规则/双结账政策——常量非空且互异(封闭枚举负空间) ----
func _test_damage_controller_vocabularies() -> void:
# oracle: design/engineering/architecture/architecture.md:89 (事件/事实/效果类型/算子四词表封闭——契约词表组)
# oracle: design/engineering/architecture/architecture.md:89 (事件/事实/效果类型/算子四词表封闭——契约词表组)
var kinds: Array = [
DamageControllerScript.KIND_HERO_ATTACK, DamageControllerScript.KIND_ENEMY_SKILL,
DamageControllerScript.KIND_BOARD_TYPED, DamageControllerScript.KIND_BOARD_BLAST,
@@ -69,6 +72,8 @@ func _test_damage_controller_vocabularies() -> void:
# ---- D2↔B1:掷点用途标签 ∈ RngService 封闭词表(EC12——跨系统字面量契约的成员性断言) ----
func _test_rng_tag_closure(db) -> void:
# oracle: design/engineering/architecture/architecture.md:90 (流 scope 封闭词表组)
# oracle: design/engineering/architecture/architecture.md:90 (流 scope 封闭词表组)
var stream_label: String = String(db.combat.settlement.damage_roll.armor_roll_stream)
Kit.check("settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)",
RngServiceScript.PURPOSE_TAGS.has(stream_label))
@@ -82,6 +87,8 @@ func _test_rng_tag_closure(db) -> void:
# ---- B1→D1 行协议:单据九栏缺省齐备 + 协议公共键形态 ----
func _test_ticket_nine_columns() -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:362 (单据九列协议组)
# oracle: design/gdd/systems/战斗循环/战斗结算.md:362 (单据九列协议组)
var ctrl = _bare_controller()
var t: Dictionary = ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "契约探针",
{"channel": "棋盘兑付", "base_value": 1, "target_rule": DamageControllerScript.TARGET_HERO, "ri": 0})
@@ -111,6 +118,8 @@ func _bare_controller():
# ---- B4→B3:增益授予布尔受理口径(受理/拒绝二元面 + 投影一致性) ----
func _test_status_boolean_acceptance(db) -> void:
# oracle: design/gdd/systems/战斗循环/增益状态.md:250 (受理布尔面组)
# oracle: design/gdd/systems/战斗循环/增益状态.md:250 (受理布尔面组)
var effects = StatusContainerScript.new(db)
# EC1 域闭:notify_battle_start 前授予 = false(受理面封闭)
var any_row: Dictionary = {}
@@ -137,6 +146,8 @@ func _test_status_boolean_acceptance(db) -> void:
# 个体键直传字面量("e1"/"e2"/"e3"——拼写约定单点在 StatusContainer.owner_key_for,测试面模拟授予路径逐只授)
func _test_status_individual_isolation(db) -> void:
# oracle: design/gdd/systems/战斗循环/增益状态.md:251 (按个体归属组)
# oracle: design/gdd/systems/战斗循环/增益状态.md:251 (按个体归属组)
var effects = StatusContainerScript.new(db)
effects.notify_battle_start()
var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0}
@@ -158,6 +169,8 @@ func _test_status_individual_isolation(db) -> void:
func _test_status_grant_all_per_unit(db) -> void:
# oracle: design/gdd/systems/战斗循环/增益状态.md:252 (授予即生效组)
# oracle: design/gdd/systems/战斗循环/增益状态.md:252 (授予即生效组)
var effects = StatusContainerScript.new(db)
effects.notify_battle_start()
var src := {"source_class": "mine_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0}
@@ -184,6 +197,8 @@ func _test_status_grant_all_per_unit(db) -> void:
func _test_status_death_clears(db) -> void:
# oracle: design/gdd/systems/战斗循环/增益状态.md:250 (战斗结束清空组)
# oracle: design/gdd/systems/战斗循环/增益状态.md:250 (战斗结束清空组)
var effects = StatusContainerScript.new(db)
effects.notify_battle_start()
var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0}
@@ -197,6 +212,8 @@ func _test_status_death_clears(db) -> void:
func _test_status_merge_per_unit(db) -> void:
# oracle: design/gdd/systems/战斗循环/增益状态.md:253 (只登记不去重组)
# oracle: design/gdd/systems/战斗循环/增益状态.md:253 (只登记不去重组)
var effects = StatusContainerScript.new(db)
effects.notify_battle_start()
var src := {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": 0}
@@ -219,6 +236,8 @@ func _test_status_merge_per_unit(db) -> void:
# ---- B4 结算链契约(断言补齐批——①增益状态结算链):衰减逐值/战斗结束全清/满层拒绝/独立并存/首授快照 ----
func _test_status_decay_and_clear(db) -> void:
# oracle: design/gdd/systems/战斗循环/增益状态.md:21 (先兑付后衰减组)
# oracle: design/gdd/systems/战斗循环/增益状态.md:21 (先兑付后衰减组)
var effects = StatusContainerScript.new(db)
effects.notify_battle_start()
var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0}
@@ -239,6 +258,8 @@ func _test_status_decay_and_clear(db) -> void:
func _test_status_stack_full_reject(db) -> void:
# oracle: design/gdd/systems/战斗循环/增益状态.md:255 (满层拒绝原子且留痕组)
# oracle: design/gdd/systems/战斗循环/增益状态.md:255 (满层拒绝原子且留痕组)
var effects = StatusContainerScript.new(db)
effects.notify_battle_start()
var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0}
@@ -256,6 +277,8 @@ func _test_status_stack_full_reject(db) -> void:
func _test_status_independent_branch(db) -> void:
# oracle: design/gdd/systems/战斗循环/增益状态.md:257 (合成不设上下限组)
# oracle: design/gdd/systems/战斗循环/增益状态.md:257 (合成不设上下限组)
var effects = StatusContainerScript.new(db)
effects.notify_battle_start()
var probe := {
@@ -280,6 +303,8 @@ func _test_status_independent_branch(db) -> void:
func _test_status_snapshot_first_grant(db) -> void:
# oracle: design/gdd/systems/战斗循环/增益状态.md:258 (快照抗重载组)
# oracle: design/gdd/systems/战斗循环/增益状态.md:258 (快照抗重载组)
var effects = StatusContainerScript.new(db)
effects.notify_battle_start()
var src_first := {"source_class": "seal_hit", "origin_cell": Vector2i(3, 4), "round_index": 1}
@@ -17,6 +17,7 @@ var _db
func _init() -> void:
# oracle: design/gdd/systems/冒险循环/冒险结构.md:281 冒险结构与专长宿主(装载组)
_db = ConfigDBScript.new()
# 表加载失败不中止(承 G2/G3/G4/G5 同款:表损坏的红落在具体断言上——
# 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评)
@@ -58,6 +59,7 @@ func _hits_kind(hits: Array, kind: String) -> Array:
# ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态)
func _smoke_adventure_api() -> void:
# oracle: design/gdd/systems/冒险循环/冒险结构.md:283 (冒险 API 组:开始受理/序列/回调)
print("--- 冒险结构 API 闸(重复开始拒绝 · 入口停点 · 放弃两态受理)---")
var adr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(42))
adr.set_hero_override(900, 400) # 首战速胜档(放弃的非战斗受理要走到选路态)
@@ -120,6 +122,7 @@ func _api_mark_node(_node: Dictionary, _info: Dictionary, seq: Array) -> void:
# ③ 冒险验收断言(映射 冒险结构.md 验收标准——表覆盖/满配冒险成功/种子复现/大奖励/败北线/
# 重复拒绝/GP 跨关延续/加算区同账/恢复数学/放弃)
func _smoke_adventure() -> void:
# oracle: design/gdd/systems/冒险循环/冒险结构.md:284 (冒险生命周期组:推进/末关/倒下/重开/生命账)
print("--- 冒险结构验收(三关序列 · 大奖励 · 摘要五件事 · 账本封闭)---")
var autoplay = load("res://sim/level_autoplay.gd")
# ① 表与覆盖义务(序列关数 = 行集关数;节点源 = 参数档在固定种子下的生成图,
@@ -460,6 +463,7 @@ func _smoke_adventure() -> void:
# ---- 专长冒烟(feats 四级 · 案D 池接线 · V3 订阅面逐件三列)----
func _smoke_feats() -> void:
# oracle: design/gdd/systems/冒险循环/奇物.md:465 (奇物授予与凭证保底组)
print("--- 专长重设计实装(feat_table 四级 · 专长带池接线 · 订阅面逐件)---")
# ① 现行表牌库构成:37 件全量入池——102 数值令牌+37 relic 令牌(四级 rarity 直映射)
Kit.check("feat_table 已加载(37 件=基础 27+迁入 2+完美保险 1+迭代 46 换图系七件——换代)", _db.crossrun.feats != null and (_db.crossrun.feats.rows as Array).size() == 37)
@@ -16,6 +16,7 @@ func _init() -> void:
func _smoke_zero_chain_rule() -> void:
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:230 (0 格连锁传播闭包——模拟玩家算子组)
var db = load("res://core/config_db.gd").new()
var load_ok: bool = db.load_tables("res://data/tables", true) and db.diagnostics_count("error") == 0
Kit.check("前置:表加载零 error", load_ok)
@@ -55,6 +55,7 @@ func _init() -> void:
# ① 缺省两参(真编排器):同种子两跑整轮驱动逐位(确定性自证——动作序列快照经 cell_revealed 事件流)
func _smoke_drive_round_contract() -> void:
# oracle: design/engineering/architecture/architecture.md:144 (效果端口件:收事件交结算取回命中批——驱动循环契约组)
var db = load("res://core/config_db.gd").new()
var load_ok: bool = db.load_tables("res://data/tables", true) and db.diagnostics_count("error") == 0
Kit.check("前置:表加载零 error", load_ok)
@@ -80,6 +81,7 @@ func _smoke_drive_round_contract() -> void:
# ② provider 形态:策略回调产出 dig/seal_place/refresh 各一——提交与日志回调四元组在案
func _provider_shapes() -> void:
# oracle: design/engineering/architecture/architecture.md:144 (端口应答束形状组)
var st := _StubSettlement.new()
var script := [ {"kind": "dig", "cell": Vector2i(1, 1)}, {"kind": "seal", "cell": Vector2i(2, 2)}, {"kind": "refresh"}, {"kind": "stop"} ]
var logs: Array = []
@@ -100,6 +102,7 @@ func _provider_shapes() -> void:
# ③ max_rejects:策略恒返被拒动作——第 3 拒后停手出环(循环次数=3)
func _max_rejects() -> void:
# oracle: design/engineering/architecture/architecture.md:144 (拒绝通道组)
var st := _StubSettlement.new()
st.minesweeper.accept = false
st.minesweeper.reject_cells = {Vector2i(9, 9): true}
@@ -119,6 +122,7 @@ func _max_rejects() -> void:
# ④ 合步 next:move 后相位变更即断弃 dig(桩 on_submit 钩子把 phase 推离 Active)
func _combined_step_next() -> void:
# oracle: design/engineering/architecture/architecture.md:144 (合并步进组)
var st := _StubSettlement.new()
st.minesweeper.on_submit = func(action: String) -> void:
if action == "move":
@@ -18,6 +18,7 @@ var _rng
func _init() -> void:
# oracle: design/gdd/systems/排雷循环/棋盘生成.md:219 棋盘生成宿主(装载组)
_db = ConfigDBScript.new()
# 表加载失败不中止(承 G2 同款:表损坏的红落在具体断言上——
# 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评)
@@ -51,6 +52,7 @@ func _mk_stream(stream_name: String, scope: int) -> RandomNumberGenerator:
# 公式 N×cell ≤ 可用高−12 必须计入 (N−1)×2px 网格间距,否则网格实高溢出被 clip_contents 裁掉;
# 现公式 = 计入间距 + 垂直预留 24px〔上 12/下 12——底部不贴 D 区边框〕)----
func _smoke_layout_cell_formula() -> void:
# oracle: design/gdd/systems/排雷循环/棋盘生成.md:75 (出生区边长表单点——格边长公式组)
print("--- 棋盘布局格边长公式(底部预留回归·四档板型)---")
var board_generator = load("res://core/board_generator.gd").new()
var board_view = load("res://presentation/views/board_view.gd").new()
@@ -80,6 +82,7 @@ func _smoke_layout_cell_formula() -> void:
# ---- 地图战斗资源·身份面单元冒烟(格面资源:roll 恰一次 / 雷安同池 / 幂等 / 清图批尾)----
func _smoke_reward_identity_unit() -> void:
# oracle: design/gdd/systems/战斗循环/地图战斗资源.md:233 (格面资源身份——三分出口与资源身份组)
print("--- 地图战斗资源·格面资源身份面 ---")
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(20260911)
@@ -141,6 +144,7 @@ class _BoardRowStub:
func _smoke_board_generator() -> void:
# oracle: design/gdd/systems/排雷循环/棋盘生成.md:221 (确定性复现+布雷纪律组)
print("--- 棋盘生成 ---")
var board_generator = load("res://core/board_generator.gd").new()
for L in [1, 2]:
@@ -235,6 +239,7 @@ func _board_dump(b) -> String:
# ---- 地图战斗资源·伤害量值区间冒烟(roll 域内 + 中点锚 + 变异实际流动 + 定值行不受扰)----
func _smoke_power_interval() -> void:
# oracle: design/gdd/systems/战斗循环/地图战斗资源.md:233 (资源量值区间——伤害量值区间组)
print("--- 地图战斗资源·伤害量值区间(上阶/下阶随机)---")
# ① 表侧静态锚(配置推导——改表不断锚):凡带 power_min/power_max 的行 = 区间行,
# 校验配对齐全(lo ≤ hi)+ power = floor 中点(行动经济 条款①期望当量锚);其余行必须零区间字段。
@@ -13,6 +13,7 @@ var _db: ConfigDb
func _init() -> void:
# oracle: design/gdd/systems/排雷循环/棋盘生成.md:219 棋盘生成(装载组)
_db = load("res://core/config_db.gd").new() as ConfigDb
if not _db.load_tables("res://data/tables", false):
Kit.check("数值表加载", false)
@@ -39,6 +40,7 @@ func _generate(L: int, D: int, seed0: int) -> BoardData:
func _test_generate_shape_and_mines() -> void:
# oracle: design/gdd/systems/排雷循环/棋盘生成.md:224 (布雷纪律组)
var b := _generate(1, 0, 71000)
if b == null:
Kit.check("生成返回棋盘", false)
@@ -59,6 +61,7 @@ func _test_generate_shape_and_mines() -> void:
# 目标雷数公式单点(d14):同参同值对拍原式 + 真实产图指标读数同源
func _test_target_mine_count_single_source() -> void:
# oracle: design/gdd/systems/排雷循环/棋盘生成.md:224 (雷数单源组)
var bad := 0
var checked := 0
for L in [1, 2, 3]:
@@ -89,6 +92,7 @@ func _test_target_mine_count_single_source() -> void:
func _test_digits_equal_neighbor_mine_count() -> void:
# oracle: design/gdd/systems/排雷循环/棋盘生成.md:223 (数字恒真组)
var b := _generate(1, 1, 72000)
if b == null:
Kit.check("生成返回棋盘(数字段)", false)
@@ -109,6 +113,7 @@ func _test_digits_equal_neighbor_mine_count() -> void:
func _test_same_seed_bit_identical() -> void:
# oracle: design/gdd/systems/排雷循环/棋盘生成.md:221 (同种子逐位复现组)
var a := _generate(2, 0, 73000)
var b := _generate(2, 0, 73000)
if a == null or b == null:
@@ -119,6 +124,7 @@ func _test_same_seed_bit_identical() -> void:
func _test_clearable_and_inferability_selfconsistent() -> void:
# oracle: design/gdd/systems/排雷循环/棋盘生成.md:226 (可清图与及格线自洽组)
var b := _generate(1, 0, 74000)
if b == null:
Kit.check("生成返回棋盘(可推理段)", false)
@@ -134,6 +140,7 @@ func _test_clearable_and_inferability_selfconsistent() -> void:
# ---- V8 出生区边长表单点(架构整改——spawn_edge 表头驱动布雷域基/几何/护栏全导出)----
func _test_spawn_edge_from_table() -> void:
# oracle: design/gdd/systems/排雷循环/棋盘生成.md:75 (出生区边长读表组)
# ① 纯函数对拍:edge=5 夹具 → target = round(d×(W×H−25))(25 格布雷域基可观测)
var p5 := {"w": 12, "h": 12, "density": 0.15, "spawn_edge": 5}
Kit.check("V8 纯函数:edge=5 布雷域基 25 格(target=round(d×(144−25)))〔架构整改 V8〕",
@@ -15,6 +15,7 @@ var _bp_path := "user://test_breakpoint_resume.json"
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:180 (断点域契约:七字段往返/续玩确定性/幂等/三结局清除组)
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
print("[breakpoint_resume_test] FAIL — 数值表加载失败")
@@ -13,6 +13,7 @@ const StatusTableScript := preload("res://data/schemas/status_effect_table.gd")
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:195 (跨表校验宿主——装载组)
_test_all_links_valid()
_test_dangling_skill_id()
_test_cast_side_mismatch()
@@ -46,6 +47,7 @@ func _cats(errs: Array) -> Array[String]:
# ---- 正向:技能池全对齿(普攻恰一行、buff 挂敌方侧)应零错,防校验误伤 ----
func _test_all_links_valid() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:195 (悬空引用类别:跨表链接全有效组)
var skills := _skill([
{"skill_id": "attack", "type": "basic"},
{"skill_id": "raise_morale", "type": "buff", "buff_ref": "st_strong"},
@@ -61,6 +63,7 @@ func _test_all_links_valid() -> void:
# ---- 负向:技能池引用表里不存在的 skill_id 必须报 ----
func _test_dangling_skill_id() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:195 (悬空引用类别:skill_id 悬空组)
var skills := _skill([{"skill_id": "attack", "type": "basic"}])
var enemies := _enemy([
{"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "goblin", "pool": [
@@ -73,6 +76,7 @@ func _test_dangling_skill_id() -> void:
# ---- 负向:buff 挂到主角侧状态(side 反向)必须报 ----
func _test_cast_side_mismatch() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:195 (side 反向校验组)
var skills := _skill([{"skill_id": "raise_morale", "type": "buff", "buff_ref": "st_hero_side"}])
var enemies := _enemy([
{"level_index": 1, "row_kind": "battle", "depth_band": 0, "enemy_id": "goblin", "pool": [
@@ -12,6 +12,7 @@ var _resolver: ConfigDbResolver
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:202 (解析器宿主——装载组)
_resolver = ResolverScript.new(
func(level, category, message) -> void: # 诊断通道签名按调用实参收(首参为表 id 字符串)
_diags.append("%s/%s/%s" % [str(level), str(category), str(message)]),
@@ -54,6 +55,7 @@ func _difficulty_table():
func _test_band_hit() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:202 (精确命中零警告组)
var hit: Array = _resolver.resolve_enemy_rowset(1, 4, "battle")
Kit.check("带命中取偏难一侧:请求深度 4 命中带 5(逐字不回带 0)〔fv1〕",
hit.size() == 1 and String((hit[0] as Dictionary).get("enemy_id", "")) == "giant_spider")
@@ -63,6 +65,7 @@ func _test_band_hit() -> void:
func _test_rowset_order_preserved() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:202 (行集寻址与行序保序组)
var rows: Array = _resolver.resolve_enemy_rowset(1, 0, "battle")
var ids: Array[String] = []
for r in rows:
@@ -72,6 +75,7 @@ func _test_rowset_order_preserved() -> void:
func _test_top_band_clamp() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:202 (超最大行走封顶取最大行组)
_diags.clear()
var rows: Array = _resolver.resolve_enemy_rowset(1, 99, "battle")
Kit.check("越顶带 clamp 取顶带行集(battle 顶带 5)〔fv3〕",
@@ -87,6 +91,7 @@ func _test_top_band_clamp() -> void:
func _test_row_kind_routing() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:202 (行类别路由组)
var battle_rows: Array = _resolver.resolve_enemy_rowset(1, 0, "battle")
var boss_rows: Array = _resolver.resolve_enemy_rowset(1, 0, "boss")
Kit.check("行类路由:同请求深度 battle 与 boss 命中不同键域〔fv4〕",
@@ -95,6 +100,7 @@ func _test_row_kind_routing() -> void:
func _test_request_oob() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:203 (越域致命中止组)
_diags.clear()
_resolver.resolve_enemy_rowset(0, 0, "battle")
Kit.check("关卡序号 <1 → fatal request_oob〔fv5〕", _has_fatal_oob())
@@ -114,6 +120,7 @@ func _has_fatal_oob() -> bool:
func _test_difficulty_clamp() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:202 (难度 clamp 组)
var m: Dictionary = _resolver.resolve_difficulty_modifiers(1, 0)
Kit.check("难度乘数越域被 clamp:hp 99 → 上限 10", absf(float(m.get("hp_mult", 0.0)) - 10.0) < 0.001)
Kit.check("难度乘数越域被 clamp:atk 0.01 → 下限 0.1", absf(float(m.get("atk_mult", 0.0)) - 0.1) < 0.001)
@@ -8,6 +8,7 @@ const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
# oracle: design/engineering/architecture/architecture.md:107 (CurioDeck:三选一/商店共享池、抽签、cap 库存——刷新付费组)
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
print("[curio_refresh_paid_test] FAIL — 数值表加载失败")
@@ -11,6 +11,7 @@ const EffectEngineScript = preload("res://core/effect_engine.gd")
func _init() -> void:
# oracle: design/engineering/architecture/architecture.md:89 (效果词表封闭——扩签封闭性组)
# ① 词表在册断言(扩签面:2 事件+8 事实+7 效果类型+1 算子)
Kit.check_quiet(EffectEngineScript.EVENTS.has("dig_stop") and EffectEngineScript.EVENTS.has("battle_won"),
"新事件在册:dig_stop/battle_won〔专长批 V1〕")
@@ -11,29 +11,34 @@ const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
# oracle: design/engineering/code-conventions.md:41 (封装边界:跨对象往来只走公开方法与属性——P2 词表单点扫描组)
_sweep_generator()
_sweep_production_face()
quit(Kit.finish("encapsulation_p2_sweep_test"))
func _src(path: String) -> String:
# oracle: design/engineering/code-conventions.md:41 (源码采样组)
var src := FileAccess.get_file_as_string(path)
Kit.check("扫描面非空前置:%s" % path, src.length() > 0)
return src
func _zero(pattern: String, src: String, label: String) -> void:
# oracle: design/engineering/code-conventions.md:41 (零命中扫描组)
var re := RegEx.create_from_string(pattern)
Kit.check("%s:%s 零命中〔架构P2〕" % [label, pattern], re.search(src) == null)
func _anchor(pattern: String, src: String, label: String) -> void:
# oracle: design/engineering/code-conventions.md:41 (正锚存在组)
var re := RegEx.create_from_string(pattern)
Kit.check("%s:%s 正锚在案〔架构P2〕" % [label, pattern], re.search(src) != null)
# ① V1 生成器词表定义删除与改指
func _sweep_generator() -> void:
# oracle: design/engineering/code-conventions.md:41 (生成器本地副本归零组)
var src := _src("res://core/node_map_generator.gd")
if src.length() == 0:
return
@@ -43,6 +48,7 @@ func _sweep_generator() -> void:
# ② V2 生产三目录定义唯一性(递归扫 .gd,恰好 1 处且落 level_table.gd)
func _sweep_production_face() -> void:
# oracle: design/engineering/code-conventions.md:41 (schema 唯一定点组)
var files: Array[String] = []
for dir_path in ["res://core", "res://data", "res://presentation"]:
_collect_gd(dir_path, files)
@@ -14,6 +14,7 @@ const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
# oracle: design/engineering/code-conventions.md:41 (封装边界——五处结构性零命中扫描组)
_sweep_settlement_segments()
_sweep_orchestrator_methods()
_sweep_breakpoint_pair()
@@ -25,6 +26,7 @@ func _init() -> void:
# 源文件读取+非空前置(返 "" 即已判红——调用侧跳过后续正则断言)
func _src(path: String) -> String:
# oracle: design/engineering/code-conventions.md:41 (源码采样组)
var src := FileAccess.get_file_as_string(path)
Kit.check("扫描面非空前置:%s" % path, src.length() > 0)
return src
@@ -32,18 +34,21 @@ func _src(path: String) -> String:
# 正则零命中断言(-pattern 意图写入断言文案——可 grep 定位)
func _zero(pattern: String, src: String, label: String, path: String) -> void:
# oracle: design/engineering/code-conventions.md:41 (零命中扫描组)
var re := RegEx.create_from_string(pattern)
Kit.check("%s:%s 零命中〔封装5.1〕" % [label, pattern], re.search(src) == null)
# 正锚在案断言(正则命中 ≥1——新公开面/新形态在用)
func _anchor(pattern: String, src: String, label: String, path: String) -> void:
# oracle: design/engineering/code-conventions.md:41 (正锚存在组)
var re := RegEx.create_from_string(pattern)
Kit.check("%s:%s 正锚在案〔封装5.1〕" % [label, pattern], re.search(src) != null)
# ① V2 settlement 七文件(束注入换代——对编排器引用归零)
func _sweep_settlement_segments() -> void:
# oracle: design/engineering/code-conventions.md:41 (结算段封装扫描组)
var classes := {
"res://core/settlement/grant_segment.gd": "GrantSegment",
"res://core/settlement/combat_segment.gd": "CombatSegment",
@@ -63,6 +68,7 @@ func _sweep_settlement_segments() -> void:
# ② V3 编排器搬家五方法零残留
func _sweep_orchestrator_methods() -> void:
# oracle: design/engineering/code-conventions.md:41 (编排器方法面扫描组)
var src := _src("res://core/battle_orchestrator.gd")
if src.length() == 0:
return
@@ -72,6 +78,7 @@ func _sweep_orchestrator_methods() -> void:
# ③ V4 断点域(adventure_runner → BreakpointStore 公开面)
func _sweep_breakpoint_pair() -> void:
# oracle: design/engineering/code-conventions.md:41 (断点对封装扫描组)
var src := _src("res://core/adventure_runner.gd")
if src.length() == 0:
return
@@ -81,6 +88,7 @@ func _sweep_breakpoint_pair() -> void:
# ④ V5 存档双向公开面(save_manager↔slot_store)
func _sweep_save_pair() -> void:
# oracle: design/engineering/code-conventions.md:41 (存档对封装扫描组)
var src_m := _src("res://core/save_manager.gd")
if src_m.length() > 0:
_zero("slots\\._", src_m, "save_manager 对槽位件私取归零", "res://core/save_manager.gd")
@@ -94,6 +102,7 @@ func _sweep_save_pair() -> void:
# ⑤ V6 效果宿主累积面(grant → BattleFxHost 公开累积方法)
func _sweep_fx_host_accumulators() -> void:
# oracle: design/engineering/code-conventions.md:41 (特效宿主累积面扫描组)
var src := _src("res://core/settlement/grant_segment.gd")
if src.length() == 0:
return
@@ -103,6 +112,7 @@ func _sweep_fx_host_accumulators() -> void:
# ⑥ V7 配置挂载面(config_db → 三 schema bind_resolver)
func _sweep_config_resolver() -> void:
# oracle: design/engineering/code-conventions.md:41 (配置挂载面扫描组)
var src := _src("res://core/config_db.gd")
if src.length() == 0:
return
@@ -16,6 +16,7 @@ func EffectEngineKindProbe(_db) -> bool:
func _init() -> void:
# oracle: design/gdd/systems/冒险循环/奇物.md:465 (奇物凭证五件与留任二件组)
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
print("[feat_vouchers_test] FAIL — 数值表加载失败")
+1
View File
@@ -14,6 +14,7 @@ const LevelRunnerScript := preload("res://core/level_runner.gd")
func _init() -> void:
# oracle: design/gdd/systems/冒险循环/冒险结构.md:288 (生命一致性:三写入口封顶 [0,生效上限]——主角账组)
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
print("[hero_ledger_test] FAIL — 数值表加载失败")
@@ -10,6 +10,7 @@ const LevelTableScript := preload("res://data/schemas/level_table.gd")
func _init() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:176 (信息迷雾类型可见性宿主——装载组)
var db = load("res://core/config_db.gd").new()
var loaded: bool = db.load_tables("res://data/tables", false)
Kit.check_quiet(loaded, "可见性用例:数值表加载成功")
@@ -102,6 +103,7 @@ func _runner(db, nodes: Array, anchor_id: int, candidates: Array, n: int = 2):
# ---- ⑥ N 进数据表(含越域校验)〔fv6〕----
func _test_field_in_table(db) -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:176 (迷雾参数读表——字段在表组)
var n_in_table: int = int(db.run.level.info_visible_layers)
Kit.check("N 落表:level.tres 读数 2(实际 %d)〔fv6〕" % n_in_table, n_in_table == 2)
var t := LevelTableScript.new()
@@ -125,6 +127,7 @@ func _has_n_domain_err(errs: Array) -> bool:
# ---- ① 邻近 N 层见类型(层距边界)〔fv1〕----
func _test_layer_distance(db) -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:176 (层距 N 可见性组)
var nodes := _graph_nodes()
var r = _runner(db, nodes, 1, [nodes[2]]) # 锚 = 节点 1(层 1),候选 = 节点 2
var vis: Array = r.type_visible_ids()
@@ -136,6 +139,7 @@ func _test_layer_distance(db) -> void:
# ---- ② 下一跳恒可见(跨层跳边候选)〔fv4〕----
func _test_candidate_exempt(db) -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:176 (候选豁免组)
var nodes := _graph_nodes()
var r = _runner(db, nodes, 6, [nodes[3]]) # 锚 = 节点 6(层 5),候选 = 节点 3(层 2,层距 3)
var vis: Array = r.type_visible_ids()
@@ -146,6 +150,7 @@ func _test_candidate_exempt(db) -> void:
# ---- ③ Boss/终点不入雾+⑤ 入口停点锚层 0〔fv3〕〔fv1〕----
func _test_boss_exempt(db) -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:176 (Boss 豁免组)
var nodes := _graph_nodes()
var r = _runner(db, nodes, -1, [nodes[0]]) # 入口停点(无当前节点,锚层 0)
var vis: Array = r.type_visible_ids()
@@ -158,6 +163,7 @@ func _test_boss_exempt(db) -> void:
# ---- ④ 单圈回雾(visited 不豁免)〔fv7〕----
func _test_single_circle(db) -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:176 (单圈可见组)
var nodes := _graph_nodes()
var r = _runner(db, nodes, 6, [nodes[7], nodes[3]])
r.visited.assign([nodes[0], nodes[1], nodes[2], nodes[3], nodes[4], nodes[5], nodes[6]])
@@ -170,6 +176,7 @@ func _test_single_circle(db) -> void:
# ---- ⑤ 单链退化形态(出度 ≤1 图,编号 = id 句柄)〔fv1〕----
func _test_single_chain_entry(db) -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:176 (单链入口组)
var nodes := _chain_nodes()
var r = _runner(db, nodes, -1, [nodes[0]])
var vis: Array = r.type_visible_ids()
@@ -182,6 +189,7 @@ func _test_single_chain_entry(db) -> void:
# ---- ⑥ N 读表:可见窗随表值随动〔fv6〕----
func _test_n_from_table(db) -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:176 (N 读表组)
var nodes := _graph_nodes()
var r = _runner(db, nodes, 1, [nodes[2]]) # 锚层 1
db.run.level.info_visible_layers = 1
@@ -199,6 +207,7 @@ func _test_n_from_table(db) -> void:
# ---- ⑦ 续玩重建同值〔fv7〕----
func _test_resume_rebuild(db) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var t := LevelTableScript.new()
t.info_visible_layers = 2
t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30}]
@@ -232,6 +232,7 @@ func _board_row_exact(level_index: int, depth: int) -> bool:
func _init() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
_db = ConfigDBScript.new()
# 表加载失败不中止(承 G2/G3/G4/G5 同款:表损坏的红落在具体断言上——
# 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评)
@@ -261,6 +262,7 @@ func _init() -> void:
# ---- 数值入表等价证明(d11/d16a——原字面量迁移:表值 == 迁移前字面量,消费端读表)----
func _smoke_table_migration() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:195 (行集纪律——表迁移组)
print("--- 数值入表(shop 两 knob + hero 显示名 · 原值迁移)---")
Kit.check("shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕", _db.run.shop.relic_base_price == 15.0)
Kit.check("hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕", _db.combat.hero.display_name == "探险者")
@@ -275,6 +277,7 @@ func _smoke_table_migration() -> void:
# 敌侧/板侧解析一律走 ConfigDB 真实消费链,禁直读行集自算)----
func _smoke_generated_coverage() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:184 (每终点前恰一店——生成覆盖扫描组)
var enemy_ok := true
var board_ok := true
var boss_band_ok := true
@@ -325,6 +328,7 @@ func _smoke_generated_coverage() -> void:
# ---- 关卡节点图·生成图推进冒烟(图 = 该关参数档 × 种子)----
func _smoke_level() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:178 (关卡图校验推进组)
print("--- 关卡节点图·生成图推进(参数档×种子产图·满配冒险成功/恢复公式/败北线/回放确定性)---")
var autoplay = load("res://sim/level_autoplay.gd")
var hero_base := {"atk": int(_db.combat.hero.atk), "hp": int(_db.combat.hero.max_hp), "ac": int(_db.combat.hero.armor_class)} # 配置基线快照——⑤ 回基线对比用(改表自动跟随,不再硬编码)
@@ -438,6 +442,7 @@ func _drive_to_rest(level_index: int, seed_val: int, gap: int) -> Dictionary:
# ---- 分支节点图冒烟:图合法性校验 + L2 幽暗地域生成图行进(图 = 参数档 × 种子)----
func _smoke_level_graph() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:178 (图结构与行进纪律组)
print("--- 关卡节点图·分支节点图(L2 幽暗地域·生成图)---")
var autoplay = load("res://sim/level_autoplay.gd")
# ① 关卡表行级校验零错误(关卡表只承载序号/显示名/恢复比例——图合法性由 validate_graph 在起关时承担)
@@ -795,6 +800,7 @@ func _fresh_rng(seed_val: int):
# ---- ① GP 冒险钱包·到账 ----
func _smoke_gp_wallet() -> void:
# oracle: design/gdd/systems/战斗循环/地图战斗资源.md:233 (GP 钱包入账组)
print("--- GP 冒险钱包·到账(战斗完成入账恰一次 + Boss 加值)---")
var autoplay = load("res://sim/level_autoplay.gd")
# ① L1 首战(普通战档——掷定 [2,4] + 关级加值 0):runner 驱动,收集 battle_finished 与钱包事件
@@ -860,6 +866,7 @@ func _escrow_sum_of(rows: Array) -> int:
# ---- ② escrow 隔离(GP安全格兑付 / 破财直验 / 钱包 clamp)----
func _smoke_gp_escrow() -> void:
# oracle: design/gdd/systems/战斗循环/地图战斗资源.md:233 (GP escrow 组)
print("--- GP escrow 隔离(GP安全格兑付入缓冲 · 破财不透支)---")
# ① 种子扫描:找GP 袋(gp 型安全格)可挖掘局——挖开兑付 +3 入 escrow,战斗末随 gp_dropped 一次上抛
var scan := {}
@@ -973,6 +980,7 @@ func _find_gp_dig(minesweeper, settlement) -> Dictionary:
# ---- ③ 商店房三收口 ----
func _smoke_shop_room() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:184 (商店房呈现组)
print("--- 商店房三收口(现价/加算区/余额不足拒/离开回选路/收束清零)---")
var autoplay = load("res://sim/level_autoplay.gd")
# 行进到商店房(按图型别定位商店节点——候选取最小句柄行进,到达即停):
@@ -1106,6 +1114,7 @@ func _shop_products_by_id(prods: Array) -> Dictionary:
# ---- ④ 商店编排性质(生成图扫描面——三条性质 + 终点跨表解析)----
func _smoke_shop_graph() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:184 (必经恰一店图纪律组)
print("--- 商店编排性质(每终点前一位恰一店 · 店数=终点数 · 全路径恰穿一店)---")
var pred_bad := 0
var term_total := 0
@@ -1176,6 +1185,7 @@ func _smoke_shop_graph() -> void:
# ---- ⑥ 大岔口分区域兑付(V3——多终点图走哪条路决定打谁,任一 Boss 胜即收束恰一次)----
func _smoke_multi_terminal_settlement() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:185 (多终局收束组)
print("--- 大岔口分区域兑付(L2 两轴各打各的终点)---")
var autoplay = load("res://sim/level_autoplay.gd")
var nodes: Array = _generated_graph(2, 42).get("nodes", [])
@@ -1243,6 +1253,7 @@ func _paths_all_one_shop(nodes: Array) -> bool:
# ---- ⑤ 商店共享池(三选一同构/乘法价/跨渠道共账/切边/断点扩键)----
func _smoke_shop_pool() -> void:
# oracle: design/engineering/architecture/architecture.md:107 (商店共享池组)
print("--- 商店共享池(抽3买1全重抽 · 乘法定价 ceil · 池空硬闸 · 切边零入店)---")
var autoplay = load("res://sim/level_autoplay.gd")
var shelf_kinds: Array = []
@@ -1405,6 +1416,7 @@ func _smoke_shop_pool() -> void:
# ---- 商店房 node_entered 载荷(尾波 V1——_enter_node pool/legacy 双发射路径归一后的行为保持断言)----
func _test_shop_node_entered_payload() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:184 (行进至此打开商店覆盖层——载荷组)
print("--- 商店房 node_entered 载荷(pool 含 shelf:true+gp 现值/legacy 无 shelf 键)---")
var autoplay = load("res://sim/level_autoplay.gd")
# pool 态:载荷字典经信号闭包当场取样(字典为引用累积器——lambda 按值捕获标量的绕行惯例)
@@ -1563,6 +1575,7 @@ func _has_err_category(errs: Array, category: String) -> bool:
# ---- 货架位数读表(架构整改——shelf_size 表头单点:进店与重抽同源)----
func _test_shelf_size_from_table() -> void:
# oracle: design/gdd/ux/shop.md:13 (货架位数读表组)
var autoplay = load("res://sim/level_autoplay.gd")
var st = _db.run.shop
var saved: int = int(st.shelf_size)
@@ -1584,6 +1597,7 @@ func _test_shelf_size_from_table() -> void:
# ---- 散折价收回规则键(架构整改 R2——shop.spend_discount 表字段退役,折价读 economy_spend.discount_pct)----
func _test_spend_discount_from_rule_key() -> void:
# oracle: design/gdd/systems/排雷循环/行动经济.md:256 (换图基座:挖格产 RAP 与刷新折扣组)
# 字段真删:shop.tres 加载零诊断 + spend_discount 属性不在位
Kit.check("shop.tres 加载零诊断(spend_discount 字段真删——schema 与表行同卸)〔架构整改 V2〕",
_db.diagnostics_count("error") == 0 and _db.run.shop.get("spend_discount") == null)
@@ -11,6 +11,7 @@ const LevelTableScript := preload("res://data/schemas/level_table.gd")
func _init() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:178 (关卡表消费侧校验宿主)
_test_positive()
_test_negative()
_test_scope_not_extended()
@@ -37,6 +38,7 @@ func _has(errs: Array, cat: String, needle: String = "") -> bool:
# ---- 正例(四检全过)----
func _test_positive() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:178 (良构基底正向组)
# 多入口多终点:两入口共汇入一岔口,两终点各在互异深度
var multi := [
_n(0, "battle", 0, [2]), _n(1, "battle", 0, [2]),
@@ -59,6 +61,7 @@ func _test_positive() -> void:
# ---- 负例九类(各报对应类别与判据消息)----
func _test_negative() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:178 (违规配置加载期致命——负向组)
var non_boss_end := [_n(0, "battle", 0, [1]), _n(1, "battle", 1, [])]
Kit.check("负例:终点非 Boss 命中「终点恒为 Boss」〔v1〕",
_has(_errs(non_boss_end), "structural", "终点 1 非 Boss"))
@@ -92,6 +95,7 @@ func _test_negative() -> void:
# ---- 「勿扩勿缩」反证:本检不含商店位与个数对参数档的核对 ----
func _test_scope_not_extended() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:178 (校验范围不外扩组)
# 合法形状但全程无商店/三个终点同入口图——四检不该因此报错(商店位归冒烟断言、个数归产出侧)
var no_shop := [_n(0, "battle", 0, [1, 2]), _n(1, "battle", 1, [3]), _n(2, "rest", 1, [3]), _n(3, "boss", 2, [])]
Kit.check("范围反证:无商店图不在加载校验内报错(商店位由冒烟断言把关)〔v1〕", _errs(no_shop).is_empty())
@@ -21,6 +21,7 @@ var _db
func _init() -> void:
# oracle: design/gdd/systems/跨局循环/元进度.md:148 元进度与锻造宿主(装载组)
_db = ConfigDBScript.new()
# 表加载失败不中止(承 G2/G3/G4/G5 同款:表损坏的红落在具体断言上——
# 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评)
@@ -132,6 +133,7 @@ func _auto_play_round(settlement, out: Dictionary) -> void:
# ---- 元进度 ②树购买面(购买受理/持久/余额不足拒绝/未知节点拒绝;
# 行价=表值 flat 单价)----
func _meta_pricing_and_buy() -> void:
# oracle: design/gdd/systems/跨局循环/天赋树.md:198 (单币可判:购买只扣钥石、余额不足拒绝零变化组)
print("--- 元进度·树购买面(隔离档 smoke_meta_buy/dry)---")
_meta_smoke_cleanup()
var sv = _meta_new_save(_META_SMOKE_PATHS[0])
@@ -158,6 +160,7 @@ func _meta_pricing_and_buy() -> void:
# ---- 元进度 ⑥Boss 两档 ⑦钥石不丢 ⑧工坊解锁 ⑩摘要副本与 cap(合成摘要驱动——settle 纯账面零跑局)----
func _meta_settle_and_workshop() -> void:
# oracle: design/gdd/systems/跨局循环/元进度.md:150 (钥石收束入账不丢组)
print("--- 元进度·收束入账与工坊(隔离档 smoke_meta_settle/workshop/cap)---")
var sv = _meta_new_save(_META_SMOKE_PATHS[2])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
@@ -350,6 +353,7 @@ func _meta_settle_and_workshop() -> void:
# ---- 元进度 ④注入可见 + 白板成立(验收 4/2/3——程序面;MC 逐位回归归 T7 全量验证)----
func _meta_injection() -> void:
# oracle: design/gdd/systems/跨局循环/天赋树.md:201 (注入可判:开局覆盖值分层可加组)
print("--- 元进度·注入可见与白板(隔离档 smoke_meta_white/dry)---")
var sv = _meta_new_save(_META_SMOKE_PATHS[1])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
@@ -393,6 +397,7 @@ func _meta_injection() -> void:
# ---- 锻造工坊·两根类型轴所有权移交(V5 前半:hero.tres 两轴删除+锻造配置默认值)----
# 后半(build_adventure_params 注入默认值/选中覆盖)随 T4 注入接线同批断言;写口/解锁面归 T3。
func _smoke_forge_axes() -> void:
# oracle: design/gdd/systems/跨局循环/锻造工坊.md:220 (锻造两轴单选注入与树零攻防组)
print("--- 锻造工坊·类型轴所有权移交(T2)---")
# V5:hero.tres 静态表两轴已删(全表加载零违例——config_db_test 同款闸;此处钉字段不在位)
Kit.check("hero.tres 无 attack_type/armor_type 字段(契约 2——静态表两轴删除)〔锻造工坊批 V5〕",
@@ -555,6 +560,7 @@ func _smoke_forge_axes() -> void:
# ---- 天赋树冒烟(v0.8 12 节点:schema/购买面/Fork 一节点双态/彩蛋特化)----
func _smoke_tree() -> void:
# oracle: design/gdd/systems/跨局循环/天赋树.md:194 (天赋树购买与六键合成组)
print("--- 天赋树(12 节点 · 购买面 · Fork 双态 · 彩蛋觉醒)---")
# 〔V1〕结构:12 行 + schema 零违例
Kit.check("meta_tree 已加载(12 节点=脊柱 2〔生命/命中淬炼一线一节点 cap4〕+锻造 2〔钥匙+彩蛋——彩蛋含在锻造 2 内非第 13 行〕+钥石 2〔秘宝匣/刷新升级〕+同调 6〔socket 3+Fork 3 双态〕——v0.8 终态)〔天赋树批 V1〕",
@@ -637,6 +643,7 @@ func _smoke_tree() -> void:
# ---- 天赋树六键行为冒烟(V10:开局姿态窗/危险直觉限次/财富之道聚散/守侧失误补偿;
# 引擎直构 + 编排授予段直注(_grant_seg._judge_seal=结算授予段封印判定单口))----
func _smoke_tree_rules() -> void:
# oracle: design/gdd/systems/跨局循环/天赋树.md:194 (规则键参数读表组)
print("--- 天赋树六键行为(V10:窗/限次/聚散/守侧)---")
# 开局姿态组:每关首轮窗(battle_index 事实——引擎直构 + 自建 ctx)
var fx = load("res://core/effect_engine.gd").new(null)
@@ -777,6 +784,7 @@ func _smoke_tree_rules() -> void:
# ---- 锻造契约补齐(断言补齐批——⑥锻造工坊):拒购零副作用/祝福件零泄漏/默认值解耦/甲型双轴 ----
func _test_forge_buy_reject() -> void:
# oracle: design/gdd/systems/跨局循环/锻造工坊.md:222 (钥石不足购买被拒、余额与等级零变化组)
var sv = _meta_new_save("user://smoke_meta_reject.json")
var mp = load("res://core/meta_progression.gd").new(_db, sv)
sv.meta_state._add_keystone(10) # 低于首购价 20——余额不足面
@@ -791,6 +799,7 @@ func _test_forge_buy_reject() -> void:
func _test_blessing_no_forge_leak() -> void:
# oracle: design/gdd/systems/跨局循环/锻造工坊.md:220 (树淬炼对攻击与 AC 零贡献组)
var sv = _meta_new_save("user://smoke_meta_blessing.json")
var mp = load("res://core/meta_progression.gd").new(_db, sv)
var fs0: Dictionary = sv.meta_state.forge_state().duplicate(true)
@@ -809,6 +818,7 @@ func _test_blessing_no_forge_leak() -> void:
func _test_default_axes_pre_key() -> void:
# oracle: design/gdd/systems/跨局循环/锻造工坊.md:217 (未点亮期间默认值照常注入组)
var sv = _meta_new_save("user://smoke_meta_axesw.json")
var mp = load("res://core/meta_progression.gd").new(_db, sv)
var ax: Dictionary = mp.hero_type_axes()
@@ -817,6 +827,7 @@ func _test_default_axes_pre_key() -> void:
func _test_armor_switch_dual_axes() -> void:
# oracle: design/gdd/systems/跨局循环/锻造工坊.md:221 (换装不丢级不生效——双轴组)
var sv = _meta_new_save("user://smoke_meta_axesw.json")
sv.meta_state._add_keystone(1000)
var mp = load("res://core/meta_progression.gd").new(_db, sv)
@@ -17,6 +17,7 @@ var _ae: ActionEconomy
func _init() -> void:
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:224 (排雷规则宿主——装载组)
_db = load("res://core/config_db.gd").new() as ConfigDb
if not _db.load_tables("res://data/tables", false):
Kit.check("数值表加载", false)
@@ -45,6 +46,7 @@ func _init() -> void:
func _test_install_invariants() -> void:
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:228 (装板四不变式组)
_rules.install_board(_board)
Kit.check("装板后 current_board 即所装棋盘", _rules.current_board() == _board)
var opened: int = _rules.revealed_cells().size()
@@ -68,6 +70,7 @@ func _unrevealed_safe() -> Vector2i:
func _test_dig_face_rules() -> void:
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:231 (挖/封印合法性与可辨拒绝组)
var revealed_first = _rules.revealed_cells()[0] as Vector2i
var out: Array = _rules.settle_batch([{"action": "dig", "cell": revealed_first}])
var o: Dictionary = (out[0] as Dictionary) if not out.is_empty() else {}
@@ -82,6 +85,7 @@ func _test_dig_face_rules() -> void:
func _test_reveal_advances() -> void:
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:230 (揭示推进与连锁组)
var c := _unrevealed_safe()
if c == Vector2i(-1, -1):
Kit.check("存在未揭示安全格(揭示推进前置)", false)
@@ -95,6 +99,7 @@ func _test_reveal_advances() -> void:
func _test_seal_place() -> void:
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:231 (重复封印拒绝组)
var c := _unrevealed_safe()
if c == Vector2i(-1, -1):
Kit.check("存在未揭示安全格(封印前置)", false)
@@ -107,6 +112,7 @@ func _test_seal_place() -> void:
func _test_reject_out_of_catalog() -> void:
# oracle: design/gdd/systems/排雷循环/排雷玩法.md:236 (目录外拒绝组)
var out: Array = _rules.settle_batch([{"action": "not_a_real_action", "cell": Vector2i(0, 0)}])
var o: Dictionary = (out[0] as Dictionary) if not out.is_empty() else {}
Kit.check("目录外意图被拒(不静默吞掉)——实际载荷 %s" % str(o).substr(0, 80),
@@ -116,6 +122,7 @@ func _test_reject_out_of_catalog() -> void:
# ---- V14 棋盘揭示预览只读口(架构整改——preview_reveal 与实挖揭示集同口径、零状态写入零事件)----
func _test_preview_reveal_readonly() -> void:
# oracle: design/engineering/architecture/architecture.md:103 (preview_reveal 只读预览口——零写入零事件组)
var events := 0
_rules.cell_revealed.connect(func(_evt): events += 1) # 零发射哨兵(preview 全程不触发揭示事件)
# 形态 A:封印在场——preview(未开格) 不越封印且零状态写入
@@ -16,6 +16,7 @@ var _gen
func _init() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
_db = ConfigDBScript.new()
var load_ok: bool = _db.load_tables("res://data/tables", true)
if not load_ok:
@@ -77,6 +78,7 @@ func _max_depth(g: Dictionary) -> int:
# ---- V1 产出合格:三档×种子扫描,自证零错误 ----
func _test_output_ok() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点图生成.md:129 (产出约束六条+结构铁律组)
var all_ok := true
var entry_exit_ok := true
for L in [1, 2, 3]:
@@ -94,6 +96,7 @@ func _test_output_ok() -> void:
# ---- V3 可复现:纯函数+counter 无关 ----
func _test_reproducible() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点图生成.md:134 (同种子同参数同图组)
var same := true
for L in [1, 2, 3]:
if _sig(_graph(L, 42)) != _sig(_graph(L, 42)):
@@ -124,6 +127,7 @@ func _test_reproducible() -> void:
# ---- V5 岔路取舍两条 ----
func _test_fork_rules() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点图生成.md:88 (岔路取舍组)
var next_hop_ok := true
var comp_ok := true
for L in [1, 2, 3]:
@@ -199,6 +203,7 @@ func _branch_types(g: Dictionary, head_id: int, sibling_outs: Array) -> Array:
# ---- V6 必经恰一店+归属锁定+多终点深度互异 ----
func _test_shop_and_attribution() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点图生成.md:48 (必经恰一店与归属锁定组)
var paths_ok := true
var lock_ok := true
var depth_ok := true
@@ -251,6 +256,7 @@ func _all_paths(g: Dictionary) -> Array:
# ---- V7 战斗保底 ----
func _test_battle_floor() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点图生成.md:129 (战斗保底组)
var ok := true
for L in [1, 2, 3]:
for s in range(42, 52):
@@ -262,6 +268,7 @@ func _test_battle_floor() -> void:
# ---- V9 含岔口与出口结构 ----
func _test_exit_structure() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点图生成.md:48 (出口结构与分支互斥组)
var fork_all := true
var l1_single := true
var big_fork_ok := true
@@ -308,6 +315,7 @@ func _terminals_reachable(g: Dictionary, id: int) -> int:
# ---- V9 Boss 出场门控(难度曲线与Boss——每区域恰一 Boss 终点)----
func _test_boss_exit_per_region() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点图生成.md:48 (每区域 Boss 出口组)
# 真实生成多区域图:全部终点 type=="boss"、boss 终点数==exit_count、深度=L−r(r∈[0,exit_count)
# ——每区域恰一 boss、深度互异即分属不同区域)
# 节点 depth 字段=入口起最长路径深(生成器校验轴);区域序号 r 的可观测形态=最深 boss(r=0)
@@ -343,6 +351,7 @@ func _test_boss_exit_per_region() -> void:
# ---- V10 RNG 单点(纯流面 counter 不变)----
func _test_rng_single_point() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点图生成.md:134 (RNG 单点流组)
var svc = RngScript.new()
svc.new_adventure(4242)
var before: int = svc.counter()
@@ -357,6 +366,7 @@ func _test_rng_single_point() -> void:
# ---- V1 违规夹具:各报对应错误类别 ----
func _test_negative_fixtures() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:198 (无解 fail-fast 夹具组)
# 夹具构造器:手写 nodes/entries/terminals
var dup := _fx([
{"id": 0, "type": "battle", "depth": 0, "next": [1]},
@@ -483,6 +493,7 @@ func _has_msg(errs: Array, needle: String) -> bool:
# ---- V6 节点图三参数表值驱动(架构整改——fork_third_way_pct/fill_rest_first_pct/width_max 入 level_gen_params)----
func _test_gen_params_table_driven() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点图生成.md:129 (参数档读表驱动组)
# ① 同种子真实表行产图逐位(表值驱动回归锚)
var g1 := _graph(2, 42)
var g2 := _graph(2, 42)
@@ -12,6 +12,7 @@ const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
if OS.get_cmdline_user_args().has("fatal-fixture"):
_trigger_fatal()
return
@@ -38,6 +39,7 @@ func _reading(gen, db, L: int) -> Dictionary:
# ---- V8 生长递增(三档×多种子逐档读数)----
func _test_growth(gen, db) -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点图生成.md:130 (生长递增组)
var steps_ok := true
var count_ok := true
for s in [42, 43, 44, 45, 46]:
@@ -61,6 +63,7 @@ func _test_growth(gen, db) -> void:
# ---- V4 参数无解判定(静态面:域/预算无解夹具必须被 check_params 拦下)----
func _test_fail_fast_static(gen, db) -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:198 (参数无解静态拒绝组)
Kit.check("生产三档参数档可行性判定零错误〔fv4〕",
NodeMapGenerator.check_params(db.run.level_gen_params.row_by_level_index(1)).is_empty()
and NodeMapGenerator.check_params(db.run.level_gen_params.row_by_level_index(3)).is_empty())
@@ -79,6 +82,7 @@ func _test_fail_fast_static(gen, db) -> void:
# ---- V4 致命中止真实通道(子进程退出码非零;零回退分支=件核)----
func _test_fatal_subprocess() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 (无解致命子进程组)
var bin := _engine_bin()
if bin == "":
Kit.check("未找到引擎二进制,致命子进程通道未跑(如实标注)〔fv4〕", false)
@@ -94,6 +98,7 @@ func _test_fatal_subprocess() -> void:
func _trigger_fatal() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var gen = GenScript.new(RngScript.new())
# 域内合法但深度预算无解:层深 6 放三出口(check_params 拦下 → 致命中止)
var unsolvable := {"layers": 6, "branch_density": 0.0, "battle_ratio": 0.5, "reward_ratio": 0.2,
@@ -13,6 +13,7 @@ func _init() -> void:
func _smoke_required_injection() -> void:
# oracle: design/engineering/code-conventions.md:40 (拆分交接:依赖构造期一次交付——日志器必填注入守卫组)
# 未注入(缺省 0):start_session 返回空串、零文件产出
var lg0 = load("res://presentation/playtest_logger.gd").new()
var p0: String = lg0.start_session(42)
+5
View File
@@ -9,6 +9,7 @@ const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
# oracle: design/engineering/architecture/architecture.md:107 (池结构案 D:候选抽取与池纪律组)
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
print("[pool_case_d_test] FAIL — 数值表加载失败")
@@ -65,6 +66,7 @@ func _tiers_of(cands: Array) -> Array:
func check_v7(a, db) -> void:
# oracle: design/engineering/architecture/architecture.md:107 (池案 D V7 组)
if not a.start_adventure(201):
Kit.check_quiet(false, "V7 前置:冒险启动")
return
@@ -110,6 +112,7 @@ func check_v7(a, db) -> void:
func check_v6(b, db) -> void:
# oracle: design/engineering/architecture/architecture.md:107 (池案 D V6 组)
if not b.start_adventure(202):
Kit.check_quiet(false, "V6 前置:冒险启动")
return
@@ -139,6 +142,7 @@ func check_v6(b, db) -> void:
func check_v12(m, db) -> void:
# oracle: design/engineering/architecture/architecture.md:107 (池案 D V12 组)
if not m.start_adventure(401):
Kit.check_quiet(false, "V12 前置:冒险启动")
return
@@ -157,6 +161,7 @@ func check_v12(m, db) -> void:
func check_v8(c, db) -> void:
# oracle: design/engineering/architecture/architecture.md:107 (池案 D V8 组)
if not c.start_adventure(203):
Kit.check_quiet(false, "V8 前置:冒险启动")
return
@@ -20,6 +20,7 @@ const TAG_OK := "board_gen" # 在册合法标签(PURPOSE_TAGS 含;wr7 必
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 (运行期违约通道——致命真的会死组)
var db = ConfigDBScript.new()
if not db.load_tables("res://data/tables", true):
push_error("[run_time_fatal_test] 表加载失败——夹具不可用(回归)")
@@ -44,6 +45,7 @@ func _init() -> void:
# 缺省形态(契约 2):不接组装点的协作件违约只落自身 push_error+assert,不外溢封印
func _default_form(db) -> bool:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var rng = RngScript.new()
rng.set_master_seed(3)
var oc = OrchestratorScript.new(db, rng)
@@ -68,6 +70,7 @@ func _default_form(db) -> bool:
# (棋盘生成半边:装板域拆件后生成器随 BoardInstaller 件内自建——组装点=引用束 fatal_cb 接线,
# 经新组装点构建后触发生成器违约,封印传导断言不变)
func _seal_propagation(db) -> bool:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var rng_a = RngScript.new()
rng_a.set_master_seed(5)
var oc_bg = OrchestratorScript.new(db, rng_a)
@@ -89,6 +92,7 @@ func _seal_propagation(db) -> bool:
# ②wr6(p6):未知用途标签 → handler 记录位为真(判据主体)+拿不到可用 RNG;合法标签对照另行验过
func _unknown_tag_channel() -> bool:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var rng_bad = RngScript.new()
rng_bad.set_master_seed(7)
if not _inject_handler(rng_bad, "wr6"):
@@ -107,6 +111,7 @@ func _unknown_tag_channel() -> bool:
# ③wr7(p7):未播种取数 → handler 记录位为真+拿不到可用 RNG;同标签播种对照另行验过
func _unseeded_channel() -> bool:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var rng_bad = RngScript.new() # 不播种(_master_seed 缺省 -1)
if not _inject_handler(rng_bad, "wr7"):
return false
@@ -124,6 +129,7 @@ func _unseeded_channel() -> bool:
# 成员在位前提检查(成员被移除时走干净退出 2,不落「赋值不存在的属性」式中途中断)
func _inject_handler(rng, label: String) -> bool:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
if not (rng.get("violation_handler") is Callable):
push_error("[run_time_fatal_test/%s] violation_handler 成员缺失(p6/p7 形态未实装——回归)" % label)
return false
@@ -22,6 +22,7 @@ func _init() -> void:
# ① 无参实例(autoload 实例化形态)=无激活态+默认合法空态(sv2 类级)
func _test_no_arg_instance_has_no_activation() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:61 (启动态无激活不读写组)
var sm := SaveManagerScript.new()
Kit.check_quiet(sm.slots.active_slot() == 0 and not sm.slots.has_write_target(),
"sv2 无参实例无激活态(路径空/激活号 0/写盘目标空)〔卡① sv2〕")
@@ -40,6 +41,7 @@ func _test_no_arg_instance_has_no_activation() -> void:
# 判别锚:空 _path 的退化写目标=裸 ".tmp"(落 res://,拆闸实验实测杂散落点)——
# 断言先清退化目标再核零新增,闸短路缺陷(拒绝口旁路、写盘真实发生)必红。
func _test_write_rejected_without_target() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:61 (无激活态写盘拒绝组)
var sm := SaveManagerScript.new()
var before := {}
for i in range(1, 4):
@@ -79,6 +81,7 @@ func _test_write_rejected_without_target() -> void:
# (生产结算链全段在位:mark_first_adventure_done→apply_adventure_summary→refresh_treasure_unlocks
# →write_to_disk;无激活态下落账内存、写盘拒绝、盘面零触碰——已知中间态的机检面)
func _test_settle_chain_write_rejected() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:61 (结算链写盘拒绝组)
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
Kit.check_quiet(false, "sv3 链测试前置:数值表加载失败〔卡① sv3〕")
@@ -101,6 +104,7 @@ func _test_settle_chain_write_rejected() -> void:
# ③ 越界槽号三原语拒绝+窥视越界返回不可读(原语边界 fail-safe)
func _test_out_of_range_slots_rejected() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:61 (三存档位域界——越域槽拒绝组)
var sm := SaveManagerScript.new()
sm.slots.attach_slot_template("user://smoke_boot_noact_%d.json")
Kit.check_quiet(not sm.slots.activate_slot(0) and not sm.slots.activate_slot(4)
@@ -19,6 +19,7 @@ var _db
func _init() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
_db = ConfigDBScript.new()
# 表加载失败不中止(承 G2–G5 同款:表损坏的红落在具体断言上)
var load_ok: bool = _db.load_tables("res://data/tables", true)
@@ -46,6 +47,7 @@ func _fresh_rng(seed_val: int):
# 损坏兜底/迁移链/域界干净/摘要 cap/摘要集成/生涯终身计数域 v6)。测试文件统一 user://smoke_save_*.json 隔离,
# 段首清场+段尾清理——绝不碰 user://save.json(红线)。
func _smoke_save() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:176 (存档域读写往返/迁移链/损坏兜底/域界组)
print("--- SaveManager 存档读写/迁移/兜底 ---")
var sm_script = load("res://core/save_manager.gd")
var paths := ["user://smoke_save_roundtrip.json", "user://smoke_save_fresh.json",
@@ -338,6 +340,7 @@ func _smoke_save() -> void:
# 测试文件统一 user://smoke_save_slot_%d.json 模板隔离(attach_slot_template 注入口——禁各自拼槽名),
# 段首清场+段尾清理——绝不碰 user://save.json 与真实槽 user://save_slot_N.json(红线)。
func _smoke_save_slots() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:61 (三槽位与摘要 cap——槽位域组)
print("--- SaveManager 三存档位承载(迭代 33 卡①)---")
var sm_script = load("res://core/save_manager.gd")
var slot_tpl := "user://smoke_save_slot_%d.json"
@@ -38,6 +38,7 @@ func _write_v7_keystone(path: String, keystone: int) -> void:
# ① 三槽往返:各槽写异值 → 写盘 → 新实例按位读回逐域相等;改槽 1 槽 2 字节不动(sv1)
func _test_three_slot_roundtrip_and_isolation() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:61 (三槽位独立文件隔离组)
var t := SaveManagerScript.new()
t.slots.attach_slot_template(TPL)
for i in range(1, 4):
@@ -65,6 +66,7 @@ func _test_three_slot_roundtrip_and_isolation() -> void:
# ② 高版本槽进档失败:拒绝+恢复切换前三态(路径/激活号/数据)+拒读槽零误写(sv9/sv10)
func _test_activate_failure_restores_previous_slot() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:61 (激活失败回滚组)
var t := SaveManagerScript.new()
t.slots.attach_slot_template(TPL)
_write_v7_keystone(TPL % 1, 311)
@@ -86,6 +88,7 @@ func _test_activate_failure_restores_previous_slot() -> void:
# ③ MetaProgression 宿主链切槽:引用同一性保持+写盘落当前槽(sv10——禁换实例先例)
func _test_metaprogression_reference_identity() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:176 (元进度引用同一性——往返一致组)
var t := SaveManagerScript.new()
t.slots.attach_slot_template(TPL)
_write_v7_keystone(TPL % 1, 533)
@@ -36,6 +36,7 @@ func _write_slot_json(path: String, content: String) -> void:
# ① 三槽异值逐槽三项正确+不激活不写内存(sv7)
func _test_peek_three_slots_distinct_values() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:174 (槽位摘要按槽窥视组)
var t := SaveManagerScript.new()
t.slots.attach_slot_template(TPL)
for i in range(1, 4):
@@ -57,6 +58,7 @@ func _test_peek_three_slots_distinct_values() -> void:
# ② 空槽零值合法态+不可读三态显式标记(sv7/契约 9)
func _test_peek_empty_and_unreadable_states() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:174 (坏槽三态组)
var t := SaveManagerScript.new()
t.slots.attach_slot_template(TPL)
DirAccess.remove_absolute(TPL % 3) # 清①段残留——空槽夹具
@@ -77,6 +79,7 @@ func _test_peek_empty_and_unreadable_states() -> void:
# ③ 窥视零副作用:字节+mtime 双比对不变+零 .corrupt.bak 改名(sv8——真实入口冒烟段同判据)
func _test_peek_zero_side_effect() -> void:
# oracle: design/gdd/systems/表现与支撑/存档序列化.md:174 (窥视零副作用组)
var t := SaveManagerScript.new()
t.slots.attach_slot_template(TPL)
_write_slot_json(TPL % 1, "corrupt bytes {{{{")
@@ -9,6 +9,7 @@ const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
# oracle: design/gdd/systems/战斗循环/战斗结算.md:362 (结算五段 Segment——五段全序组)
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
print("[settle_segments_test] FAIL — 数值表加载失败")
@@ -10,6 +10,7 @@ const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
# oracle: design/gdd/systems/跨局循环/秘宝.md:246 (秘宝 12 件逐件行为——效果行为组)
var db = load("res://core/config_db.gd").new()
if not db.load_tables("res://data/tables", false):
Kit.check_quiet(false, "数值表加载")
@@ -53,6 +53,7 @@ func _all_revealed(_c: Vector2i) -> bool:
func _init() -> void:
# oracle: design/gdd/systems/跨局循环/秘宝.md:246 (秘宝结构冻结与护栏校验——结构组)
# ① 授予归因+受理序+Used 不可逆
var inv = _new_inv()
var r1: Dictionary = inv.add_item("sapper_order", "safe_cell", Vector2i(3, 4), 2)
@@ -7,6 +7,7 @@ extends SceneTree
func _init() -> void:
# oracle: design/gdd/systems/跨局循环/天赋树.md:195 (等级阈值前置:schema 四拒一过+判定三态+投影两态组)
var failures := 0
var db = load("res://core/config_db.gd").new()
var mt = load("res://data/schemas/meta_tree_table.gd").new()
@@ -12,6 +12,7 @@ extends SceneTree
func _init() -> void:
# oracle: design/gdd/systems/跨局循环/天赋树.md:194 (结构封闭:六键合成行闭集校验组)
var eng = load("res://core/effect_engine.gd")
var tree_tbl = load("res://data/schemas/meta_tree_table.gd")
var db = load("res://core/config_db.gd").new()
@@ -88,6 +89,7 @@ func _init() -> void:
# ---- 规则键参数域校验(架构整改批 R1——六键参数字典):正例全过/缺参数拒/多参数拒/越域拒/非字典拒 ----
func _test_rule_key_params(eng, tree_tbl, db) -> Array:
# oracle: design/gdd/systems/跨局循环/天赋树.md:194 (规则键参数字典——参数名/取值域合规组)
var errs: Array = []
var params_tbl: Dictionary = tree_tbl.RULE_KEY_PARAMS
# 正例:meta_tree.tres 现行 Fork 行逐键过 validate(加载期已验——此处显式复核)
@@ -12,6 +12,7 @@ var _db
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:200 致命真的会死:正式通道加载进程非 0 退出(装载前提组)
_db = ConfigDBScript.new()
var load_ok: bool = _db.load_tables("res://data/tables", true)
if not load_ok:
@@ -23,6 +24,7 @@ func _init() -> void:
func _trigger_oob() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:204 越域请求在进入解析前致命中止(Fatal 级诊断+进程非 0,无兜底)(越域组)
# 共享 .tres 上的 resolver 归最后装载的实例——autoload ConfigDB 的 _ready 在测试 _init 之后
# 二次注入覆盖私有实例,故触发前把解析通道归还被测实例(诊断落 _db,退出码判据才成立)
_db.combat.board_gen._resolver = _db._resolver
@@ -9,6 +9,7 @@ const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:198 干净检出:表清单内全部表加载成功、就绪态断言(装载组)
var db = ConfigDBScript.new()
var load_ok: bool = db.load_tables("res://data/tables", true)
Kit.check("全部表加载 + 校验通过(含跨表护栏零 error 诊断)〔fv6〕", load_ok and db.diagnostics_count("error") == 0)
@@ -30,6 +31,7 @@ func _init() -> void:
# ---- 进度坐标 ceil 解析(R1:向上下取整保证偏难;越最大行 clamp 取最大值 + 警告去重)----
func _test_progress_resolution(db) -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:203 向上取整解析:请求无精确行返回不低于请求坐标的最近行(进度查行组)
var bg_rows: Array = db.combat.board_gen.rows
# 行键全集自表推导(禁写死关卡/深度数值)
var level_keys: Array = []
@@ -86,6 +88,7 @@ func _test_progress_resolution(db) -> void:
# ---- 变异池门控(unlock_level 闸 + ∅ board_types 通配 + 节点身份第三维)----
func _test_variant_pool_gate(db) -> void:
# oracle: design/gdd/systems/排雷循环/地形变异.md:153 候选池空合法:门控筛空时静默零布置(变异门控组)
var rows: Array = db.combat.variation.rows
var gated_row := {}
var wildcard_row := {}
@@ -128,6 +131,7 @@ func _rows_contain(pool: Array, row: Dictionary) -> bool:
# ---- ShopTable 消费面(行集解析单点——runner/编排禁自扫表的契约消费侧验证)----
func _test_shop_accessors(db) -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 校验类别全覆盖:行结构与引用闭合(商店访问器行结构组)
var products: Array = db.run.shop.product_rows()
if products.is_empty():
Kit.check("ShopTable products 行族非空(表已已入库前提)", false)
@@ -155,6 +159,7 @@ func _test_shop_accessors(db) -> void:
# ---- MetaTable 消费面 ----
func _test_meta_accessors(db) -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 校验类别全覆盖:行结构与引用闭合(元进度访问器行结构组)
# 无 forge_products 行族断言(行族/访问器不在表,V20 清单)
var key_rows: Array = db.crossrun.meta.boss_keystone
if not key_rows.is_empty():
@@ -174,6 +179,7 @@ func _test_meta_accessors(db) -> void:
# ---- TABLE_MANIFEST ↔ data/tables 目录同步(各系统→D2 单点契约的登记面)----
func _test_manifest_dir_sync(db) -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:198 表目录无清单外文件(清单对账组)
var manifest: Dictionary = db.TABLE_MANIFEST
var dir := DirAccess.open("res://data/tables")
var disk_files: Array = []
@@ -198,6 +204,7 @@ func _test_manifest_dir_sync(db) -> void:
# (rowset_rule——行序即契约,R6 加载期兜底)③行集实例总数 ≤4(骰后值运行面,spawn 守卫——冒烟侧验证)。
func _test_rowset_discipline() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 校验类别:重复键与缺锚行逐类断言(行集纪律组)
# 行集用 rows.assign([...]) 而非直接赋值:rows 是 Array[Dictionary] 定型属性,直接赋 untyped
# 数组字面量会触发「Invalid assignment」脚本错误——该错误不改退出码,却会**中断本函数剩余断言**
#(本函数五条纪律断言曾因此全部静默不跑,套件仍报 all green,是零覆盖假象)。
@@ -20,6 +20,7 @@ const PROBE_REF := ' {\n "feat_id": "ref_probe",\n "display_name": "引用探
func _inject(dir: String, probe: String) -> bool:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 校验类别全覆盖:前缀违规夹具注入(前缀夹具组)
var path := dir + "/feats.tres"
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
@@ -44,6 +45,7 @@ func _inject(dir: String, probe: String) -> bool:
func _assert_rejected(bad_dir: String, probe: String, label: String) -> bool:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:200 致命真的会死:违规表拒绝且进程非 0(拒绝判定组)
# 返回 true=正确拒绝;false=放行或夹具失败(调用方据此退出 2)
if not Kit.copy_tables(SRC_DIR, bad_dir) or not _inject(bad_dir, probe):
push_error("[%s] 夹具目录准备失败" % label)
@@ -39,6 +39,7 @@ const PROBE_TRADE_KEY := ' {
func _inject(dir: String, probe: String) -> bool:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 校验类别全覆盖:退役词夹具注入(退役词夹具组)
var path := dir + "/feats.tres"
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
@@ -63,6 +64,7 @@ func _inject(dir: String, probe: String) -> bool:
func _assert_rejected(bad_dir: String, probe: String, label: String) -> bool:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:200 致命真的会死:退役词加载致命中止(拒绝判定组)
# 返回 true=正确拒绝;false=放行或夹具失败(调用方据此退出 2)
if not Kit.copy_tables(SRC_DIR, bad_dir) or not _inject(bad_dir, probe):
push_error("[%s] 夹具目录准备失败" % label)
@@ -12,6 +12,7 @@ const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:198 锻造表装载与就绪态(装载组)
var db = ConfigDBScript.new()
var load_ok: bool = db.load_tables("res://data/tables", true)
Kit.check("生产 forge.tres 随全表加载 + validate 零违例〔锻造工坊批 V1〕", load_ok and db.diagnostics_count("error") == 0)
@@ -26,6 +27,7 @@ func _init() -> void:
# ---- 生产表六行正向(V1 生产面 + V11 行字段闭集)----
func _test_production_rows(db) -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 校验类别全覆盖:行 schema 正反逐类断言(生产行结构组)
var rows: Array = db.crossrun.forge.route_rows()
Kit.check("六路线行族规模恰 6(三武器+三甲)〔锻造工坊批 V1〕", rows.size() == 6)
var ids := []
@@ -89,6 +91,7 @@ func _test_production_rows(db) -> void:
# ---- 几何价格梯(V2 价格面单测半边——购买写在 T3 冒烟)----
func _test_price_ladder(db) -> void:
# oracle: design/gdd/systems/跨局循环/锻造工坊.md:220 定价结构:每级价格随已购级数几何递增、基价与公比读表单点(价格梯组)
var row: Dictionary = db.crossrun.forge.route("sword")
var prices := []
for owned in range(5):
@@ -147,6 +150,7 @@ func _mut_spear_initial(t) -> void:
func _test_negative_cases() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 校验类别全覆盖(负向 schema 组)
# 夹具本身须零违例(否则负向判读失效)
var t_ok = ForgeTableScript.new()
t_ok.routes = _base_rows()
@@ -215,6 +219,7 @@ func _test_negative_cases() -> void:
# 32→31:改写族清退批摘 spike(CI run 87 追绿——词表合法收窄的断言随动)。
func _test_effect_kinds_closed() -> void:
# oracle: design/engineering/architecture/architecture.md:89 事件/事实/效果类型/算子四词表封闭(特效闭集组)
Kit.check("效果引擎效果类型闭集 31 种(甲案词表退役 overflow/attack_segments+改写族清退摘 spike)〔锻造工坊批 V11〕",
EffectEngineScript.EFFECT_KINDS.size() == 31)
Kit.check("效果引擎事件词表在册 21 种(charge_full 位摘名——蓄力层删键批)〔锻造工坊批 V11〕",
@@ -11,6 +11,7 @@ const Kit := preload("res://tests/unit/test_kit.gd")
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:198 参数档表装载与就绪态(装载组)
var db = ConfigDBScript.new()
var load_ok: bool = db.load_tables("res://data/tables", true)
Kit.check("生产 level_gen_params.tres 随全表加载 + validate 零违例〔fv2〕",
@@ -42,6 +43,7 @@ func _table(rows: Array) -> Variant:
# ---- 生产三行正向(V2)----
func _test_production(db) -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点图生成.md:127 产出合格:任意参数档产出的图必过产出约束(生产档组)
var r1: Dictionary = db.run.level_gen_params.row_by_level_index(1)
var r2: Dictionary = db.run.level_gen_params.row_by_level_index(2)
var r3: Dictionary = db.run.level_gen_params.row_by_level_index(3)
@@ -59,6 +61,7 @@ func _test_production(db) -> void:
# ---- 访问器缺行防御 ----
func _test_accessor() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点图生成.md:127 参数档访问器读回与产出约束(访问器组)
var t = _table([_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, 4)])
Kit.check("row_by_level_index 缺行返回空字典〔fv2〕", t.row_by_level_index(9).is_empty())
@@ -66,6 +69,7 @@ func _test_accessor() -> void:
# ---- 域校验逐类负向(V2)----
func _test_negative() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 校验类别全覆盖:参数档 schema 拒绝(负向组)
# 合法基线三行
var base := [_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, 4),
_row(2, 9, 0.5, [0.5, 0.2, 0.15, 0.15], 2, 2, 6),
@@ -14,6 +14,7 @@ const BAD_DIR := "user://treasure_fixture_dangling"
func _inject_violation() -> bool:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 校验类别:悬空引用夹具(悬空引用组)
var path := BAD_DIR + "/reward.tres"
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
@@ -36,6 +37,7 @@ func _inject_violation() -> bool:
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:200 致命真的会死:悬空引用被拒绝、进程非 0(拒绝组)
if not Kit.copy_tables(SRC_DIR, BAD_DIR) or not _inject_violation():
push_error("[reward_dangling_treasure_test] 夹具目录准备失败")
quit(2)
@@ -12,6 +12,7 @@ const TEST_SEED := 20260928 # 夹具种子(任意固定值;语义 = 可复
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:201 黄金向量:登记向量逐个复算(装载组)
_test_derive_determinism()
_test_counter_ledger()
_test_stream_draw_determinism()
@@ -27,6 +28,7 @@ func _fresh_service():
# 单盐派生纯函数:两全新实例同种子逐位一致(derive_at 不推计数器——纯函数契约)
func _test_derive_determinism() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:201 黄金向量:随机派生纯函数对登记向量逐个复算相等、同输入恒同输出(派生确定性组)
var a = _fresh_service()
var b = _fresh_service()
var same := true
@@ -42,6 +44,7 @@ func _test_derive_determinism() -> void:
# 全局取数计数器:唯一账本——open_stream 逐次 +1(scope 单调)
func _test_counter_ledger() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:202 计数器纪律:连续取号跨模拟关卡切换单调不重置(计数账本组)
var rng = _fresh_service()
var c0: int = rng.counter()
rng.open_stream("armor_roll")
@@ -59,6 +62,7 @@ func _test_counter_ledger() -> void:
# 同种子两实例同标签流的实际取数序列逐位一致(回放确定性契约——存档快照=主种子+计数器的前提)
func _test_stream_draw_determinism() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:201 同输入恒同输出:流取号确定性(流取号组)
var faces := 20 # d20 探针面数(夹具值,非玩法表值——只用于产生可比较序列)
var a = _fresh_service()
var b = _fresh_service()
@@ -73,6 +77,7 @@ func _test_stream_draw_determinism() -> void:
# 负输入 abs 归一化(警告通道)——|−s| 与 s 同派生
func _test_negative_seed_normalization() -> void:
# oracle: design/engineering/architecture/architecture.md:90 RngService:master_seed 与各流 scope 计数器单点(种子规约组)
var pos = _fresh_service()
var neg = RngServiceScript.new()
neg.set_master_seed(-TEST_SEED)
@@ -22,6 +22,7 @@ func _init() -> void:
# 单元宿主改由 `_init` 显式调用,断言逐字未动;源 :70 事件日志落盘条经主会话裁决一
# 改判集成测试——不随本宿主,见迁移账本转办清单)----
func _smoke_initialize() -> void:
# oracle: design/engineering/architecture/architecture.md:90 RngService autoload:open_stream 有状态门面与启动初始化(启动流组)
print("=== Dungeon Sweeper 垂直切片冒烟 ===")
var db = ConfigDBScript.new()
var load_ok: bool = db.load_tables("res://data/tables", true)
@@ -29,6 +29,7 @@ var _db
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:198 表结构守卫装载与就绪态(装载组)
_db = ConfigDBScript.new()
# 表加载失败不中止(沿被迁断言面的源语义:表损坏的红落在具体断言上——
# 诊断在此展开,后续断言按坏表实况逐条验红;collect_only=true 下成员照常赋值可评)
@@ -91,6 +92,7 @@ func _first_encounter_band(level_index: int, enemy_id: String) -> int:
# ① 冒险表(单行全局 Rule 4 豁免——建表与接线闸;TP0 令牌池:配额结构/件值对/池总量域检)
func _smoke_adventure_table() -> void:
# oracle: design/gdd/systems/冒险循环/冒险结构.md:281 冒险结构表校验(冒险表组)
print("--- 冒险表(AdventureTable——令牌池行值单点:回血/配额/件值对/同调位)---")
Kit.check("db.adventure 已加载(adventure.tres 入 manifest)", _db.run.adventure != null)
if _db.run.adventure == null:
@@ -209,6 +211,7 @@ func _smoke_adventure_table() -> void:
# ---- 敌人表深度带键结构(迭代 34 建·L1 混编批换代:29 行键序=覆盖表序·(1,3) 混编行集·Boss 行集带键清单)----
func _smoke_enemy_bands() -> void:
# oracle: design/gdd/systems/冒险循环/难度曲线与Boss.md:316 难度与敌人带校验(敌人带组)
print("--- 敌人表深度带键结构(29 行键序 · (1,3) 混编 · Boss 带键覆盖表)---")
# ① 29 行计数 + 键序 = 覆盖表序(文件行序不变——行序即契约;(1,3) 两行相邻、熊地精在前)
# 迭代 35 生成图深度扩面覆盖批随动:战斗带顶扩到各关最大生成深度(L1→6/L2→9/L3→13)、
@@ -297,6 +300,7 @@ func _smoke_enemy_bands() -> void:
# ④ 掷点参数域:档位边界升序/档位值降序无零档/骰面与暴击参数在域(加载校验双保险)。
func _smoke_ac_swap_guard() -> void:
# oracle: design/gdd/systems/跨局循环/锻造工坊.md:219 切换语义:一次冒险=一个武器类型+一个甲型,冒险开始后锁死(AC 换装守卫组)
print("--- 护甲等级掷点批·换装护栏 ---")
# ① 三处无双轨
Kit.check("状态表 defense_up 行已退役(无双轨)", _db.combat.status_effects.row_by_id("defense_up").is_empty())
@@ -415,6 +419,7 @@ func _smoke_ac_swap_guard() -> void:
# ---- 地形变异 框架面(四行两轴×两关 + 门控 + 跨字段互锁)----
func _smoke_variation_framework() -> void:
# oracle: design/gdd/systems/排雷循环/地形变异.md:153 候选池门控与静默零布置(变异框架组)
print("--- 地形变异 框架面(首批四类型 + 候选池门控 + 跨字段互锁)---")
Kit.check("变异清单 6 行(荆棘丛/藤蔓缠绕/岩浆/烬覆 + 落石/矿尘——首批类型定稿批 + 第三关内容批;贫矿脉 2026-10-16 用户令屏蔽删行)",
_db.combat.variation.rows.size() == 6)
@@ -450,6 +455,7 @@ func _smoke_variation_framework() -> void:
# ---- 难度曲线与Boss DifficultyTable 结构位冒烟(AC-10:空表零行为 + 套乘点 = 行值快照前)----
func _smoke_difficulty_table() -> void:
# oracle: design/gdd/systems/冒险循环/难度曲线与Boss.md:316 难度修饰套乘(难度表组)
print("--- 难度曲线与Boss·DifficultyTable 结构位 ---")
Kit.check("DifficultyTable MVP 空表加载(结构位)", _db.run.difficulty != null and _db.run.difficulty.rows.is_empty())
# ① 空表零行为:敌人系统.spawn 敌值 = EnemyTable 行值原样
@@ -480,6 +486,7 @@ func _smoke_difficulty_table() -> void:
# ---- ⑤ 格面资源九型 + gp_drop 流(黄金向量)----
func _smoke_nine_types_gp_stream() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 校验类别全覆盖:九型 GP 流校验(九型流组)
print("--- 格面资源九型 + gp_drop 流(奖励字典 18 格 + 黄金向量)---")
Kit.check("格面资源十型封闭(第九型 gp 九型批 2026-09-20 / 第十型 treasure_item 秘宝内容批 2026-09-29——词表 v1.2)",
RewardTable.CELL_TYPES.size() == 10 and RewardTable.CELL_TYPES.has("gp") and RewardTable.CELL_TYPES.has("treasure_item"))
@@ -529,6 +536,7 @@ func _smoke_nine_types_gp_stream() -> void:
# ---- 元进度·MetaTable 表结构(行值全 provisional 归元进度设计文档配读数;
# conversion 覆盖恰三,Boss 两档 first ≥ repeat)----
func _smoke_meta() -> void:
# oracle: design/gdd/systems/跨局循环/元进度.md:148 元进度行族校验(meta 行族组)
print("--- 元进度·MetaTable 表结构 ---")
var meta_tbl = _db.crossrun.meta
Kit.check("MetaTable 非空 + validate 零错(三行族词表封闭/唯一/连续/域界/first≥repeat/ratio 全过)",
@@ -568,6 +576,7 @@ func _conversion_row_of(rows: Array, attr: String) -> Dictionary:
# ---- V7 节点图三新键域校验(架构整改——缺新键行拒/width_max 越域拒/两 pct 越域拒/表值全过)----
func _test_level_gen_new_fields() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var tbl = _db.run.level_gen_params
var mk := func(mod: Dictionary) -> Dictionary:
var row := {"level_index": 1, "layers": 6, "branch_density": 0.3, "battle_ratio": 0.5, "reward_ratio": 0.2,
@@ -604,6 +613,7 @@ func _test_level_gen_new_fields() -> void:
# ---- 变异表头域校验 / feat cap 必填(架构整改——丁案三件的数据面守卫)----
func _test_variation_header_domains() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var inst = load("res://data/schemas/variation_table.gd").new()
inst.percent_of_cells = 5
inst.per_level_bonus = 0
@@ -621,6 +631,7 @@ func _test_variation_header_domains() -> void:
func _test_feat_cap_required() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var inst = load("res://data/schemas/feat_table.gd").new()
# 生产表全行 cap 在场(加载期已验——此处显式复核 37 行)
var all_cap := true
@@ -15,6 +15,7 @@ const BAD_DIR := "user://treasure_fixture_target_violation"
func _inject_violation() -> bool:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:199 校验类别全覆盖:违规夹具逐类注入(真视窄域夹具组)
var path := BAD_DIR + "/treasure_items.tres"
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
@@ -39,6 +40,7 @@ func _inject_violation() -> bool:
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/数据配置.md:200 致命真的会死:违规表被拒绝、进程非 0(违规拒绝组)
if not Kit.copy_tables(SRC_DIR, BAD_DIR) or not _inject_violation():
push_error("[treasure_target_violation_test] 夹具目录准备失败")
quit(2)
+1
View File
@@ -10,6 +10,7 @@ extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:check/汇总退出样板单点
func _init() -> void:
# oracle: design/engineering/test-conventions.md:37 (套件协议模板——执行载体与退出码范例组)
Kit.check("headless SceneTree 协议可用(quit(code) 通道)", _test_headless_protocol())
Kit.check("命名规范可执行:test_[scenario]_[expected]", _test_naming_convention_followed())
quit(Kit.finish("example_test"))
@@ -18,6 +18,7 @@ class _BossRunner:
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:260 (战斗叙述宿主——装载组)
_test_numeric_lines_carry_values()
_test_end_text_differs_by_result()
_test_phase_text_nonempty()
@@ -28,6 +29,7 @@ func _init() -> void:
func _test_numeric_lines_carry_values() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:272 (结算行数值带入组)
var rest: String = NarratorScript.rest_line(12, 30, 40)
Kit.check("休整叙述带入回复量与当前血量(实际「%s」)" % rest,
rest.contains("12") and rest.contains("30") and rest.contains("40"))
@@ -38,6 +40,7 @@ func _test_numeric_lines_carry_values() -> void:
func _test_end_text_differs_by_result() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:272 (胜负互异文案组)
# 关卡收束(level_end=true)恒为「关卡完成!」——单场胜负文案只在非收束态出现
var level: String = NarratorScript.battle_end_text("lose", true)
Kit.check("关卡收束态恒为「关卡完成!」(实际「%s」)" % level, level == "关卡完成!")
@@ -50,6 +53,7 @@ func _test_end_text_differs_by_result() -> void:
func _test_phase_text_nonempty() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:272 (阶段行非空组)
var t: String = NarratorScript.phase_text(1, "", false, false, "")
Kit.check("相位叙述非空(实际「%s」)" % t, t != "")
var boss: String = NarratorScript.phase_text(2, "", false, true, "灰矮人石君王")
@@ -59,6 +63,7 @@ func _test_phase_text_nonempty() -> void:
# 叙述寻址按行类取 Boss 行集(S 清单 S15 换代):Boss 节点名必须来自 (关卡, boss, 带) 行集,
# 不能落到同深度的战斗行集——期望名由表行集首行读出,不写死字面量。
func _test_boss_name_uses_boss_rowset() -> void:
# oracle: design/gdd/systems/战斗循环/敌人系统.md:295 (Boss 名读行集组)
var db = load("res://core/config_db.gd").new()
var loaded: bool = db.load_tables("res://data/tables", false)
Kit.check_quiet(loaded, "叙述寻址用例:数值表加载成功")
@@ -87,6 +92,7 @@ func _test_boss_name_uses_boss_rowset() -> void:
# 关卡完成停点文案族措辞钉子(口径换代批):回血行/满血合法态/渠道句/摘要关数行——
# 旧措辞退役后玩家可见面由本组断言钉在新措辞上(撤销证明:回退 battle_narrator 对应行→断言红→复原复绿)
func _test_level_clear_wording() -> void:
# oracle: design/gdd/ux/level-clear.md:11 (关卡结算屏文案组)
var healed: String = NarratorScript.curio_pick_header({"level_index": 1, "heal_gained": 5, "hp_before": 10, "hp_after": 15})
Kit.check("关卡完成回血行带入回血量与生命区间(实际「%s」)〔口径换代 V1〕" % healed,
healed.contains("关卡完成回血 +5(生命 10 → 15)"))
@@ -106,6 +112,7 @@ func _test_level_clear_wording() -> void:
# 真实入口=呈现状态快照函数 presentation_snapshot:A 有削弱 B 无 → 两卡投影各现各的(零共享引用)
func _test_snapshot_effects_per_unit() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var db = load("res://core/config_db.gd").new()
Kit.check("V8 前置:表加载零 error", db.load_tables("res://data/tables", true) and db.diagnostics_count("error") == 0)
var rng = load("res://core/rng_service.gd").new()
@@ -10,10 +10,12 @@ const _EXPECTED_KEYS: Array[StringName] = [&"gate", &"forge", &"dummy", &"chest"
func _initialize() -> void:
# oracle: design/gdd/ux/camp.md:29 (营地场景布局数据件——纵深与地平线组)
call_deferred("_test_layout_hotspot_geometry")
func _test_layout_hotspot_geometry() -> void:
# oracle: design/gdd/ux/camp.md:16 (七物件命中区几何红线组)
var table: CampSceneLayoutTable = load("res://data/tables/camp_scene_layout.tres")
Kit.check("V14 布局表加载且 schema 类在位〔营地图批 V14〕", table is CampSceneLayoutTable and table != null)
if table == null:
@@ -10,6 +10,7 @@ const _TEST_SAVE := "user://test_forge_workbench.json"
func _initialize() -> void:
# oracle: design/gdd/systems/跨局循环/锻造工坊.md:219 (工作间两区六路线——装载组)
call_deferred("_run")
@@ -21,6 +22,7 @@ func _row_by_id(ov) -> Dictionary:
func _run() -> void:
# oracle: design/gdd/systems/跨局循环/锻造工坊.md:219 (六路线 UI 直验组)
var db = load("res://core/config_db.gd").new()
var load_ok: bool = db.load_tables("res://data/tables", true)
if not load_ok:
@@ -9,6 +9,7 @@ const HudScript := preload("res://presentation/hud_presenter.gd")
func _init() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:260 (HUD 投影宿主——装载组)
_test_accepted_text()
_test_reject_text_by_reason()
_test_unknown_reason_fallback()
@@ -16,11 +17,13 @@ func _init() -> void:
func _test_accepted_text() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:212 (受理反馈呈现组)
var t: String = HudScript.outcome_text({"accepted": true})
Kit.check("受理态文案固定为「执行完成。」(实际「%s」)" % t, t == "执行完成。")
func _test_reject_text_by_reason() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:212 (拒绝三源可辨文案组)
var cases := {
"not_adjacent": "不能执行:目标与小人不相邻",
"target_revealed": "不能执行:目标已开",
@@ -39,6 +42,7 @@ func _test_reject_text_by_reason() -> void:
func _test_unknown_reason_fallback() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:212 (未知兜底文案组)
var t: String = HudScript.outcome_text({"accepted": false, "reject_reason": "brand_new_reason"})
Kit.check("未知拒绝原因兜底为「动作被拒。」而非空串(实际「%s」)" % t, t == "动作被拒。")
var sub: String = HudScript.outcome_text({
@@ -11,10 +11,12 @@ const ScreenScript := preload("res://presentation/views/level_map_screen.gd")
func _initialize() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:178 (信息迷雾呈现宿主——装载组)
call_deferred("_run")
func _run() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:178 (迷雾显隐宿主组)
var db = load("res://core/config_db.gd").new()
var loaded: bool = db.load_tables("res://data/tables", false)
Kit.check_quiet(loaded, "迷雾呈现用例:数值表加载成功")
@@ -46,6 +48,7 @@ func _runner(db, nodes: Array, candidates: Array, n: int = 2):
# ---- ① 范围外只见结构:雾下节点类型图标与文字均不绘制〔fv2〕----
func _test_fog_hides_type() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:178 (雾下类型隐藏组)
var node := {"id": 3, "type": "battle", "depth": 3, "next": []}
var shown: Dictionary = ScreenScript.node_display(node, 2, [2], false, false)
Kit.check("可见集内节点:图标/类型文字均绘制〔fv2〕",
@@ -59,6 +62,7 @@ func _test_fog_hides_type() -> void:
# ---- ② Boss/终点不入雾+多终点标记〔fv3〕----
func _test_boss_exempt_and_multi_boss() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:178 (Boss 描边豁免与多 Boss 组)
var type_vis: Array = [1, 7, 9] # 双 Boss 图:7/9 均在可见集
var boss_a: Dictionary = ScreenScript.node_display({"id": 7, "type": "boss", "depth": 7, "next": []}, 7, type_vis, false, false)
var boss_b: Dictionary = ScreenScript.node_display({"id": 9, "type": "boss", "depth": 9, "next": []}, 9, type_vis, false, false)
@@ -76,6 +80,7 @@ func _test_boss_exempt_and_multi_boss() -> void:
# ---- ③ 节点内详情图上从不显示〔fv5〕----
func _test_details_never_shown() -> void:
# oracle: design/gdd/systems/冒险循环/关卡节点地图.md:178 (详情零显示组)
var plain := {"id": 3, "type": "battle", "depth": 3, "next": [4]}
var loaded := {"id": 3, "type": "battle", "depth": 3, "next": [4], "enemy_id": "goblin_secret", "reward_id": "reward_secret"}
var a: Dictionary = ScreenScript.node_display(plain, 3, [3], false, true)
@@ -92,6 +97,7 @@ func _test_details_never_shown() -> void:
# ---- ④ label_text 三态文案〔fv2〕----
func _test_label_three_states() -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var node := {"id": 3, "type": "battle", "depth": 3, "next": [4]}
Kit.check("当前位文案=「当前位置」〔fv2〕",
String(ScreenScript.node_display(node, 3, [3], true, false)["label_text"]) == "当前位置")
@@ -144,6 +150,7 @@ func _big_graph_nodes() -> Array:
func _test_big_graph_fit(db) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var nodes := _big_graph_nodes()
var entry: Dictionary = nodes[0]
var runner = _runner(db, nodes, [nodes[1]])
@@ -197,6 +204,7 @@ func _test_big_graph_fit(db) -> void:
# 命中读数 = is_same 同实例判别(重算必产新字典、命中返回缓存原实例)——零生产计数器。
func _test_layout_cache(db) -> void:
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
var nodes := _big_graph_nodes()
var runner = _runner(db, nodes, [nodes[1]])
var screen = ScreenScript.new()
@@ -11,6 +11,7 @@ const ModalStackScript := preload("res://presentation/modal_stack.gd")
func _init() -> void:
# oracle: design/engineering/code-conventions.md:54 (模态编排纯逻辑——层级与输入锁组)
# 哑层与桩 Callable(纯逻辑类零场景前提的证明面)
var start := Control.new()
var inter := Control.new()
Loaded 100 of 129 files, more files were not shown because too many files have changed in this diff. Show more