Compare commits

..
65 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 c1acb47c92 合并(合回前置检查钩子批): pre-merge-commit钩子落地——主区merge --no-ff落合并提交前自动跑run_all_gates.sh --premerge(冒烟+单元与集成全套件4分片约2分钟,退出码聚合即判定,不落回执不比基线); 红态拦合并提交不拦合并动作(修红续合或merge --abort); 快进合并不触发,--no-verify仅属用户; 规则随动五处在案(AGENTS/process-conventions×4处/test-conventions); 本批凭据: worktree侧--premerge全绿+--gates绿(闸壳13件)+--docs绿,本合并即钩子首次实弹(绿读数见本合并输出)
ci / gates (push) Successful in 35s
2026-11-06 04:10:41 +08:00
DKChan adfe15db08 工程+文档(合回前置检查钩子批): pre-merge-commit钩子落地(形态A薄调用)——run_all_gates.sh加--premerge档(冒烟+单元与集成全套件4分片,退出码聚合即判定,不落回执不比基线;选闸推导/静态扫描/熵增/DIRTY_GAME四段档外跳过;闸壳自检收件面改hooks全量无后缀件find式)+tools/hooks/pre-merge-commit(主区guard同post-merge款,红态输出处置句:修红git commit续合或merge --abort;快进合并不触发;--no-verify仅属用户); 规则随动五处: AGENTS运行与验证首条+车道铁律句/process-conventions §1第2条收口序+第4条③执法分层+§3第8条拦截面定位+第9条重跑语义三类/test-conventions §4时机; 本步凭据: --premerge档全绿(2分08秒,冒烟15项0失败+82套件4分片exit全0)+--gates绿(闸壳自检13件含新钩子,失败0)+--docs绿(规约随动全过基线比对) 2026-11-06 04:10:31 +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 336e66163a 合并(行尾纪律钉死批): 全仓文本LF钉死(.gitattributes扩面)——修CRLF环境红: core.autocrlf=true把工作区检出成CRLF,行尾进入settlement_wiring_snapshot_test内嵌多行golden基线,逐位比对在本机必红(Mac CI绿run88在案); 检出层钉LF治本,字节级证据面(逐位对拍/MC对拍/金图)跨机复现; 本批凭据: 冒烟15项0失败PASS+单元82套件4分片全绿(1分58秒)+窄测绿,worktree分支尖c17a5430
ci / gates (push) Successful in 34s
2026-11-06 03:22:02 +08:00
DKChan c17a5430f6 工程(行尾纪律钉死批): .gitattributes扩面——全仓文本LF钉死(* text=auto eol=lf+二进制显式豁免); 根因=core.autocrlf=true把工作区检出成CRLF,行尾进入内嵌多行golden基线数据面(settlement_wiring_snapshot_test的GOLDEN raw string),运行时JSON.stringify只发\n,逐位比对在本机必红而Mac CI绿(run 88在案); 修法=检出层钉LF,Windows/Mac两平台检出一致,治本面=未来一切内嵌字符串基线; 本步凭据: 冒烟15项0失败PASS(2.7秒)+单元82套件4分片全绿(1分58秒,四片exit=0,零失败零SCRIPT ERROR零Parse Error)+settlement套件窄测绿(文件翻LF后) 2026-11-06 03:21:54 +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 1730a8fbda 合并(普查条状态机合规追红): 普查条待执行状态残留缺口属性摘除,遗留待裁句并回实况——普查批合入带入的第4处流水线闸红,赶在推送前清零; 本步凭据: 流水线闸PASS(工作区分支尖03f0f1aa)
ci / gates (push) Successful in 7m4s
2026-11-06 01:18:02 +08:00
DKChan 03f0f1aa0d 收尾(追红修复·普查条状态机合规): 「效果通道与事件词的零现役普查」条状态待执行不得残留缺口属性——遗留待裁句并回实况(流水线闸run86起第4红,普查批合入带入,本批合回前清零); 本步凭据: 流水线闸PASS 2026-11-06 01:17:48 +08:00
DKChan ec1c0da1ff 合并(迭代53批后收尾+CI追绿修复): 收尾批=反哺点落文(用户四点裁决在案:3按方案/7-9照落/4裁入数据表行/5登记待办)——反哺点3 GDD退役名清退(术语表v1.27+地图战斗资源v1.6掷格件两路投影换机制名订阅加权+00-游戏概念删旧六行专名,退役记录句保留)+反哺点4规范句落战斗结算v1.8(明细文案=数据面字段读表拼行)+反哺点7 golden台账补lmap冻结日+反哺点8能力行名实回表+反哺点9探针直读受控例外注记; 未决迁账面(结算明细文案入表条目待文档设计/闸机超时守卫缺陷与mac两Shot基线登记待执行); 体验验证清单52节两目检项; 吸收即死退役五件(反哺点清单/实现报告/验收审计/两需求卡片); roadmap迭代53行已完成+当前位换代(迭代1–53)。追绿批=run87三红修复: forge_table_test效果类型闭集32→31(卡②摘spike断言随动)+level_map_info_fog_test布局缓存换代断言第三判据等强换键(位置差前提随画布同帧收敛失义,失效重算正常,换键=重算字典键数恰为新图节点数)+capability_audit真实入口锚面补probe_funcs(探针共用面,不属放宽)+level_map_shot_probe.gd.uid边车补入库+待办清单QA条目补仓库内证据句(修run86起流水线闸红)。验证: 两修复套件窄测绿(forge42/fog21全绿)+流水线闸PASS(27条目)+能力审计红项0+--docs全绿回执909a9709。post-merge钩子推送触发远端CI
# Conflicts:
#	待办清单.md
2026-11-06 01:16:02 +08:00
DKChan 909a9709b4 收尾(迭代53批后收尾)+CI追绿修复: 反哺点落文(用户四点裁决在案)——反哺点3 GDD退役名清退(术语表v1.27/地图战斗资源v1.6掷格件两路投影换机制名订阅加权、00-游戏概念删旧六行专名、奇物与03-内容规模退役记录句保留且后者删过时尾句)+反哺点4规范句落战斗结算v1.8(明细文案=数据面字段读表拼行,用户裁入表)+反哺点7 golden台账补lmap冻结日+反哺点8能力行名实回表_and_columns+反哺点9 test-conventions探针直读受控例外注记; 未决迁账面(反哺点4新立结算明细文案入表条目待文档设计/反哺点5闸机超时守卫缺陷与反哺点6 mac两Shot基线登记待执行); 体验验证清单52节两目检项+实现报告并入审计结论(通过·D1/D2/D3去向在案); 吸收即死退役五件(反哺点清单/实现报告/验收审计/两需求卡片,退役对账承接面核过); roadmap迭代53行已完成+当前位换代(迭代1–53); CI追红修复: forge_table_test效果类型闭集32→31(卡②摘spike断言随动)+level_map_info_fog_test布局缓存换代断言第三判据等强换键(位置差前提随画布同帧收敛失义——缓存失效重算本身正常,换键=重算字典键数恰为新图节点数,同一性判据不动)+capability_audit真实入口锚面补probe_funcs(探针共用面func定义,与代表用例D规则同口径不属放宽)+level_map_shot_probe.gd.uid边车补入库+待办清单QA条目补仓库内证据句(修流水线闸run86起红)。验证: 两修复套件窄测绿(forge 42 checks/fog 21 checks全绿)+流水线闸PASS+能力审计红项0+--docs全绿(合回前复跑凭据见本笔后续回执) 2026-11-06 01:12:55 +08:00
DKChan 7bbde05243 合并(三关色相抽测脚本+效果通道零现役普查报告): 抽测脚本落地(S1 机检 D 三关 H2 槽全过/S2 样图机检 17 项全绿,模型口径订正=shader 精确合成)+普查报告(埋点层与内容层两层分开判——词表全保留零清退项,内容层零孤儿);叙述订正 5 处与迭代 53 改写族清退批撞车——冲突四件全取 master 侧(迭代 53 专名清零已覆盖订正目标,分支侧冗余放弃),普查报告叙述节事实以迭代 53 后状态为准待后续批订正;待办清单普查条推进(待写卡→待执行,遗留旧词收尾待裁)。验证:分支侧冒烟 15 项 0 失败三闸全过 2026-11-06 01:06:43 +08:00
DKChan 111bf1bd10 注释(叙述脱节订正·普查批执行): 5 处按「预留、未设计入表」口径订正——effect_engine OPS 头注 mod_zero/gte_ref/lt_ref 设想用途改预留注记+EFFECT_KINDS 头注补 spike 独立 kind 岗=预留实走 modify_ticket mode 通道、reward_grant _charge_gate 头注掘进蓄能叙述改通用受理闸+预留注记、action_economy request_dig 头注深挖钻头叙述改通用闸+预留注记、treasure_item_table cell_removal 爆破工兵包叙述改预留注记;零逻辑零词表改动。待办清单普查条推进:普查结转(报告凭据+订正已执行),遗留旧词收尾待裁,状态待写卡→待执行。验证:冒烟 15 项 0 失败三闸全过(worktree 冷检缺 .import 假红已经导入清偿——在册环境坑) 2026-11-06 00:48:30 +08:00
DKChan 515a50f896 报告(效果通道与事件词零现役普查): 按读者动作重组——第一节=需用户裁断的2件事(叙述订正5处做不做/旧词迁移收尾做不做何时做)、第二节=无需处理项计数存档(预留词面全部保留零裁决点)、第三节=方法存档;词表面台账从正文移除 2026-11-06 00:38:09 +08:00
DKChan b4e6db2f89 报告(效果通道与事件词零现役普查): 口径收窄重写——埋点层与内容层两层分开判;四词表+schema旧词层全部属预留埋点面词面全保留零清退项(架构原则=词表先行内容后接,effect_engine 头注明文);内容层零孤儿(数据行全部在役);spawn_spell=锻造法杖设计在册待接线、bias_rewards/counter_spend=架构在役机制承载位、旧词层=迭代22秘宝批换代遗留迁移尾巴(收尾归用户另令);唯一实质发现=叙述脱节5处(掘进蓄能/深挖钻头/爆破工兵包三专长未入表而注释作在役叙述,订正方向=注明预留不动逻辑) 2026-11-06 00:33:47 +08:00
DKChan 1345179032 合并(迭代53·卷轴视口批+改写族清退批): 卡①卷轴视口批=关卡画面视口化(level-map.md 布局决策5落地)——新增 level_map_viewport.gd 承载件(裁切/平移clamp/锚点跟随/平滑滚动减动效/指引件渐隐带+chip), level_map_screen.gd 承载改造与阈值输入协议(提交沿=未超阈值抬杆), 探针26断言+golden两Shot面扩项冻结(用户裁:只对拍关卡最开始与Boss节点)+渲染闸lmap对拍段, schema头注指针反转; 卡②改写族清退批=damage_controller 四段零现役注册死分支与穿线键摘除(对拍六组diff=0)+effect_engine 词表收窄(spike/trade_atk/trade_hit)+负域探针两枚+注释面退役名35处清零+玩家可见文案三处(减半标签/中立移除reason/反弹标签)与sim死匹配; 产出物=实现报告+反哺点清单10条+独立验收审计(通过·两卡中档)。验证: 场景探针族全绿+冒烟15/0+S13基线内7新增0+渲染闸全绿(lmap changed=0.00%)+窄测八套exit=0+负面套件四探针全拒; 审计建议级3项落反哺点。post-merge 钩子推送触发远端 CI
ci / gates (push) Failing after 6m49s
2026-11-06 00:08:38 +08:00
DKChan c89e59febb iter53-收尾:迭代53产出物: 实现报告+反哺点清单10条+独立验收审计(通过);并master(fd9c8ad9 QA harness 定案登记+营地背景层两批,零交集) 2026-11-06 00:08:18 +08:00
DKChan 76a7067440 iter53-卡2:改写族清退批: 四段零现役注册死分支摘除+词表收窄spike与trade双键+负域探针两枚+注释面退役名35处清零+玩家可见文案三处与sim死匹配 2026-11-06 00:08:18 +08:00
DKChan 55f4de3ca4 iter53-卡1:卷轴视口批: 关卡画面视口化(level-map.md 布局决策5落地)——视口承载件/锚点跟随/平滑滚动减动效/拖拽滚轮平移与clamp/指引件/探针26断言/golden两Shot冻结 2026-11-06 00:08:18 +08:00
DKChan 4a52d769ec 报告(效果通道与事件词零现役普查): 四词表+schema旧词层+channel域全量取证——事件词零订阅4(armor_roll/reward_roll/treasure_used/seal_settle,发射点全在=完整空架子)、效果词零使用10(空架子6/死词3/spike岗位错位)、schema旧词零行9(死词4/空架子5,ap_charge_safe在役)、事实词6/算子词7零行、叙述脱节5处(掘进蓄能/深挖钻头/爆破工兵包三专长零在表而注释作在役叙述);原实况4组复核成立;裁决点清单A–G供逐组裁。接收方=待办清单普查条(写卡输入合同) 2026-11-06 00:02:40 +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 fd9c8ad90e 合并(QA harness 定案登记): 待办清单新立「QA 覆盖总览面板与自动试玩扩展」条目(三批定案+裁决在案+方案外部指针); 同批并入 playnote.md 用户试玩记录更新; --docs 验收通过零新增红
ci / gates (push) Failing after 26s
2026-11-05 23:13:41 +08:00
DKChan 33ab7deebd 工程+文档(QA harness 定案登记): 待办清单新立「QA 覆盖总览面板与自动试玩扩展」条目——三批定案(B 覆盖总览面板+本体枚举器/C 自动试玩扩展/探针存量真输入化承接既有条目),裁决在案(A 执行覆盖矩阵不做/D 缺口闸不做/变异测试禁入),方案全文外部指针 qa-harness-discussion; 同批并入 playnote.md 用户试玩记录更新(营地问题记录等)。--docs 验收通过零新增红 2026-11-05 23:13:01 +08:00
DKChan 2b06ad163c 合并(营地背景层分片): 远景片+地面片两段提示词,山体上缘≤15% 写进硬指标
ci / gates (push) Successful in 26s
2026-11-05 19:39:06 +08:00
DKChan c9ae0dea07 文档(营地背景层): 拆成远景片与地面片两片出图,两段提示词成文
整张出图时模型把远山挤到画面下半部——三张实测里两张山体上缘(y280/y227)落在墙顶 217 之下,
被围墙整段遮没。改成分两片出:

- §2.1.1 远景片 bg_far(2560×734,3.5:1):天空 0–14%、远山 14–55%(山体占 41% 为硬指标)、
  草地 55–100%,小路只在草地内、从底边收窄到山脚为止
- §2.1.2 地面片 bg_floor(2560×866,3:1):营地内踩实裸土,明度 30–45%,
  中线与左右同色(无路无径无车辙)
- §2.1.3 拼接:按 y734 上下拼成一张 2560×1600,接缝整条落在围墙 406–734 背后
- §2.3 坐标表补两片行、远山带与草地带;§4 判据改为两片+整图三段
  (山体上缘 ≤15%、山体 ≥40%、山脚过渡行均色差 ≤6/255、地面无路带)
- §5 加第 8 条;art-bible §6.2 交付形态注明背景层由两片拼一张

验证:tools/run_all_gates.sh --docs exit=0「验收通过·无新增红」(基线 8be1634d)。
2026-11-05 19:39:04 +08:00
DKChan 8be1634d04 合并(营地背景层地面分区): 营地内踩实裸土无路,营地外草地+土路,取消近景贯通路
ci / gates (push) Successful in 34s
2026-11-05 15:44:45 +08:00
DKChan e8bd1f58de 文档(营地背景层): 地面拆成营地内/营地外两区,取消近景那条路
旧稿把地面当一整片写,路又从画面底贯通到门洞,等于在营地内部修了条路。
按用户 2026-11-05 口径拆成两区:

- 营地外(y734 以上,山脚到地平线):冷调灰绿草地,一条土路从门洞位置向远山收窄
- 营地内(y734 至画面底):踩实裸土、灰褐去饱和、与草地异色相、无草无路无车辙,
  只留拖拽脚印、碎石、踩踏斑块
- 两区分界正好被围墙层(406–734)遮住,画面里看不到硬分界
- §4 补三条判据:门洞内土路只允许在 y734 以上;y734 以下中线与两侧色差 ≤6/255;
  营地内地面与草地平均色 (R−B) 差 ≥10/255
- §5 已裁补第 7 条

验证:tools/run_all_gates.sh --docs exit=0「验收通过·无新增红」(基线 c3d78eed)。
2026-11-05 15:44:44 +08:00
DKChan 441111f49d 工具(三关色相抽测 S1/S2): 并列样图脚本落地——S1 机检 D 三关 H2 槽全过(L 0.346/0.532/0.345)、S2 样图机检 17 项全绿(阶梯单调 9 组、组间 CR 最弱 1.224≥1.20、B/C 45 行零违例、灰度零彩色、四缘净空);模型口径订正=shader 精确合成(方案 0.49/0.51 系反解近似,ui_kit 头注明文距离场非扁平)、表二行数订正 90 行;读数表落 reports/ 2026-11-05 08:32:36 +08:00
DKChan c3d78eed85 合并(营地背景层 prompt 收紧): 地平线 45.9%+地面明度下限+两条机检判据
ci / gates (push) Successful in 34s
2026-11-05 07:44:05 +08:00
DKChan 36be33f81d 文档(营地背景层 prompt 收紧): 地平线量化到 45.9%、地面明度给下限、补两条机检判据
背景层是本轮唯一要重出的素材,提示词补三处可验证约束:
- 地平线位置由「46 percent」改为精确 y=734/1600=45.9%
- 地面明度写明 30%–45% 中段档,禁整片压暗(旧稿地面发闷的成因)
- §4 补两条机检:地面中段域 L 0.30–0.45;地平线上下各 8px 行均色差 ≤6/255
  (旧稿 y321 处实测行均色差约 37/255,硬切证据在案)

验证:tools/run_all_gates.sh --docs exit=0「验收通过·无新增红」(基线 859926b5)。
首跑 doc_term 命中一处退役词("不得"→"禁"),改后复跑绿。
2026-11-05 07:44:05 +08:00
DKChan 859926b5e5 合并(营地场景重排·美术线): 地平线 y367+围墙 150+门洞 84 落进提示词卡与相邻口径
ci / gates (push) Successful in 35s
美术线文档批,只动 design/ 三份:camp-scene-prompts 的 §2 分层规格与 §3-§5 全部换新值、
art-bible §6.2 落盘矩形与门扇帐篷口径、camp.md 地平线描述与帐篷落位。
素材与数据表未动,等重出图到位再改 camp_scene_layout.tres。
验证:文档域全套闸 exit=0「验收通过·无新增红」。post-merge 推送触发远端 CI。
2026-11-05 07:36:51 +08:00
DKChan 898f77e552 文档(营地场景重排): 地平线提到终稿 y367、围墙加高到 150、门洞压到 84
用户 2026-11-05 裁定的场景构图重排,落进美术提示词卡与相邻口径:

camp-scene-prompts.md
- §2.1 背景层重写:地平线出图 y734/终稿 y367;远山只占上半、山脚轮廓起伏,
  明令禁出现平直横向色带(旧稿 y321/y551 两条 1px 硬切即判红);地面放开细节
  (稀疏草簇/石子/车辙/脚印)并要求提亮,解除旧稿「禁一切植被」导致的地面朴素;
  门洞土路改为从画面底 y1600 连续贯通到门洞,底宽顶窄
- §2.2 围墙层重写:桩身 300(终稿 150)、门柱 328(164)、门洞 y566–734 / x1040–1520,
  外接矩形 (0,406,2560,328);解除旧稿「只占画面 1/18 的矮墙」描述
- §2.3 坐标表、§3 表(门扇锁定 240×147 / 贴图 240×138 随门洞压扁)、§4 验收判据、
  §5 已裁九条全部换新值

art-bible.md §6.2:落盘矩形与底锚、门扇与帐篷裁决口径同步
camp.md:地平线描述由「中景带自画面底部往上约 2/3」改为「地平线 y367、围墙带 y203–367」;
帐篷由「尺寸待重定」改为 251×170 落位 (982,335)

验证:tools/run_all_gates.sh --docs exit=0「验收通过·无新增红」(基线 709d9a85)。
首跑 doc_term 命中一处退役词("不得"→"不能"),改后复跑绿。
素材未重出,数据表 camp_scene_layout.tres 等新图到位后再改。
2026-11-05 07:36:36 +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
167 changed files with 5088 additions and 914 deletions

No files matched your search

+17
View File
@@ -1,2 +1,19 @@
# git 钩子文件必须保持 LF——autocrlf=true 检出会转 CRLF,bash 执行带 \r 的脚本即崩。
tools/hooks/* text eol=lf
# 全仓文本一律 LF 入库与检出——Windows core.autocrlf=true 会把工作区检出成 CRLF,
# 行尾一旦进入逐字符比对的字符串基线数据面(如测试内嵌的多行 golden 常量),
# 运行时基线(\n)与源码基线(\r\n)逐位比对必红。本规则让一切平台检出落 LF。
* text=auto eol=lf
# 二进制面显式豁免(防 auto 误判转码损坏素材与基线图)
*.png binary
*.jpg binary
*.jpeg binary
*.webp binary
*.gif binary
*.ico binary
*.ogg binary
*.wav binary
*.mp3 binary
*.ttf binary
*.otf binary
*.zip binary
+2 -2
View File
@@ -16,7 +16,7 @@
## 运行与验证(工作目录 = 含 project.godot 的切片目录)
- **合回验证 = 远端 CI(2026-11-03 用户裁)**:git merge 合回 master 落地后,post-merge 钩子(`tools/hooks/post-merge`)自动 `git push origin master` 触发远端 CI(Gitea Actions,runner=Mac 本机 host 模式,环境自举件=`tools/ci/mac_setup.sh`),工作流(`.gitea/workflows/ci.yml`)按改动域分流(分流算法单点=`tools/ci/domain_route.sh`):游戏码批=两域全量含渲染闸,文档批=文档域秒级闸,纯闸件/管线批=零触发。远端结果**按需现查**:`tools/ci_check.sh <合并提交>` 读该提交任务的 run `status`/`conclusion`,只呈报、不落盘(2026-11-04 用户裁:合回不等待、不唤醒;判据=run 落定后的终局态,且绿须坐实跑闸步真跑过——零触发批呈报「未验」不作绿证,细则=process-conventions §1 第 3 条)。**合回落地即向用户呈报本批总结并止回合**,禁前台 sleep/轮询等 CI。CI 红态不回滚合并——红项在案、修复批合回即再推再验追绿;开工先跑现查件读 CI 状态,红未修不开新批。同一份字节禁止第二次人手跑。`tools/run_all_gates.sh` 仍是全部验证闸的**唯一入口与唯一清单,域划分与选闸算法只写在它这一个地方**,文档禁另列清单;`--code`/`--docs`/`--quick` 是开发期改完即测的选档入口(细则见 process-conventions §4),全量档不替代它。**runner 依赖**:Mac 需开机在线(关机=远端任务排队延后);换机/重装按 `tools/ci/mac_setup.sh` 自举
- **合回验证 = 前置检查 + 远端 CI(前置检查 2026-11-06 用户裁)**:主区 `git merge --no-ff` 落合并提交前,pre-merge-commit 钩子(`tools/hooks/pre-merge-commit`)自动跑 `tools/run_all_gates.sh --premerge`——冒烟测试+单元与集成测试全套件(4 分片),热导入缓存约 2 分钟;红=合并提交不落(修红后 git commit 续合,或 git merge --abort 放弃),快进合并不触发钩子,`--no-verify` 可跳过且仅属用户。git merge 合回 master 落地后,post-merge 钩子(`tools/hooks/post-merge`)自动 `git push origin master` 触发远端 CI(Gitea Actions,runner=Mac 本机 host 模式,环境自举件=`tools/ci/mac_setup.sh`),工作流(`.gitea/workflows/ci.yml`)按改动域分流(分流算法单点=`tools/ci/domain_route.sh`):游戏码批=两域全量含渲染闸,文档批=文档域秒级闸,纯闸件/管线批=零触发。远端结果**按需现查**:`tools/ci_check.sh <合并提交>` 读该提交任务的 run `status`/`conclusion`,只呈报、不落盘(2026-11-04 用户裁:合回不等待、不唤醒;判据=run 落定后的终局态,且绿须坐实跑闸步真跑过——零触发批呈报「未验」不作绿证,细则=process-conventions §1 第 3 条)。**合回落地即向用户呈报本批总结并止回合**,禁前台 sleep/轮询等 CI。CI 红态不回滚合并——红项在案、修复批合回即再推再验追绿;开工先跑现查件读 CI 状态,红未修不开新批。同一份字节禁止第二次人手跑。`tools/run_all_gates.sh` 仍是全部验证闸的**唯一入口与唯一清单,域划分与选闸算法只写在它这一个地方**,文档禁另列清单;`--code`/`--docs`/`--quick` 是开发期改完即测的选档入口(细则见 process-conventions §4),全量档不替代它。**runner 依赖**:Mac 需开机在线(关机=远端任务排队延后);换机/重装按 `tools/ci/mac_setup.sh` 自举
- 单件直跑(=改完即测的最窄确认,其读数就是那一步的凭据;跨改动面的漏网由合回后远端全量档兜):冒烟测试 `python tools/run_smoke.py`(`game/tests/smoke_boot.tscn` 极小启动场景)· 系统测试 `python tools/run_system_test.py`(宿主 `game/tests/system_test.gd`)· 场景探针 `…console.exe --headless --path . res://probe/ui_probe.tscn` · 单元与集成 `… --script res://tests/run_all.gd`
- 调参批量:`--script res://sim/adventure_monte_carlo.gd`(全冒险期望)· `res://sim/reward_expectation.gd`(奖励期望);关卡完成率/难度类测量一律走全冒险
- 数值验证:`python tools/balance_verifier.py`(采集端 `res://sim/verifier_batch.gd`,报告与原始数据落 `sim-data/`)
@@ -104,7 +104,7 @@
## 硬性红线(勿违反;操作细则在 process-conventions.md 与各规约)
- 车道铁律(2026-10-31 用户裁恢复)——主工作区只作合并落点,一切开发/设计改动走独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`(建区即跑 `tools/setup_worktree_userdir.sh`),收口=冻结→件核→并 master→重塑→主区 `git merge --no-ff`→删区(合回即删,2026-09-19 原裁),CI 红不回滚合并——新开修复 worktree(`fix-<任务名>` 分支,免重塑免数卡)修复合回追绿;会话单区(2026-11-04 用户裁)——同一时刻每会话至多一个 worktree 在役,同会话追加事项并入在役区、只做一次合入(一次 CI),合回后用户再发令=新回合另开区,迭代实现批独占回合(细则见 process-conventions §1 第 5 条);主区唯一合法写动作=合并,agent 侧由 worktree-guard 拦主区 Edit/Write 与非 merge 提交;豁免面仅 `.zcode/`、`.qoder/`(不入 git,主区直改);commit 点零钩子检查(pre-commit 直改闸已退役),全量验证归合回后远端 CI(post-merge 钩子推送触发,见「运行与验证」首条);`--no-verify` 仅属用户,agent 禁用(细则见 process-conventions §1)
- 车道铁律(2026-10-31 用户裁恢复)——主工作区只作合并落点,一切开发/设计改动走独立 worktree:`git worktree add ../g-plan2-<任务名> -b <任务分支>`(建区即跑 `tools/setup_worktree_userdir.sh`),收口=冻结→件核→并 master→重塑→主区 `git merge --no-ff`→删区(合回即删,2026-09-19 原裁),CI 红不回滚合并——新开修复 worktree(`fix-<任务名>` 分支,免重塑免数卡)修复合回追绿;会话单区(2026-11-04 用户裁)——同一时刻每会话至多一个 worktree 在役,同会话追加事项并入在役区、只做一次合入(一次 CI),合回后用户再发令=新回合另开区,迭代实现批独占回合(细则见 process-conventions §1 第 5 条);主区唯一合法写动作=合并,agent 侧由 worktree-guard 拦主区 Edit/Write 与非 merge 提交;豁免面仅 `.zcode/`、`.qoder/`(不入 git,主区直改);commit 点零钩子检查(pre-commit 直改闸已退役);合回 merge 点另有前置检查钩子(pre-merge-commit,2026-11-06 用户裁,见「运行与验证」首条);全量验证归合回后远端 CI(post-merge 钩子推送触发,见「运行与验证」首条);`--no-verify` 仅属用户,agent 禁用(细则见 process-conventions §1)
- **仓库根禁止设会话状态文件**(`active.md` 已废除)——现状看 roadmap「当前位」,待办看待办清单,过程看 git 提交信息,产出物看 `reports/`;任何文档禁止另设「当前状态快照」节,两处记同一件事必然漂移(细则见 doc-conventions 第 10 节)
- **改完即测铁规(2026-11-05 用户裁,取代原「测试执行两入口铁规」)**——任何会话里的任何改动,落笔之后立刻跑一遍覆盖该改动面的验证并跑绿、读数写进这一步的提交信息,这一步才算完成;验证载体一律按 `tools/run_all_gates.sh` 的域划分与该件自身的直跑方式,不扩出改动面、不请示、不等点名,未改动的面不测,合回后远端 CI 的全量跑批不替代本条。读数类采集(MC/verifier)不在该域划分内,不属本条义务
- **红账不清不开新批**——上一批远端跑批有真红未闭合不开新批;假红(导入缓存、user 目录未隔离那类在册环境坑)走异常分级处置,不算未闭合
+7 -7
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;新色入板过两道门〉
@@ -392,16 +392,16 @@
- **情绪与色域**:承载 §2.8——全作最暖:烛光黄 `#F6C667` × 木色 `#8A5A34`(§7.2),中等饱和,无棋盘式高对比。
- **场景构成**:一张可点击的营地场景图(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 序)。**篝火例外**:命中矩形只取下半(火堆),上半(火焰)只作特效展示。围墙层落盘按细条裁,不做全幅(全幅约 3.9 MB 显存,细条约 0.35 MB);**实装落盘矩形 `(0, 261, 1280, 49)`、底锚 y310**(Y 压缩按桩身 43px 终稿锁定值精确,落带内〔规划带 (0,240,1280,70)〕;备选 A′〔带高 70/桩身 62〕存档 artwork 同目录)。
- **色彩与氛围依据**:营地场景的冷暖分工(冷的大面积后退、暖的小面积前进)、空气透视三档与大面积用色规则 = `reports/提案-营地色彩与氛围脚本-2026-10-23.md`。本节不复述色值;新增环境色按 §4.1 场景环境子板条款登记(每屏一套、≤32 色、过 §4.2 语义检查与 RGB 三通道差 ≥8/255 两道门)。
- **交付形态**:**背景层 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)〕)。
- **色彩与氛围依据**:营地整体偏暖、安全温馨,与冒险中的地下城形成对比;现行口径 = 本文件 §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×220 追认现值,坐标按门洞 x520–760 对齐(`camp-scene-prompts.md` §2.3);③ **帐篷的落地尺寸**——300×170 追认现值(各素材第一张样本稿已过审为终稿,重定即触发重出,不裁)。
- **已裁(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%),改探针口径须同步本件。
+95 -33
View File
@@ -1,9 +1,9 @@
# 营地场景 · 分层生图提示词卡 v1.5
> **性质**:营地场景图(1280×800,一张可点击场景图 + 十张分层:背景层 1 + 围墙层 1 + 八个物件层)的出图执行侧文件。**场景级资产,与角色级 [art-prompts](art-prompts.md) 并列,两文件互不覆盖**。
> **依据**:色彩与氛围 = `reports/提案-营地色彩与氛围脚本-2026-10-23.md`(本卡不复述色值);坐标与层序 = `artwork/2026-10-23-营地场景/布局草案-1280x800.html`;物件命名与消费链 = [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-10-25 · 状态:十条提示词成文(背景层 1 + 围墙层 1 + 八个物件层);2026-11-05 按地平线 y367 重排,背景层与围墙层待按 §4 新判据重出,门扇随门洞压扁重画
---
@@ -14,7 +14,7 @@
3. **后处理**:脚本管线代跑——去底转透明、降采样到终稿尺寸(整数倍)、色板量化、四联底测试条。**背景层不去底(全幅不透明)**;围墙层与八个物件层去底转透明。后处理工具清单与用法见 [art-pipeline-tools](art-pipeline-tools.md)。
4. **转正**:终稿转入切片资产目录,按坐标表接线。
**出图顺序(先锁后产,不能跳)**:概念脚本 → **本卡提示词** → 贴风格参考图**先出背景层 `scene_camp_bg` 1 张** → 作者确认风格前缀 → 才扩产围墙层 `scene_camp_wall` 与八个物件层(共 9 张)。
**出图顺序(先锁后产,不能跳)**:概念脚本 → **本卡提示词** → 贴风格参考图**先出远景片 `bg_far` 1 张** → 作者确认风格前缀 → 才出地面片 `bg_floor`、围墙层与八个物件层。
---
@@ -39,26 +39,61 @@ Style reference: the attached images are this game's own finished pixel art and
## 2. 背景层与围墙层(两张,不可点)
围墙从背景层拆出、独立成层:围墙层可单独锁死桩高、门洞做成透明缺口透出背景层的小路;背景层不混入暖木色、更易守住冷色占比判据(判据见 §4)。两层都不可点;按乙工序门**先只出背景层 1 张**供作者确认风格前缀,确认后才扩产围墙层与八个物件层。
围墙从背景层拆出、独立成层:围墙层可单独锁死桩高、门洞做成透明缺口透出背景层的小路;背景层只画天、山、草地与地面,不画任何物件,远景保持淡而退后,让近景的暖调成为画面主角。两层都不可点;按乙工序门**先只出背景层 1 张**供作者确认风格前缀,确认后才扩产围墙层与八个物件层。
### 2.1 背景层(`scene_camp_bg`,不可点,不透明全幅)
**出图画布**:2560×1600(终稿 1280×800 的 2 倍,保证整数倍降采样)
**画什么**:天空、远山、整片地面、门洞位置一条通向远处的小路
**不画什么**:木围墙、门柱、门扇、七个物件、角色、接地阴影——围墙与门柱是 §2.2 的独立层,门洞是它的透明缺口;小路在本层画好、透出来即可
**产出方式**:分两片出图,后处理按 y734 拼成一张 2560×1600。接缝整条落在围墙层(406–734)背后,画面里看不见。
| 片 | 出图画布 | 终稿落位 | 内容 |
|---|---|---|---|
| 远景片 `bg_far` | 2560×734(3.5:1 超宽) | (0, 0, 1280, 367) | 天空+远山+草地+小路 |
| 地面片 `bg_floor` | 2560×866(3:1 超宽) | (0, 367, 1280, 433) | 营地内踩实裸土 |
**为什么分两片**:整张一次出图时,模型按「构图好看」分配天地比例,山被挤到画面下半部——三张实测里两张的山体整段落在围墙后面(山体上缘 y280/y227,墙顶 217),远山等于消失。拆开后远景片的全部高度都归天空+山+草地,山才可能长到围墙之上。
#### 2.1.1 远景片(`bg_far`)
**画布比例**:3.5:1 超宽。通道支持直接出 2560×734;不支持则按 3:2 出图后纵向压到 3.5:1,构图比例不变
**分带(按画面高度)**:
- 天空 0–14%
- 远山 14–55%(**山体占 41%,硬指标**)
- 草地 55–100%
- 小路在草地内:从底边向上收窄,到山脚为止,**不越过山脚、不上探到天空**
**画什么**:天空、2–3 层远山、成片草地、草地中央一条土路
**不画什么**:围墙、门柱、门扇、物件、角色、营地内地面(那是 §2.1.2);山与草地之间禁出现平直硬边
```
Pixel art style. True pixel art game background layer drawn with chunky square pixels, strict pixel grid alignment, hard edges, no anti-aliasing, no gradients, flat colors only, a limited palette of about 24 colors, retro 16-bit look, Aseprite style, all ages, flat side view, orthographic, no perspective distortion. Wide horizontal composition, 16:10 aspect ratio. This is the FAR BACKGROUND LAYER ONLY of a small adventurer camp: sky, distant hills and the whole ground. It must contain no palisade wall, no gate posts, no camp objects, no gate leaves, no characters and no drop shadows, because all of those are drawn as separate transparent layers on top of this one. COLOR SCRIPT — cool recedes, warm advances: the sky, the hills and the ground are all COOL, pale, desaturated and low in contrast; there is no warm accent anywhere in this layer, warm color belongs only to the separate object layers drawn on top. ATMOSPHERIC DEPTH: every plane that is farther away is lighter, closer to the value of the sky, cooler, lower in contrast and simpler in detail, while the nearest plane is the darkest and highest in contrast. SKY (top of frame): the lightest thing in the picture, flat pale cool off-white with a faint blue-grey tint, no yellow or cream tone, no sun anywhere; the very top strip of the frame, about one sixteenth of the frame height, stays completely calm and empty, reserved as clear space with nothing in it; at most one or two extremely simple flat low-contrast clouds. FAR DISTANCE: two or three layers of low rolling hill silhouettes in pale cool blue-grey, each farther layer lighter, cooler and lower in contrast than the one in front, fading towards the value of the sky, no trees, no individual foliage. HILLS TO GROUND: the hills fade gradually and smoothly down into the ground, with NO visible horizon line and NO distant ground band — the transition must not read as a separate horizontal stripe of a different color. GATE PATH: in the middle of the frame, between x=1040 and x=1520, one clearly visible dirt path runs from the ground at two thirds of the frame height measured from the bottom edge away into the distance between the hills, narrowing as it recedes, painted in a muted desaturated grey-brown earth tone, never golden or warm yellow; this path shows through the empty gate opening of the wall layer above. FOREGROUND GROUND: the darkest area of the picture, dry packed bare earth in a neutral desaturated grey-brown, never golden, amber or warm yellow, hard flat color blocks, hard edges, no gradient, no grass, no grass tufts, no weeds and no plants of any kind. The foreground ground is completely empty apart from the dirt path: no objects, no icons, no badges, no markers, no decorations of any kind. Do not add any of the following: realistic, photorealistic or 3D rendering; painterly brushwork, smooth anti-aliased lines, gradient or dithered shading, glossy highlights; black or heavy dark outlines; drop shadows, contact shadows or any shadow band; blur or compression artifacts; a vignette, dark corners, or any mood darkening; night, dusk, moon, stars, sunset or sunrise light; a sun or any light source drawn in the sky; a visible horizon line, a separate horizontal distant ground band or any distant grass strip below the hills; trees, forests or foliage in the distance; grass, grass tufts, weeds or plants in the foreground; a palisade wall, wooden fence, fence posts, gate posts or any wall structure; any warm, golden, amber or yellow cast on the sky, on the hills or on the ground; any camp object, furniture, crate, barrel, fire, tent or tool; any gate leaf, door panel or hinge; any purple or magenta anywhere; any text, letters, numbers, watermark, signature or logo; any frame, border, panel, arrow or UI element; any icon, badge, item tile, emblem, sign, marker or symbol anywhere in the frame; any characters, people, creatures or animals.
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 超宽
**内容**:营地内被太阳晒着的踩实黄土地,铺满整幅
**硬指标**:**中线与左右两侧同色——没有任何路、小径、车辙**;明度读到晴天下被晒亮的干土,不暗、不闷;色相与远景片草地明显区分
**不画什么**:草、植被、大石块、物件、天空、地平线、围墙、接缝线
```
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 拼接与后处理
1. 两片各缩放到终稿尺寸:远景片 1280×367、地面片 1280×433
2. 按 y367 上下拼接成 1280×800
3. 量化到营地环境子板(≤32 色)
4. 需要 2× 档时整数倍放大到 2560×1600
5. 接缝不做过渡处理——它整条落在围墙(203–367)背后
### 2.2 围墙层(`scene_camp_wall`,不可点,透明底)
**出图画布**:2560×1600(与背景层同画布,门洞 x 坐标直接对齐);墙横贯画在出图 y480–620 这条**窄带**内,其余为纯色底 `#F2F2F2`。落盘由 S4 后处理裁成终稿外接矩形 `(0, 240, 1280, 70)` / 出图 `(0, 480, 2560, 140)` 的细条,**不做全幅 1280×800 落盘**(全幅约 3.9 MB 显存,细条约 0.35 MB)。
**画什么**:横贯全宽的密排竖木桩墙(左右两段)+ 两根略高的门柱,门洞处完全透明
**出图画布**:2560×1600(与背景层同画布,门洞 x 坐标直接对齐);墙横贯画在出图 y406–734 这条带内(桩身 y434–734,门柱 y406–734),其余为纯色底 `#F2F2F2`。落盘由 S4 后处理裁成终稿外接矩形 `(0, 203, 1280, 164)` / 出图 `(0, 406, 2560, 328)` 的细条,**不做全幅 1280×800 落盘**(全幅约 3.9 MB 显存,细条约 0.35 MB)。
**画什么**:横贯全宽的密排竖木桩墙(左右两段,桩身终稿 150 高)+ 两根略高的门柱(终稿 164 高),门洞处完全透明
**不画什么**:天空、远山、地面、小路、门扇、七个物件、角色;门洞内不画任何东西,透出背景层的小路
```
Pixel art style. True pixel art game layer drawn with chunky square pixels, strict pixel grid alignment, hard edges, no anti-aliasing, no gradients, flat colors only, a limited palette of about 24 colors, retro 16-bit look, Aseprite style, all ages, flat side view, orthographic, no perspective distortion. Wide horizontal composition, 16:10 aspect ratio. This is the PALISADE WALL LAYER ONLY of a small adventurer camp, drawn on a plain flat uniform light grey #F2F2F2 background for easy keying. A wooden palisade wall runs straight across the full width of the frame, seen from the front, built from closely spaced vertical logs standing side by side with no gaps between them, every log cut to a sharp point at the top, a solid barrier with no horizontal rails, no see-through gaps, no ranch-style post-and-rail fence. The wall is SHORT and LOW: including the pointed tips the whole band occupies only about one eighteenth of the total frame height, a thin horizontal strip, NOT a tall barrier; the logs stand only about one eighteenth of the total frame height above their base line. In the middle of the wall one wide gap holds two thick gate posts slightly taller than the wall, and the entire opening between them is completely EMPTY — plain flat uniform light grey #F2F2F2 showing through, no logs, no path, no ground, no scenery inside the opening, because the dirt path and the hills belong to the background layer below and the gate leaves are a separate object layer. The wall is painted in a muted greyed wood brown, with a thin warm cream outline around the whole silhouette, flat shading. Do not add any of the following: realistic, photorealistic or 3D rendering; painterly brushwork, smooth anti-aliased lines, gradient or dithered shading, glossy highlights; black or heavy dark outlines; drop shadows or contact shadows; blur or compression artifacts; a vignette or dark corners; any sky, hills, ground, grass, dirt path or scenery of any kind; any gate leaf, door panel or hinge; any camp object, furniture, crate, barrel, fire, tent or tool; any characters, people, creatures or animals; any purple or magenta anywhere; any text, letters, numbers, watermark, signature or logo; any frame, border, panel, arrow or UI element.
Pixel art style. True pixel art game layer drawn with chunky square pixels, strict pixel grid alignment, hard edges, no anti-aliasing, no gradients, flat colors only, a limited palette of about 24 colors, retro 16-bit look, Aseprite style, all ages, flat side view, orthographic, no perspective distortion. Wide horizontal composition, 16:10 aspect ratio. This is the PALISADE WALL LAYER ONLY of a small adventurer camp, drawn on a plain flat uniform light grey #F2F2F2 background for easy keying. A wooden palisade wall runs straight across the full width of the frame, seen from the front, built from closely spaced vertical logs standing side by side with no gaps between them, every log cut to a sharp point at the top, a solid barrier with no horizontal rails, no see-through gaps, no ranch-style post-and-rail fence. The wall stands on the ground line at y=734 of 1600. The LOG BODIES, pointed tips included, rise 300 pixels above that base line — about nineteen percent of the total frame height — so it reads as a real barrier with presence, clearly taller than the gate opening cut into it, not a thin decorative strip. In the middle of the wall one wide gap holds two thick gate posts standing 28 pixels taller than the logs, and the entire opening between them is completely EMPTY — plain flat uniform light grey #F2F2F2 showing through, spanning x=1040 to x=1520 and rising from the base line y=734 up to y=566, no logs, no path, no ground, no scenery inside the opening, because the dirt path and the hills belong to the background layer below and the gate leaves are a separate object layer. The wall is painted in a muted greyed wood brown, with a thin warm cream outline around the whole silhouette, flat shading. Do not add any of the following: realistic, photorealistic or 3D rendering; painterly brushwork, smooth anti-aliased lines, gradient or dithered shading, glossy highlights; black or heavy dark outlines; drop shadows or contact shadows; blur or compression artifacts; a vignette or dark corners; any sky, hills, ground, grass, dirt path or scenery of any kind; any gate leaf, door panel or hinge; any camp object, furniture, crate, barrel, fire, tent or tool; any characters, people, creatures or animals; any purple or magenta anywhere; any text, letters, numbers, watermark, signature or logo; any frame, border, panel, arrow or UI element.
```
### 2.3 出图画布上的坐标(终稿 ×2)
@@ -66,11 +101,17 @@ Pixel art style. True pixel art game layer drawn with chunky square pixels, stri
| 元素 | 终稿 1280×800 | 出图画布 2560×1600 |
|---|---|---|
| 顶栏 HUD 保留带 | y ≤ 48 | **y ≤ 96**(保持空旷) |
| 背景层 `scene_camp_bg` | 全幅 1280×800 | 全幅 2560×1600 |
| 围墙带(wall 层桩身) | y 267–310(**高 43px**) | y 534–620(**高 86px**) |
| 门柱(wall 层,略高) | y 240–310;x 500–520 / 760–780 | y 480–620;x 1000–1040 / 1520–1560 |
| 门洞(wall 层透明缺口) | x 520–760 | x 1040–1520 |
| 门洞内小路(画在 bg 层) | x 520–760,自 y310 向上收窄至远山 | x 1040–1520,自 y620 向上收窄至远山 |
| 远景片 `bg_far` | y 0–367(高 367) | y 0–734(高 734,3.5:1) |
| 地面片 `bg_floor` | y 367–800(高 433) | y 734–1600(高 866,3:1) |
| 两片接缝 | **y 367**(落在围墙 203–367 背后) | **y 734**(落在围墙 406–734 背后) |
| 地平线(山脚/草地起点) | y 与两片接缝重合,墙背后不可见 | 同左 |
| 远山带 | y 51–202(画面 14%–55%,山体 41%) | y 103–403 |
| 草地带 | y 202–367 | y 403–734 |
| 围墙层外接矩形 | y 203–367(高 164) | y 406–734(高 328) |
| 围墙带(wall 层桩身) | y 217–367(**高 150px**) | y 434–734(**高 300px**) |
| 门柱(wall 层,略高) | y 203–367(高 164);x 500–520 / 760–780 | y 406–734;x 1000–1040 / 1520–1560 |
| 门洞(wall 层透明缺口) | y 283–367(高 84);x 520–760 | y 566–734(高 168);x 1040–1520 |
| 门洞内小路(画在远景片草地内) | x 520–760 穿门洞,自接缝 y367 向上收窄,到山脚 y202 消失 | x 1040–1520,自 y734 向上收窄,到 y403 消失 |
---
@@ -97,7 +138,7 @@ Do not add any of the following: realistic, photorealistic or 3D rendering; pain
| 件 | z 序 | 终稿矩形 | 出图画布 | 正文 |
|---|---|---|---|---|
| 布告板 | 2 | 130×140 | 260×280 | §3.1 |
| 门扇 | 3 | 240×220 | 480×440 | §3.2 |
| 门扇 | 3 | 240×147 | 480×294 | §3.2 |
| 木桩 | 4 | 120×180 | 240×360 | §3.3 |
| 锻炉·铁砧 | 4 | 240×240 | 480×480 | §3.4 |
| 帐篷 | 4(不可点) | 300×170 | 600×340 | §3.5 |
@@ -105,7 +146,7 @@ Do not add any of the following: realistic, photorealistic or 3D rendering; pain
| 笔记本 | 5 | 100×80 | 200×160 | §3.7 |
| 篝火 | 6 | 160×160 | 320×320 | §3.8 |
**落位实测(锁定矩形 contain)**:布告板 130×121/门扇 240×211/木桩 120×180/锻炉 240×193/帐篷 251×170/宝箱 106×90/笔记本 100×66/篝火 160×149——贴图锚定=底中心(帐篷/锻炉/宝箱/篝火/笔记本)+顶锚(门扇悬挂/布告板挂墙);命中矩形按锁定值。换算式登记(源 2× 档→落位尺寸)存 artwork 工作区。
**落位尺寸(2026-11-05 用户裁地平线 y367 重排后)**:布告板 130×121/门扇 240×138/木桩 120×180/锻炉 240×193/帐篷 251×170/宝箱 106×90/笔记本 100×66/篝火 160×149——贴图锚定=底中心(帐篷/锻炉/宝箱/篝火/笔记本)+顶锚(门扇悬挂/布告板挂墙);命中矩形按锁定值。八件只改落位,门扇另随门洞压扁重画(原 240×211 → 240×138),其余七件沿用现有终稿。
**3.1 布告板(→ 图鉴)**
```
@@ -114,7 +155,7 @@ A wooden notice board hung on a wall, with two or three blank pinned papers, no
**3.2 门扇(→ 冒险)**
```
Two wooden gate leaves hanging from their hinges, half open and tilted outward, each made of thick planks with a cross brace and dark iron fittings, seen from the front, the pair shown together as one single object; wood brown #8A5A34 planks, dark iron fittings.
Two wooden gate leaves hanging from their hinges, half open and tilted outward, each made of thick planks with a cross brace and dark iron fittings, seen from the front, the pair shown together as one single object; the pair together fills a WIDE LOW rectangle about 240 wide by 138 tall, so the leaves read as broad rather than tall, each leaf roughly twice as wide as it is high; wood brown #8A5A34 planks, dark iron fittings.
```
**3.3 木桩(→ 天赋树)**
@@ -151,17 +192,32 @@ A campfire made of a ring of grey stones around crossed stacked logs with a warm
## 4. 验收(机检 + 目检)
**背景层 `scene_camp_bg`**
- 黄度 =(R+G)/2−B 分区取样:天空 ≤20、远山 ≤20、地面 ≤35
- 冷色像素占比 ≥55%;本层不应出现任何暖色高饱和像素
- 目检:顶部约 1/16 完全空旷;**无地平线、无远景地面带**(远山自然渐隐过渡到地面);不含围墙、门柱、门扇、物件、角色
- 目检/机检:门洞 x1040–1520 处**有一条清晰可辨、向远处收窄的土路**(非纯色块),透出「出去即冒险」
- 机检/目检:全幅**无任何独立物件/图标/徽章/标记**(前景地面除小路外全空);参考做法=中下方块区独立色数应≈邻区
**远景片 `bg_far`**
- 整体读作晴天暖调、安全温馨,与冒险中的地下城形成对比;色温与色值判据按 §5 第 11 条随环境子板登记回填
- 整体暖调但保持中等饱和、低对比;远景淡而退后、近景暖调为主,高饱和只留给火焰,禁出现整屏高饱和偏色
- 机检:**山体上缘 ≤ 画面高度 15%**(这是硬指标——山体上缘落得越低,被围墙挡住后越是什么都看不见)
- 机检:**山体占画面高度 ≥40%**(山体上缘到山脚)
- 机检:草地上限 ≤ 画面高度 55%
- 机检:山脚与草地交界处,上下各 8px 窄带的行均色差 ≤6/255(禁平直硬边;旧稿 y321 处约 37/255)
- 目检:顶部天空空旷、无太阳、云不超过两朵扁平简笔
- 目检:小路只在草地内,从底边向上收窄到山脚为止,**不越过山脚、不上探到天空**
- 目检:无围墙、门柱、门扇、物件、角色
**地面片 `bg_floor`**
- 明度读到晴天下被晒亮的干土,不暗、不闷;硬阈值随环境子板登记回填(§5 第 11 条)
- 机检:画面中线 200px 带与左右各 250px 带的平均色差 ≤6/255(**营地内无路带**)
- 机检:与远景片草地的平均色 (R−B) 差 ≥10/255(两种材质色相可辨)
- 目检:能读出晴天下被晒过的干土与柔和日晒斑块;禁出现草、植被、大石块、物件、水面;细节读作拖拽脚印/碎石/踩踏斑块,不是满屏噪点
**整图(两片拼接后)**
- 接缝 y367 落在围墙 203–367 背后,**画面里看不见接缝**
- 单张 ≤32 色(量化后)
- 全幅无任何独立物件/图标/徽章/标记
**围墙层 `scene_camp_wall`**
- 透明底 `#F2F2F2` 可去底、奶油描边闭合
- **桩身高度 = 出图 y534–620(终稿 43px);门柱略高 y480–620。桩身超出围墙带下沿 y620 达 5px 以上即判红**
- 目检:密排竖木桩、桩顶削尖、不透缝、无横木;门洞 x1040–1520 **完全透明**(透出 bg 小路)、无门扇
- **桩身 = 出图 y434–734(终稿 150px);门柱 y406–734(终稿 164px);外接矩形 (0,406,2560,328)。桩身超出下沿 y734 达 5px 以上即判红**
- 目检:密排竖木桩、桩顶削尖、不透缝、无横木;门洞 y566–734 / x1040–1520 **完全透明**(透出 bg 小路)、无门扇;桩身高度须明显大于门洞高度
- 单张色数 ≤32(量化后)、缩放为整数倍无拉伸痕迹
> **量化与缩放实况注记**:围墙层与八件已按营地主色板 **32 色重量化回写**(art-bible §4.1 子板行——违例对 0);落位缩放实况=源 2× 档经后处理换算为锁定矩形 contain(×0.1934–0.4404 非整数倍),「整数倍无拉伸痕迹」判据的机检面以落位实测尺寸与目检兜底执行。
@@ -175,10 +231,16 @@ A campfire made of a ring of grey stones around crossed stacked logs with a warm
---
## 5. 已裁(2026-11-01 用户裁;不再阻塞坐标表与定稿)
## 5. 已裁(2026-11-05 用户裁;不再阻塞坐标表与定稿)
1. **帐篷尺寸**:300×170 追认现值(各素材第一张样本稿已过审为终稿;重定即按比例重出、不裁)
2. **门扇高宽**:240×220 追认现值,坐标按门洞 x520–760 对齐(§2.3);围墙带桩身高 43px、门扇叠在门洞上的叠放关系随此锁定
3. **布告板位置**:采纳建议值 `(845, 268, 130, 140)` 挂回围墙带;终值随坐标表(`camp_scene_layout.tres`)落盘
4. **描边颜色**:奶油色追认——去底脚本「奶油描边膨胀 3px 当屏障」的技术前提随现值锁定(§1 注记同源);改深灰褐须换去底方案全量重出,不裁
5. **营地环境子板色值**:不前置拍板——量化场景实际用色后按 art-bible §4.1 两道门登记(语义检查+RGB 三通道差 ≥8/255),本卡仍不复述色值
1. **地平线与整体分带**:地平线定在终稿 y367(出图 y734);地面 433px、可见远景 217px(27%);围墙、门、布告板随之上调
2. **围墙高度**:桩身 150(出图 300)、门柱 164(出图 328)、门洞 84(出图 168);墙高须大于门洞高
3. **门扇尺寸**:贴图 240×138(锁定 240×147),随门洞压扁重画,坐标按门洞 x520–760 对齐
4. **帐篷**:251×170 沿用现终稿,落位 (982, 335),遮住墙下部 32px(覆盖围墙可接受)
5. **笔记本**:随帐篷跟随,落在帐篷左下方外侧、两者不重叠——`x = 帐篷 x − 112`、`y = 帐篷底 + 10`
6. **宝箱离底**:贴图底 y744,距画面底 56px;其余物件同步上移 46px
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 注记同源);改深灰褐须换去底方案全量重出,不裁
11. **营地环境子板色值**:不前置拍板——量化场景实际用色后按 art-bible §4.1 两道门登记(语义检查+RGB 三通道差 ≥8/255),本卡仍不复述色值
+4 -4
View File
@@ -10,9 +10,9 @@
主工作区(master 检出)只作合并落点,一切开发/设计改动禁止直接发生在主工作区(2026-10-31 用户裁恢复;验证模型=批内按 §4 改完即测、合回后远端 CI 是唯一强制全量验证点,细则见本节第 3 条):
1. **worktree 车道(一切面)**:每回合先建独立 worktree(建区粒度=回合,会话单区细则见第 5 条):`git worktree add ../g-plan2-<任务名> -b <任务分支>`;建区后立刻跑 `tools/setup_worktree_userdir.sh`——给该区独立 Godot user 目录(测试夹具、试玩日志、截图产物不与其他区共用;主区不生成,真实档保持原位)。所有改动与提交都在 worktree 内自由进行——commit 点零钩子检查(`tools/hooks/pre-commit` 已删除)。豁免面仅 `.zcode/`(钩子与配置)与 `.qoder/`(本机 harness 配置)允许主工作区直改——两份实测均不入 git(走 worktree 反而带不进去,改动只对本机生效)
2. **收口顺序(固定)**:**冻结 → 件核 → 并 master → 重塑 → 主区合回 → 删区**——代码与断言改完即冻结,全仓件核 grep 查残留;master 已被别的批次推进时先在 worktree 内 `git merge master` 解冲突(必须在合回之前,合回后才并=CI 验的是中间态);**重塑**=迭代分支(iter<N> 形)合回前执行 `python tools/reshape_cards.py`,把分支线性史按卡末态快照压成一卡一笔(可选追加一笔纯文档收尾),master 台账由此每卡一行——工具自带「重塑前后树逐字节一致」断言与 backup 分支,不改任何文件内容;非迭代批免重塑;然后主工作区 `git merge --no-ff <任务分支>`——这是主区唯一合法写动作(合回守卫对迭代分支数卡校验,未归卡提交拦下)。**合回落地即 `git worktree remove`+删分支(合回即删,2026-09-19 原裁);post-merge 钩子已在合回落地时自动触发 CI,红态不回滚合并——新开修复 worktree(`fix-<任务名>` 分支,非 iter<N> 形免重塑免数卡)修复提交再合,落地复验追绿后删修复区**。批内的验证义务按 §4 改完即测铁规履行——改完即测自己改过的那部分,不请示、不等点名;收口顺序本身不设跑批关口。**卡片复核前置判据(2026-11-05 用户裁)**:需求卡片的 grilling 复核先于卡片提交、卡片提交先于合回——复核未跑完、缺陷清单未裁完,禁把卡片落 commit(落了也不算成立),`master` 永不落未成立卡;本条与 §2 段三出口铁律、doc-conventions §12.1「已写卡」凭据同判。
2. **收口顺序(固定)**:**冻结 → 件核 → 并 master → 重塑 → 主区合回 → 删区**——代码与断言改完即冻结,全仓件核 grep 查残留;master 已被别的批次推进时先在 worktree 内 `git merge master` 解冲突(必须在合回之前,合回后才并=CI 验的是中间态);**重塑**=迭代分支(iter<N> 形)合回前执行 `python tools/reshape_cards.py`,把分支线性史按卡末态快照压成一卡一笔(可选追加一笔纯文档收尾),master 台账由此每卡一行——工具自带「重塑前后树逐字节一致」断言与 backup 分支,不改任何文件内容;非迭代批免重塑;然后主工作区 `git merge --no-ff <任务分支>`(pre-merge-commit 钩子先跑前置检查——冒烟+单元与集成全套件约 2 分钟,红则停合并提交,细则见第 4 条 ③)——这是主区唯一合法写动作(合回守卫对迭代分支数卡校验,未归卡提交拦下)。**合回落地即 `git worktree remove`+删分支(合回即删,2026-09-19 原裁);post-merge 钩子已在合回落地时自动触发 CI,红态不回滚合并——新开修复 worktree(`fix-<任务名>` 分支,非 iter<N> 形免重塑免数卡)修复提交再合,落地复验追绿后删修复区**。批内的验证义务按 §4 改完即测铁规履行——改完即测自己改过的那部分,不请示、不等点名;收口顺序本身不设跑批关口。**卡片复核前置判据(2026-11-05 用户裁)**:需求卡片的 grilling 复核先于卡片提交、卡片提交先于合回——复核未跑完、缺陷清单未裁完,禁把卡片落 commit(落了也不算成立),`master` 永不落未成立卡;本条与 §2 段三出口铁律、doc-conventions §12.1「已写卡」凭据同判。
3. **合回后验证(远端 CI,2026-11-03 用户裁)**:git merge 合回 master 落地后 post-merge 钩子(`tools/hooks/post-merge`)自动 `git push origin master` 触发远端 CI(Gitea Actions,runner=Mac 本机 host 模式),工作流按改动域分流(分流算法单点=`tools/ci/domain_route.sh`:游戏码批=两域全量含渲染闸,文档批=文档域秒级闸,纯闸件/管线批=零触发)——唯一强制全量验证点。远端结果用 `tools/ci_check.sh <合并提交>` 按需现查(读该提交任务的 run 自身 `status`/`conclusion`,只呈报、不落盘),agent 开工先读 CI 状态的纪律不变,红未修不开新批。**绿须坐实**:零触发批的 run 照样建、照样 conclusion=success,但跑闸步是 `skipped`(run 55 实证)——一条闸都没跑不构成验证,现查件对此呈报「未验」(退出码 4),禁当绿证用;job 列表为空、取不到步级读数或工作流里没有跑闸步时呈报「不可判」,同样禁当绿证。CI 红态不回滚合并——修复走新开修复 worktree(`fix-<任务名>` 分支,非 iter<N> 形免重塑免数卡),修复合回落地即再推再验追绿(追绿同链)。导入缓存刷新(.godot 不入 git 的在册坑)由工作流「导入缓存刷新」步接管。**合回零等待纪律(2026-11-04 用户裁,取代 2026-11-02 回执等待纪律)**:合回落地即向用户呈报本批总结并止回合——不挂后台轮询、不等回执、不被唤醒;CI 结论什么时候要看就现查一次(`tools/ci_check.sh`,run 那时早已落定,秒级)。退役缘由=原轮询件把「commit status 离开 pending」当终局判据,会把 run 未落定期的尝试级失败写成终局红(run 37 实证:开跑 36 秒写红,该 run 三分半后 `conclusion=success`),且零触发批与 runner 排队必熬满超时再写红;同 commit 的多路轮询还重复覆写同一份判定。跑闸等一切分钟级跑批的等待纪律不变——后台运行,禁前台 sleep/轮询循环。
4. **执法分层(全机械,零自觉依赖)**:①`.zcode/hooks/worktree-guard.sh`(PreToolUse,本机件不入 git)——agent 侧:主工作区 Edit/Write 与主区非 merge `git commit` 拦截(豁免 `.zcode/`、`.qoder/`;worktree 内目标快速放行;合并收尾提交凭 MERGE_HEAD 豁免)、`git commit --no-verify` 拦截(该旗标仅属用户)、iter<N> 分支合回的一卡一提交数卡校验、规则闸(写 design//game/ 前核对会话规约读取记录)。②post-merge 钩子——远端 CI 触发(只推送,不挂等待;仅主工作区,任务 worktree 内的合并不触发)。③git 层不设车道钩子——用户手工改动不受限;ZCode 层只约束 agent(子代理不经守卫层)
4. **执法分层(全机械,零自觉依赖)**:①`.zcode/hooks/worktree-guard.sh`(PreToolUse,本机件不入 git)——agent 侧:主工作区 Edit/Write 与主区非 merge `git commit` 拦截(豁免 `.zcode/`、`.qoder/`;worktree 内目标快速放行;合并收尾提交凭 MERGE_HEAD 豁免)、`git commit --no-verify` 拦截(该旗标仅属用户)、iter<N> 分支合回的一卡一提交数卡校验、规则闸(写 design//game/ 前核对会话规约读取记录)。②post-merge 钩子——远端 CI 触发(只推送,不挂等待;仅主工作区,任务 worktree 内的合并不触发)。③git 层钩子=合回链两件:pre-merge-commit(合回前置检查,2026-11-06 用户裁:主区 merge 落合并提交前自动跑 `tools/run_all_gates.sh --premerge`=冒烟+单元与集成全套件 4 分片,热导入缓存约 2 分钟;exit≠0 拦合并提交不拦合并动作——红态仓库停 MERGE_HEAD 态,修红 git commit 续合或 git merge --abort 放弃;快进合并不触发;--no-verify 可跳过且仅属用户)+post-merge(远端 CI 触发,只推送,不挂等待);合回链之外 git 层不设车道钩子——用户手工改动不受限;ZCode 层只约束 agent(子代理不经守卫层)
5. **会话单区(2026-11-04 用户裁)**:建区粒度=回合——同一时刻每会话至多一个 worktree 在役;合入删区之前,会话内用户追加的事项一律并入在役区推进,禁为追加事项另开新区。区内全部事项完成后只做一次合入(一次 `git merge --no-ff`、post-merge 一次推送、远端 CI 只跑一次),追加事项在合入提交信息里分列;无改动需求的事项不建区。合回删区后用户再发令的事项=新回合,另开新区(区名取该回合主任务名)。CI 红的 `fix-<任务名>` 修复区属同一回合的追绿延续,不另算新区。迭代实现批(iter<N> 形分支)独占回合——其他类事项不得并入迭代区(合回守卫数卡校验本就拦未归卡提交),待其合回后按新回合另开。
## 2. 五段流水线
@@ -49,8 +49,8 @@
7. **验收改动先行声明**——验收变化(基线换代/探针增改/旧断言改判)预先写进需求卡片,禁事后补
**完成**
8. **合回后 CI 验证(唯一强制全量验证点)**——主工作区 `git merge --no-ff <任务分支>` 落地后 post-merge 钩子自动推送 origin master 触发远端 CI;远端按改动域分流(§1 第 3 条),全量档兜的是跨卡触面、探针与未申报套件的消费漏网(迭代 44 实证),判定与红项用 `tools/ci_check.sh <合并提交>` 按需现查(读 run 终局态,只呈报不落盘)。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。`tools/run_all_gates.sh` 是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。开发期的验证义务与选档一律见 §4,CI 全量档不替代它。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占 CI 全量——其质量义务=按 §4 把该件实跑一遍(最小参数全路径含产出落盘)并核对判据口径未变;闸真崩溃时远端 CI 判红在案(job 失败=verdict 红,fail-closed 语义保留)
9. **重跑语义(2026-10-30 用户裁)**——同一份字节禁止第二次人手跑;允许的重跑只剩两类:① CI 红后的修复合回(post-merge 钩子再推再验,每轮修复至多触发一轮);② 用户自发发起的全量回归。「跑都跑了再跑一遍保险」「顺手改点再补一跑」都不构成重跑理由。导入缓存刷新由远端工作流「导入缓存刷新」步接管,合并后无手工动作
8. **合回后 CI 验证(唯一强制全量验证点)**——主工作区 `git merge --no-ff <任务分支>` 落地后 post-merge 钩子自动推送 origin master 触发远端 CI;远端按改动域分流(§1 第 3 条),全量档兜的是跨卡触面、探针与未申报套件的消费漏网(迭代 44 实证),判定与红项用 `tools/ci_check.sh <合并提交>` 按需现查(读 run 终局态,只呈报不落盘)。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。`tools/run_all_gates.sh` 是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。开发期的验证义务与选档一律见 §4,CI 全量档不替代它。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占 CI 全量——其质量义务=按 §4 把该件实跑一遍(最小参数全路径含产出落盘)并核对判据口径未变;闸真崩溃时远端 CI 判红在案(job 失败=verdict 红,fail-closed 语义保留)。合回前的 pre-merge-commit 前置检查(§1 第 4 条 ③)是当场拦截面不是验证台账——不落回执不入基线,不改变本条「唯一强制全量验证点」口径
9. **重跑语义(2026-10-30 用户裁)**——同一份字节禁止第二次人手跑;允许的重跑只剩三类:① CI 红后的修复合回(post-merge 钩子再推再验,每轮修复至多触发一轮);② 用户自发发起的全量回归;③ 合回 pre-merge-commit 前置检查(§1 第 4 条 ③——与合回后远端跑批判面不同档〔拦截面 vs 台账〕,不算同一判面的二次人手跑)。「跑都跑了再跑一遍保险」「顺手改点再补一跑」都不构成重跑理由。导入缓存刷新由远端工作流「导入缓存刷新」步接管,合并后无手工动作
10. **批次完成后的登记义务**——合回 master 后按 doc-conventions §12.1 推进待办清单条目状态(做完整条删除),涉及迭代编排的在 roadmap 迭代总表与顶部「当前位」节换代——执行入口=项目技能 batch-closure(批后收尾与中间文档退役),跨会话教训写记忆库。批次过程与结论由 git 提交信息承载,禁止再写会话状态文件(见 doc-conventions 第 10 节)。`design/` 与根规则文件(`AGENTS.md`/`roadmap.md`/`待办清单.md`/`体验验证清单.md`)实现批禁写,见 §12
11. **文档同步闸(实现批禁写文档)**——实现批**禁写 `design/` 整个目录与根规则文件**,**另禁写 `game/tests/README.md`(能力清单,归写卡段维护,test-conventions §8.9)**,要往外交代的东西一律落 `reports/`:实现报告 `reports/iter<N>-实现报告-<日期>.md`(各卡状态+实机目检总清单+遗留读数义务+实现决定记录——超白名单/超卡片明列范围事项逐条记「决了什么/为什么/代价/撤销方式」,全部按卡执行时只写一行「零拍板」)、GDD 反哺点清单 `reports/iter<N>-反哺点-<日期>.md`(每条写清哪个文件哪一节、改成什么、依据是什么)。设计权威文档(GDD/art-bible)**任何批都不直改**:用户在会话中逐点裁决后,由工程/文档设计改动批(直改类)写入 `design/`(执行入口=doc-design 技能),并把反哺条目从 `reports/` 清掉(吸收即死;退役执行=batch-closure 技能)。机械面文档(roadmap 行/术语表/README 工具注)同样归工程/文档设计改动批在合并前更新。反哺点未落地期间相关 GDD 节视为待反哺,读前先查 `reports/` 与待办清单;需求卡片必列「关联文档同步面」按两层标注
12. **实机目检归用户,只登记不阻塞**——真人检测项登记进实现报告目检总清单与体验验证清单即完成批内义务;禁把等目检当停工理由或完成前置
+7 -3
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 轮;
@@ -27,7 +29,7 @@
## 4. 验证时机(合回后远端 CI 制,2026-11-03 换代)
开发期义务=`process-conventions.md` §4 改完即测铁规:本批新增/改动的验收测试自己跑绿、读数写进该步提交信息才算完成(卡内只测自己改的东西)。全量验证归合回后的远端 CI:git merge 合回 master 落地后 post-merge 钩子推送 origin master,远端按改动域分流跑 `tools/run_all_gates.sh`(游戏码批=不带参数档两域全量、单元与集成测试全套件——跨卡触面与关联档选测的盲区由全量兜底;文档批=文档域秒级闸;分流算法单点=`tools/ci/domain_route.sh`),远端结果用 `tools/ci_check.sh <合并提交>` 按需现查(读 run 自身 `status`/`conclusion`,只呈报、不落盘——合回不等待、不唤醒,process-conventions §1 第 3 条);CI 红态下修复合回落地即再推再验追绿(修复走新开 `fix-<任务名>` 修复 worktree,process-conventions §1)。同一份字节禁止第二次人手跑。该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。
开发期义务=`process-conventions.md` §4 改完即测铁规:本批新增/改动的验收测试自己跑绿、读数写进该步提交信息才算完成(卡内只测自己改的东西)。合回 merge 提交落笔前另有 pre-merge-commit 前置检查(2026-11-06 用户裁:冒烟+单元与集成全套件 4 分片,约 2 分钟——当场拦截面,不落回执;细则见 process-conventions §1 第 4 条 ③)。全量验证归合回后的远端 CI:git merge 合回 master 落地后 post-merge 钩子推送 origin master,远端按改动域分流跑 `tools/run_all_gates.sh`(游戏码批=不带参数档两域全量、单元与集成测试全套件——跨卡触面与关联档选测的盲区由全量兜底;文档批=文档域秒级闸;分流算法单点=`tools/ci/domain_route.sh`),远端结果用 `tools/ci_check.sh <合并提交>` 按需现查(读 run 自身 `status`/`conclusion`,只呈报、不落盘——合回不等待、不唤醒,process-conventions §1 第 3 条);CI 红态下修复合回落地即再推再验追绿(修复走新开 `fix-<任务名>` 修复 worktree,process-conventions §1)。同一份字节禁止第二次人手跑。该脚本是全部验证闸的唯一入口与唯一清单,域划分与选闸算法只写在脚本里,本文禁另列闸件清单。
行为探针使用随机化玩家行为(封印/停手/刷新/无效点击)——自动玩家不做的动作就是测试盲区。
## 5. 测试函数怎么写——验收清单条目的落码规范
@@ -77,7 +79,7 @@
### 8.2 四层与真实入口
- **单元**:不依赖场景树和真实宿主的纯逻辑;测试从领域类的对外方法进出,断言钉对外可观测面(返回值/信号/查询接口/落盘内容)。内部字段直查=受控例外:仅当该行为没有公开可观测面可钉时允许,对照表该行注明理由;存量不回头清扫,被批次触碰时顺带换代。
- **单元**:不依赖场景树和真实宿主的纯逻辑;测试从领域类的对外方法进出,断言钉对外可观测面(返回值/信号/查询接口/落盘内容)。内部字段直查=受控例外:仅当该行为没有公开可观测面可钉时允许,对照表该行注明理由;存量不回头清扫,被批次触碰时顺带换代。探针直读视口承载子件结构(画布取视口首子件、渐隐带两片与平移提示子件按名取)属此类——断言对象即承载结构本身,无公开可观测面可钉,理由随对照表行申报。
- **接口**:模块边界的公开契约——数据表加载、存档与断点、事件载荷、命令接口;测试从公开入口进出。接口级断言随单元套件落(`unit/contracts/`),不另设承载目录。
- **集成**:跨宿主的接线、持久、生命周期——例如 LevelRunner 把 level_counters 传给 BattleOrchestrator、两场战斗共享同一个桶、断点续玩;测试必须实例化真实宿主链,禁止用裸 EffectEngine 代替。
- **探针/golden**:玩家可达性与观感;真实实例化场景、驱动真实输入。
@@ -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` 头部。
-6
View File
@@ -47,12 +47,6 @@
| 专名 | 是什么 | 出处条目 |
|---|---|---|
| 显影尘 | 信息类秘宝 | aux_items |
| 工兵令 | 加行动点的秘宝 | aux_items |
| 探宝罗盘 | 预读奖励身份的秘宝 | aux_items |
| 探巢摆钟 | 探知巢穴位置的秘宝 | aux_items |
| 地穴全图卷 | 全图级稀有秘宝 | aux_items |
| 爆破工兵包 | 行动点 + 探巢的稀有秘宝 | aux_items |
| 地精 / 熊地精 / 食人魔酋长 | 首发三敌种(末者为 Boss) | enemy |
| 第一层 · 地精洞窟 | 首关层名 | level_table |
| 封印 | 正式动作名(全库统一,非比喻、非呈现层别名)——对未开格实施封印,印记=封印印记 | terminology |
+1 -1
View File
@@ -38,7 +38,7 @@ MVP 不加新敌种,也不做同深度多敌种混搭;敌种扩容走 `enemy
目录(重设计 v1)已定稿 **12 件**(普通 7 / 优秀 3 / 稀有 2——代号制,正式命名归用户;数值全 provisional 归调参批),现行事实源=[秘宝](systems/跨局循环/秘宝.md)「目录(重设计 v1)」节。数据行随实装批落表(同调件数据表 = `data/tables/treasure_items.tres`),MVP 不新增。
旧目录(旧 aux_items 六行:显影尘/工兵令/探宝罗盘/探巢摆钟/地穴全图卷/爆破工兵包,及过渡形态诸件)已全数退役、只作素材库(design/content-archive/道具退役素材库.md);旧行删除归实装批。
旧目录(旧 aux_items 六行:显影尘/工兵令/探宝罗盘/探巢摆钟/地穴全图卷/爆破工兵包,及过渡形态诸件)已全数退役、留底 design/content-archive/道具退役素材库.md(数据表旧行已删净——treasure_items.tres 旧六名扫描零命中)。
## 增益与状态
@@ -1,7 +1,7 @@
# 地图战斗资源
> 一句话:格面资源的产出、极性字典与清图奖励封装
> 状态:已定稿;现行为 v1.5
> 状态:已定稿;现行为 v1.6
## 设计目的
@@ -96,7 +96,7 @@
- **档由两个参数定义:概率位 + 保底上限**。「永不保底」不单立规则——T4 的保底上限 = 0 即其统一表达;T1/T2 上限同为 0。
- **保底数量挂在型上、不挂在档上**:每个型各自登记「每板至少出几个」。档内登记保底的型数 ≤ 档上限 → 全部兑现;> 上限 → 每板从登记型中随机选定「上限」个兑现,其余型该板按普通概率出现、不保底。
- **补签纪律**:执行序 = 装板摇号(含偏运硬币投影)→ 按上限定本期保底型集合 → 校验补签 → 板上全程只读。补签只改类型身份:危险格与安全格不动(补签格可为雷可为安全格,危险格与安全格同池不破)、不消费权重(补签不是一次取号)、清图判定不动、不泄露元信息(补签格与摇号格不可区分)。粒度 = 板,换板重新校验。
- **补签纪律**:执行序 = 装板摇号(含订阅加权投影)→ 按上限定本期保底型集合 → 校验补签 → 板上全程只读。补签只改类型身份:危险格与安全格不动(补签格可为雷可为安全格,危险格与安全格同池不破)、不消费权重(补签不是一次取号)、清图判定不动、不泄露元信息(补签格与摇号格不可区分)。粒度 = 板,换板重新校验。
- **GP 数值锚**:以战斗结束 GP 奖励为锚——小 GP 袋 ≈ 其 1/10、大宝箱 ≈ 其 1 倍(具体数值归调参批)。
- 大宝箱为枚举新增(九型→十型走本文修订流程,不随手加行);微光归通用增益组(五色相不变,组内不细分正好容下)。
- **对外稀有度映射**(现行事实源=[核心循环](../../01-核心循环.md)「档位与获取规则」节):T1=普通 / T2=优秀 / T3=稀有 / T4=传说;任何概率提升禁止改变本档保底承诺(保底=承诺非概率)。
@@ -1,7 +1,7 @@
# 战斗结算
> 一句话:轮末结算的编排与逐行兑现
> 状态:已定稿;现行为 v1.7
> 状态:已定稿;现行为 v1.8
## 设计目的
@@ -84,7 +84,7 @@
| 判定行 | 胜 / 负 / 继续各一行;终局判定行同时是胜负画面的数据锚 |
| 高光行 | 完美封印成立时一行,只记实际有效兑付 |
每行还携带公共字段:来源类别、来源格、轮次、显示名、数值,以及可空的高光标记(用于强调封印标中、清图奖励这类值得高亮的结果)。奖励直伤行的数值字段沿用 `effectiveness` 键名、值为整数点数——键名与量纲是数据导出与分析工具的兼容契约,改动须连动分析工具面。没生效也要留痕:两类"未生效"行如实记录合法例外,呈现时聚合计数,不逐行刷屏。
每行还携带公共字段:来源类别、来源格、轮次、显示名、数值,以及可空的高光标记(用于强调封印标中、清图奖励这类值得高亮的结果)。**显示名等玩家可见文案是数据面字段:文案值登记在数据表行,代码按来源类别与效果身份读表拼行,不在代码里写死文案字串**(2026-11-06 用户裁——文案归属数据表承载;承载表与字段形态归表结构设计段定,设计闭合后由实装批落表)。奖励直伤行的数值字段沿用 `effectiveness` 键名、值为整数点数——键名与量纲是数据导出与分析工具的兼容契约,改动须连动分析工具面。没生效也要留痕:两类"未生效"行如实记录合法例外,呈现时聚合计数,不逐行刷屏。
任何一行找不到来源即断言失败——宁可返回错误,也不让"合并到看不出来源"的呈现混过数据层。但有些行天然没有棋盘来源,断言为此留了两层豁免,豁免集合是封闭清单,改动同样走本文修订。
@@ -87,7 +87,7 @@ HUD框架站在玩家与全部玩法系统之间:挖掘期它是输入的翻
| toast | 非模态 | 自动消退 2.5 秒,不打断挖掘 |
| 图例面板 / 明细回看 / 胜负终局 | 模态 | 同一张登记序栈:统一入栈出栈、后开置顶、关闭逐层恢复 |
两条纪律横跨两族。其一,"第一次遇到"类提示(本作没有双击快开、挖不相邻格要先走近)走 toast,每场冒险至多弹一次。其二,拒绝反馈不属于一次性族——每次被拒都要可感知,且图标与文案双通道冗余(不靠颜色单独承载);棋盘面拒绝(动作不合棋盘规则,目标格边框闪)与资源面拒绝(行动点或储备不足,数字脉冲)必须可区分。图例面板恒可随时打开回看,不设解锁门。
三条纪律横跨两族。其一,"第一次遇到"类提示(本作没有双击快开、挖不相邻格要先走近)走 toast,每场冒险至多弹一次。其二,拒绝反馈不属于一次性族——每次被拒都要可感知,且图标与文案双通道冗余(不靠颜色单独承载);棋盘面拒绝(动作不合棋盘规则,目标格边框闪)与资源面拒绝(行动点或储备不足,数字脉冲)必须可区分。图例面板恒可随时打开回看,不设解锁门。其三,**确认弹窗的按钮位次定值**:推进操作的钮在左、维持现状的钮(取消/留下/否)在右,三钮形态的次推进钮居中;全项目确认弹窗共用同一件、同一序,四处消费方(删档确认/营地大门有断点的三出口/营地大门无断点是否开局/篝火离场)皆然。**位次不随文案变**,语义由文案承担。
### 呈现层订阅纪律:界面不自持状态、订阅面只留两条线
+4 -4
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)(主菜单语境)
@@ -20,13 +20,13 @@
| 木桩 | 天赋树 | |
| 布告板 | 奖励格图鉴 | 内容冻结未激活,点进看占位——**不是禁用态** |
| 篝火 | 离开营地回主菜单 | **必须二次确认**;命中区只含火堆、不含火焰 |
| 帐篷 | — | 承载件,**不可点**;落地尺寸与占位范围待作者重定(待裁口径见 art-bible §6.2) |
| 帐篷 | — | 承载件,**不可点**;251×170 落位 (982, 335),上端遮住围墙下部;笔记本在其左下方外侧、两者不重叠 |
纵深=远景天空与远山/中景木围墙与营地大门/近景篝火与左右物件(作者纵深稿)。中景围墙是横贯全宽的密排竖木桩墙——木桩一根挨一根无缝、桩顶削尖、无横木,只在中段留缺口给大门。
**场景按要素分层(10 张)**:远景天空/远山/整片地面/门洞内的小路合成 `scene_camp_bg` 背景层(z0,不透明全幅);中景木围墙独立成 `scene_camp_wall` 围墙层(z1,透明底,门洞处留透明缺口);其余八个物件层各一张、由上往下叠。门洞是围墙层的透明缺口,透出背景层画的**一条通向远处的小路**,不可填死。
**中景带自画面底部往上约 2/3 处起**;近景物件允许压住围墙(围墙不可点,z 序在近景物件之下),**唯一例外=中间门洞必须留空、不被任何物件遮挡**。围墙形态、门扇比例与帐篷尺寸的现行口径归 [art-bible](../../art/art-bible.md) §6.2,本 spec 不再复述。
**地平线在终稿 y367**(自画面底部往上约 46%),其上是天空与远山、其下整片地面;**中景围墙带 y203–367**,桩身 150、门柱 164;近景物件允许压住围墙(围墙不可点,z 序在近景物件之下),**唯一例外=中间门洞必须留空、不被任何物件遮挡**。围墙形态、门扇比例与帐篷尺寸的现行口径归 [art-bible](../../art/art-bible.md) §6.2,本 spec 不再复述。
坐标草案与分层验收判据见 [camp-scene-prompts](../../art/camp-scene-prompts.md) §2.3/§4(10 张口径现行事实源);2026-10-22 美术方案讨论沿革查 git 提交史。
@@ -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〉。刷新入口已实装=营地刷新单口(进档与营地内写口尾触发,现开界面与顶栏余额同步取现值)。
## 叠层与开合
+2 -2
View File
@@ -1,7 +1,7 @@
# 术语表
> 全库专用术语的唯一对照表(2026-10-11 用户令建立)。规矩:新术语先登记本表、再在文档使用;退役词见文末对照表,文档正文一律使用现行词。文档中遇到看不懂的词,查本表。用词纪律(禁用词、替换值、豁免范围)见「用语规范」节。
> 状态:**现行 v1.26**——2026-10-11 建立;历次升版明细见 git 提交史。
> 状态:**现行 v1.27**——2026-10-11 建立;历次升版明细见 git 提交史。
## 格与棋盘
@@ -95,7 +95,7 @@
| 术语 | 定义 |
|---|---|
| 效果端口件 | 战斗业务类(伤害管线/行动经济)唯一的效果发射出口:收事件与载荷、交效果引擎结算、取回命中批——业务类零引擎直连(机制归属见架构文档「战斗编排域拆件定稿」节轴戊;实现类 EffectPort) |
| 掷格件 | 装配期格面资源摇号的承载件:池行装配、区间行 roll、权重合成(偏运硬币/board_skew 两路投影);受理账面留守地图战斗资源件(机制归属同上;实现类 RewardRoller) |
| 掷格件 | 装配期格面资源摇号的承载件:池行装配、区间行 roll、权重合成(订阅加权/board_skew 两路投影);受理账面留守地图战斗资源件(机制归属同上;实现类 RewardRoller) |
| `seal_missed` | 封印落空事件(旧名已换代):封印动作落在安全格上时发出,订阅面据此做限次与豁免判定 |
| `forgive_seal_miss` | 本场首次封印落空豁免——专长「完美保险」的效果类型,`domain=battle` 首例;有效误标=误标总数 − 豁免数,豁免不改写误标账 |
+1 -1
View File
@@ -79,7 +79,7 @@ func try_spend(action_kind: String) -> bool:
# 排雷玩法 挖掘专用:chain_size = 连锁附加格数(不含首格);clears_map = 本次动作后全图安全格开完。
# 深挖钻头付费开掘(dig_paid counter_spend 订阅施加面):按序消耗族——AP 富余(≥基础+对价)时本动作付全价并积本轮付费计数(效果引擎
# 付费开掘(dig_paid counter_spend 订阅施加面):按序消耗族——AP 富余(≥基础+对价)时本动作付全价并积本轮付费计数(效果引擎
# 计数器=授予段追加产出配额取数面);AP 不足全价时 fallback 降级只付基础 1 照常开格、不积计数
# ——持有钻头永不锁死普通挖格(散件保底 B-2)。
func request_dig(chain_size: int, clears_map: bool) -> bool:
+2 -2
View File
@@ -255,7 +255,7 @@ func begin_battle(enc: Dictionary) -> bool:
_begin_battle_fx_state(enc)
_begin_battle_attune_params(enc)
_begin_battle_preview_economy(enc)
# 工兵晨令=round_start grant_ap(mode:queue) 订阅行承载(when round_index ≤ effective_rounds
# 晨间 AP=round_start grant_ap(mode:queue) 订阅行承载(when round_index ≤ effective_rounds
# value_from 定影);round_start 发射先于行动经济轮切换=树规则 queue 型当轮入队当轮兑现
# (先手压制=首轮 AP+1 语义锚);计数器 round_start 重置点同点生效
round_index = 1
@@ -535,7 +535,7 @@ func _exit_distribution() -> void:
if not _install_and_reset():
_fatal("换图编排失败")
return
# 工兵晨令续队直调拆除(内容数据驱动扩展批 T4——round_start 订阅行承载,见 begin_battle 注)
# 晨间 AP 续队直调拆除(内容数据驱动扩展批 T4——round_start 订阅行承载,见 begin_battle 注)
action_economy.notify_settlement_done(mode)
# round_start 发射先于行动经济轮切换(queue 型 grant_ap 当轮入队当轮兑现——与首轮同口径)
round_index += 1
+19 -66
View File
@@ -106,7 +106,7 @@ func _per_value(name: String, payload: Dictionary) -> int:
return 0
# 巢穴控制数(fact nest_count 取数面——敌方名下 weaken 实例总数,猎巢人订阅面;
# 巢穴控制数(fact nest_count 取数面——敌方名下 weaken 实例总数,nest_count 订阅消费面;
# 个体化后削弱为累加行、每体各持一条——总数 = 被削弱个体数)
func _nest_count() -> int:
var n := 0
@@ -135,7 +135,7 @@ func submit(t: Dictionary) -> int:
# attack_commit 发射点(我方伤害单据开立入口;
# 命中挂单据 context 随行——施加面在 _resolve_hero_attack 管线算术前逐族施加,开单时点=主角攻击
# 单恒为段内首单(结账现场与开单现场一致)。载荷 atk/target/matchup + 挖掘 streak 事实
# (执拗订阅面)+巢穴控制数〔猎巢人〕)
# (开格 streak 订阅面)+巢穴控制数〔nest_count 订阅面〕)
# spec 束必载键(缺载 assert——fail-fast):channel/base_value/target_rule/ri;
# 可选键缺省=原形缺省:origin_cell=(-1,-1)/attack_type=""/rider=0/highlight=""/context={}。
# 返回单据字典九栏目+requires/source_unit 逐字不变(契约 8)。
@@ -247,9 +247,9 @@ func _resolve(t: Dictionary) -> void:
# 主角先手攻击:掷点 → 折扣/暴击 → 克制点数 → hero_offense×enemy_defense 池乘区 → 下限 → 交付
# (attack_commit/clamp_check/armor_roll 订阅施加面:命中=开单入口随单
# context 携带,按固定算序逐族施加——命中序=数据表声明序,确定性)。
# context 携带,按固定算序逐族施加——命中序=数据表声明序,确定性;掷点折扣读数→克制点数两拍)。
# 四步分段:命中面(_hero_attack_ctx,目标/护甲/
# 掷点/按序改写族四拍/克制点数) → 基值装配(_hero_attack_base_value,加法族/计数器施加/倍乘) →
# 掷点/掷点折扣读数/克制点数) → 基值装配(_hero_attack_base_value,加法族/计数器施加) →
# 掷点兑现(_hero_attack_redeem,rolled/池乘/下限/rider/落地/击杀发射) → 交付(_hero_attack_deliver,
# 状态施加/标签/明细行)。步骤间经 ctx/redemption 字典穿线——算序/取值固定(🔴 逐位一致契约)。
func _resolve_hero_attack(t: Dictionary) -> void:
@@ -261,7 +261,7 @@ func _resolve_hero_attack(t: Dictionary) -> void:
_hero_attack_deliver(t, ctx, redemption)
# 命中面(目标/护甲克制 matchup/armor_roll 发射→按序改写族四拍→克制点数读值+订阅改写;
# 命中面(目标/护甲克制 matchup/armor_roll 发射→掷点折扣读数→克制点数读值+订阅改写;
# 后两步各由 _ctx_rewrites/_ctx_clamp_delta 承载)。返回穿线 ctx(空=目标不可达)。
# clamp_check 只在 _ctx_rewrites 内发射一次——_ctx_clamp_delta 消费同批命中(禁重复发射)。
func _hero_attack_ctx(t: Dictionary) -> Dictionary:
@@ -278,54 +278,25 @@ func _hero_attack_ctx(t: Dictionary) -> Dictionary:
return {"target": target, "armor": armor, "atk_type": atk_type,
"ac_hits": (t.context.get("fx_attack_hits", []) as Array), "ac_payload": ac_payload,
"roll_read": rewrites["roll_read"], "threshold": rewrites["threshold"],
"crit": rewrites["crit"], "discount": rewrites["discount"],
"spike_ready": rewrites["spike_ready"], "cdelta": cdelta}
"crit": rewrites["crit"], "discount": rewrites["discount"], "cdelta": cdelta}
# 按序改写族四拍+掷点快照(armor_roll 发射点在此):第一拍矿工重锤命中交换(正负双修原生形态:
# 命中判定减成 trade_hit 点〔更易命中〕换攻击增伤 trade_atk%——高 AC 负期望/低 AC 正期望;
# 改写位先于掷点折扣读数)→ 第二拍专注免掷骰恒定折扣(方差买断成期望——稳定器人格;roll 记 0 归因)
# → 第三拍满层必中(spike ready 时 discount 恒全额、暴击面走倍乘不加掷)→ 第四拍猎弱本能对轻甲敌折扣档改善 value 档
# (对冲 pierce 列 L1 −1 开局罚;改善 = 折扣表向全额方向移档,nat 最大面/已全额不重复)。
# clamp_check 同在此发射(唯一发射点——命中随返回值穿给 _ctx_clamp_delta)。
# 掷点折扣读数+克制点数命中批(armor_roll 发射点在此):一拍护甲掷骰折扣读数(掷点对照
# 防御阈值定折扣档——阈值=防御 AC+攻击加值,nat 面沿折扣表)→ 二拍 clamp_check 发射
# (唯一发射点——命中随返回值穿给 _ctx_clamp_delta)。
func _ctx_rewrites(t: Dictionary, target: Dictionary, armor: String, matchup: String) -> Dictionary:
var ac_hits: Array[Variant] = (t.context.get("fx_attack_hits", []) as Array)
var roll_read: Dictionary[Variant, Variant] = _armor_roll()
var threshold: int = defender_ac(target) + int(db.combat.settlement.damage_roll.attack_bonus)
# armor_roll 发射点(敌护甲掷骰判定;免掷恒折扣订阅面)
var ar_hits := _fx_hits("armor_roll", {"target_ac": threshold, "roll": int(roll_read.roll)})
var trade_hit := 0
for h in ac_hits:
if String(h.get("kind", "")) == "modify_ticket" and int(h.get("trade_hit", 0)) > 0:
trade_hit += int(h.trade_hit)
if trade_hit > 0:
threshold = maxi(1, threshold - trade_hit)
# armor_roll 发射点(敌护甲掷骰判定——事件在册、现役零订阅行,发射面保留)
_fx_hits("armor_roll", {"target_ac": threshold, "roll": int(roll_read.roll)})
var crit: bool = bool(roll_read.crit)
var discount: int = _roll_discount(int(roll_read.roll), threshold)
for h in ar_hits:
if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "armor_rewrite":
discount = int(h.get("value", 0))
crit = false
roll_read["roll"] = 0
var spike_ready := false
for h in ac_hits:
if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "spike":
spike_ready = true
discount = 100
var clamp_hits := _fx_hits("clamp_check", {"matchup": matchup})
var shift := 0
for h in clamp_hits:
if String(h.get("kind", "")) != "modify_ticket":
continue
if String(h.get("mode", "")) == "discount_shift":
shift += int(h.get("value", 0))
if shift > 0 and armor == "light" and discount < 100:
discount = _discount_shift_up(discount, shift)
return {"roll_read": roll_read, "threshold": threshold, "crit": crit,
"discount": discount, "spike_ready": spike_ready, "clamp_hits": clamp_hits}
"discount": discount, "clamp_hits": clamp_hits}
# 克制点数读值+订阅改写(魔网织者/湛蓝誓约/猎手刺剑 clamp_check modify delta——克制矩阵查表值
# 克制点数读值+订阅改写(clamp_check 带模式行 modify delta——克制矩阵查表值
# 后的直调;hero_attack 与 board_typed 两消费面同享)。命中批由 _ctx_rewrites 单次发射后传入。
# 淬毒刃纹(行 triggers clamp_check
# modify(mode:clamp_bonus) 订阅命中并入本处 delta 合计;mode 路由=主角攻击面专享,
@@ -338,8 +309,8 @@ func _ctx_clamp_delta(atk_type: String, armor: String, clamp_hits: Array) -> int
return cdelta
# 基值装配(基值+单位标记加伤+矿工重锤增伤面+加法族(attack_bonus/
# value_counter 层值)+计数器施加(attack_commit 用后清零行——命中序=声明序)+spike 倍乘。
# 基值装配(基值+单位标记加伤+加法族(attack_bonus/
# value_counter 层值)+计数器施加(attack_commit 用后清零行——命中序=声明序)。
func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float:
var ac_hits: Array[Variant] = ctx["ac_hits"]
var ac_payload: Dictionary[Variant, Variant] = ctx["ac_payload"]
@@ -347,10 +318,6 @@ func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float:
# 单位标记加伤(处刑台 target_attack_bonus 挂单位字典 fx_atk_bonus 键;
# 阈值沿发射点已写入,本处通用消费——对该敌伤害加算)
base_value += float(int((ctx["target"] as Dictionary).get("fx_atk_bonus", 0)))
# 按序改写族第五拍:矿工重锤增伤面(交换的正向侧)
for h in ac_hits:
if String(h.get("kind", "")) == "modify_ticket" and int(h.get("trade_atk", 0)) > 0:
base_value *= 1.0 + float(int(h.trade_atk)) / 100.0
# 加法族:attack_bonus per 计数器层值(层数×value 加算)。
# value_counter 扩展〔疼痛记忆/弃图兑换/陷阱本能——rider 值=计数器账面,
# 值读于施加时点=命中序先行 rider 后清零行〕
@@ -365,11 +332,6 @@ func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float:
if String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
_port.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
# 任意真族:modify_ticket mode=spike 满层倍乘(强制必中已在折扣拍承载)
if ctx["spike_ready"]:
for h in ac_hits:
if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "spike":
base_value *= float(int(h.get("value", 1)))
return base_value
@@ -391,7 +353,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_offense", StatusContainer.OWNER_HERO), clamped_pool("enemy_defense", StatusContainer.owner_key_for(target)))
var landed: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), pooled)
# 加法族:英雄侧 rider(enemy_skill rider 对称位补齐口径)——后置必中附加,独立于掷点与乘区
# (奥术余烬「普攻命中后恒定结算」语义);猎巢人巢控光环(per nest_count)/矿工的执拗开格加成
# (普攻命中后恒定结算语义);巢控光环(per nest_count)/开格 streak 加成
# (per dig_streak、cap 封顶)同通道合计;档一质变 rider+1 / 档二(crit 时)rider 乘暴击倍率。
# rider 不改变下限语义(landed 含 rider 后不再过 floor)。
var hero_rider := int(_fact_or("rider_tier_bonus") or 0) # or 0=未绑缺省与 int(null) 缺省同值(int() 构造拒 null——端口缺省 null 面的空值合并)
@@ -421,7 +383,7 @@ func _kill_effects(t: Dictionary, target: Dictionary) -> void:
_port.reveal_effect() # 斩首学者——击杀揭示 1 格(端口路由宿主入队,授予段末兑付)
# 交付:即时执行族(碎甲震击打重甲附加「护甲裂解」——复用 off_guard,零新状态行;
# 交付:即时执行族(重甲附加「护甲裂解」——复用 off_guard,零新状态行;
# apply_effect 域闭/重复授予由状态容器自行守卫)/标签装配/明细行发射/兑现登记。
func _hero_attack_deliver(t: Dictionary, ctx: Dictionary, redemption: Dictionary) -> void:
var payload: Dictionary[Variant, Variant] = redemption["payload"]
@@ -524,11 +486,11 @@ func _resolve_board_typed(t: Dictionary) -> void:
var armor_b: String = String(target.get("armor_type", "light"))
var atk_b: String = String(t.attack_type)
# clamp_check 发射+施加(克制判定修正点棋盘面;与 hero_attack 同表同点:
# modify delta 订阅改写合计进克制点数;discount_shift 档位面属主角攻击专享,本面不施加)
# modify delta 订阅改写合计进克制点数;带模式行只施放于主角攻击面,本面不施加)
var cdelta: int = _counter_delta(atk_b, armor_b)
if atk_b != "":
for h in _fx_hits("clamp_check", {"matchup": atk_b + "_vs_" + armor_b}):
# 无模式 delta 才入棋盘窗(mode 路由:clamp_bonus=主角攻击面专享、discount_shift=折扣档面)
# 无模式 delta 才入棋盘窗(mode 路由:带模式行=主角攻击面专享,棋盘窗只吃无模式 delta)
if String(h.get("kind", "")) == "modify_ticket" and h.has("delta") and String(h.get("mode", "")) == "":
cdelta += int(h.delta)
var dealt: int = DamageMath.counter_component(int(t.base_value), cdelta, int(db.combat.settlement.damage_roll.damage_floor))
@@ -619,15 +581,6 @@ func _counter_delta(attack_type: String, armor: String) -> int:
return int(row.get(armor, 0))
# 折扣档改善(猎弱本能——折扣表向全额方向移 value 档;查不到当前档 = 不动防御口径)
func _discount_shift_up(discount: int, shift: int) -> int:
var tbl: Array[Variant] = db.combat.settlement.damage_roll.roll_tier_discounts
var idx := tbl.find(discount)
if idx < 0:
return discount
return int(tbl[mini(tbl.size() - 1, idx + shift)])
# 池内加算 → clamp [-100,+100](池间乘算归 DamageMath.pool_damage 调用侧);
# 池按个体读(增益状态 v1.5):主角侧池传 OWNER_HERO,敌方侧池传被打者/施放者个体键——
# 乘区只合计该个体名下实例(B 敌的削弱不进 A 敌的账)
+6 -6
View File
@@ -27,7 +27,7 @@ const MINE_EVENTS := ["dig_reveal", "dig_paid", "seal_hit", "mine_hit", "seal_se
## bias_rewards/preview。需求卡片标题计 23 系笔误,落码按行内容全收——commit 留痕)
const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open_ticket",
"attack_bonus", "yield_mult", "spawn_spell", "grant_material",
"grant_ap", "grant_gp", "bias_wallet", "shield", "spike", "apply_status", "counter_add",
"grant_ap", "grant_gp", "bias_wallet", "shield", "apply_status", "counter_add",
"counter_spend", "reveal_tiles", "remove_mines", "refresh_pick",
"bias_rewards", "preview",
# 六型效果(奇物.md 基础件表 27 件声明清单):
@@ -61,8 +61,8 @@ const FACTS := ["matchup", "is_first_dig_of_battle", "terrain_kind", "target_arm
"ap_exhausted_last", "hp_ratio", "amount", "ap_left", "pct",
"battle_index"] # 关卡域战斗序(1 基,enc 携带;开局姿态组「每关首轮窗」条件面)
## when 算子词汇(封闭——fact 查询面落法:mod_zero=计数器取模判满〔掘进蓄能〕、
## advantageous=克制矩阵正面判定〔淬毒刃纹〕、gte_ref/lt_ref=与另一计数器比较〔深挖钻头配额〕、
## when 算子词汇(封闭——fact 查询面落法:mod_zero=计数器取模判满、
## advantageous=克制矩阵正面判定〔淬毒刃纹〕、gte_ref/lt_ref=与另一计数器比较(计数器配额面)、
## ends_with=后缀匹配〔对某甲族的泛条件〕、mod_eq=取模余数判定〔双管镐每第 2 次——value=模数
## ref=余数;dispatch 预评估口径〔when 于命中前读计数器现值〕下「每第 N 次」系用 ref=N-1 表达〕。
## 均系映射表现状条件装进 fact 的查询面落法,白名单覆盖)
@@ -79,7 +79,7 @@ const WHEN_KEYS := ["fact", "op", "value", "id", "ref", "value_from"]
## axis=bonus_delta 加算区轴向)
const DO_KEYS := ["kind", "value", "value_from", "delta", "delta_from", "mult", "mult_from",
"cap", "cap_from", "cost_mult", "per", "divisor", "dtype", "mode", "channel", "counter_id",
"set", "status_id", "label", "fallback", "target_kinds", "trade_atk", "trade_hit",
"set", "status_id", "label", "fallback", "target_kinds",
"value_event", "value_counter", "axis"]
## 主动件分发通道(不经事件订阅——刷新/显影/移除是玩家主动动作;use_treasure 按 kind 分发)
@@ -154,7 +154,7 @@ func _push_tree_rule(rk: String, params: Dictionary[Variant, Variant]) -> void:
"do": {"kind": "shield", "value": int(params["shield"])}}, {})
"sense_sharp":
# 锋=踩中危险格伤害减半·每关限次——mine_hit 挂点现成(damage_mult 收集→halve_pct,
# 与惊巢反制同路径;伤害在结算战斗段末兑付,探索阶段零改动)
# 与挖掘惩罚减半同路径;伤害在结算战斗段末兑付,探索阶段零改动)
_push_trigger("tree", rk, {"on": "mine_hit",
"when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": int(params["cap_per_level"])}],
"do": {"kind": "damage_mult", "value": int(params["halve_pct"])}}, {})
@@ -449,7 +449,7 @@ static func _vtr_from_refs(rid: String, tr: Dictionary, row: Dictionary, errs: A
## 加法族(数值相加,可交换):add_ticket_rider、attack_bonus、grant_material、grant_ap、
## counter_add、shield——发射点逐条累加收集器。
## 乘法族(收集后一次施加,正负分桶):yield_mult、damage_mult。
## 任意真族(任一命中即生效):spawn_spell、spike。
## 任意真族(任一命中即生效):spawn_spell。
## 按序改写族(收集→单点按声明序施加):modify_ticket 及一切改写窗口效果。
## 按序消耗族(按订阅序消耗共享资源,耗尽即止;可选 fallback):counter_spend。
## 即时执行族(每条命中各自执行一次,执行序=声明序):open_ticket、
+6 -6
View File
@@ -1,5 +1,5 @@
## 奖励授予件(自 battle_orchestrator 收编 grant 族):极性字典路由兑付(grant_routed——含封印溢价
## 与惊巢反制减半)、三种行应用(flat 四分支/percent/gp)、伤害单据提交面(submit——案C 暂存闸承载)、
## 与挖掘惩罚减半)、三种行应用(flat 四分支/percent/gp)、伤害单据提交面(submit——案C 暂存闸承载)、
## 清图奖励与身份授予(完美封印第二份)、战斗内 GP 缓冲 escrow、秘宝使用四步编排(use_treasure——
## 相位闸→校验→受理闸→逐 kind 应用→消耗)、揭示奖励兑付(reveal_redeem——fx_host flush 的兑付回调面)。
## 生命周期=随编排器(编排器 _init 同建同灭,每战新建)。
@@ -127,7 +127,7 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary:
return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec}
# 受理闸(treasure_used 发射+施加:counter_spend 订阅=掘进蓄能积点账本,积点不足拒绝零消耗。
# 受理闸(treasure_used 发射+施加:counter_spend 订阅=付费积点账本,积点不足拒绝零消耗。
# 零 item_id 特判;拒绝文案读装配集积点进度——宿主 attune 读投影)
func _charge_gate(v: Dictionary, target: Vector2i) -> Dictionary:
var tu_hits := fx_host.fx_emit("treasure_used", {"item_id": String(v.item_id), "target": target})
@@ -180,13 +180,13 @@ func _safe_dig_open(v: Dictionary) -> Dictionary:
return {}
# 爆破工兵包兑付(中立移除危险格:未揭示/无封印/是雷过滤归排雷玩法;
# 中立移除兑付(未揭示/无封印/是雷过滤归排雷玩法;
# 邻接数字冻结零重算、清图分母零涉(危险格不入分母);空集拒绝零消耗——
# consume 之前 return(拒绝面注记扩「③内零消耗分支」),拒绝 reason 可呈现)
func _cell_removal_open(v: Dictionary) -> Dictionary:
var blasted: Array[Variant] = fx_host.minesweeper.remove_mines_in(v.resolved)
if blasted.is_empty():
return {"ok": false, "reason": "爆破工兵包:3×3 区域内无可移除危险格(须未揭示且未封印)"}
return {"ok": false, "reason": "中立移除:3×3 区域内无可移除危险格(须未揭示且未封印)"}
return {}
@@ -218,12 +218,12 @@ func grant_routed(snap: Dictionary, polarity: String, att: Dictionary) -> bool:
row = row.duplicate()
row["power"] = int(round(int(row.power) * prem_pct / 100.0))
row["premium_pct"] = prem_pct
# 惊巢反制(减半系数=mine_hit damage_mult 订阅命中〔行 value 50=减 50%,取整 ceil〕;
# 挖掘惩罚减半(减半系数=mine_hit damage_mult 订阅命中〔行 value 50=减 50%,取整 ceil〕;
# duplicate 隔离——减半不回写极性字典)
if halve_pct > 0 and polarity == "negative" and source_class == "mine_hit":
row = row.duplicate()
var half: int = int(ceil(float(int(row.power)) * float(100 - halve_pct) / 100.0))
emit_cb.call({"type": "effect", "source_class": source_class, "origin_cell": cell}, ri, "惊巢反制——危险格挖掘惩罚减半(%d→%d)" % [int(row.power), half], 0, {"effect_id": "nest_counter"})
emit_cb.call({"type": "effect", "source_class": source_class, "origin_cell": cell}, ri, "挖掘惩罚减半(%d→%d)" % [int(row.power), half], 0, {"effect_id": "nest_halve"})
row.power = half
return _dispatch_row(row, att)
+4 -4
View File
@@ -1,4 +1,4 @@
## 掷格件:装配期格面资源摇号——池行装配、区间行 roll、权重合成(偏运硬币/board_skew 两路投影)。
## 掷格件:装配期格面资源摇号——池行装配、区间行 roll、权重合成(订阅加权/board_skew 两路投影)。
## 自 RewardEconomy 迁出掷格面(architecture v1.29 轴戊②);受理账面(grant/身份查询/快照/retire/
## polarity_row)留守 RewardEconomy——衔接面=格→奖励身份映射批束(本件产出、受理件接收)。
## 取号流声明:roll 每批恰开一 reward_roll 流(scope=每次 install 批不变),体序=normalize(行主序
@@ -38,12 +38,12 @@ func _init(bundle: Dictionary) -> void:
func roll(eligible: Array) -> Dictionary[Variant, Variant]:
var list: Array[Dictionary] = _normalize_eligible(eligible)
var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(格批 + 清图批尾 + 区间行追加取号)
# reward_roll 发射归 BattleFxHost(偏运硬币 bias_rewards 订阅命中合计经注入查询口取回);
# reward_roll 发射归 BattleFxHost(bias_rewards 订阅命中合计经注入查询口取回);
# 零命中/未接线=0=原行集零扰动
var luck_bias := 0
if luck_bias_query.is_valid():
luck_bias = luck_bias_query.call(list.size())
var cell_rows: Array[Variant] = _biased_cell_rows(luck_bias) # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰)
var cell_rows: Array[Variant] = _biased_cell_rows(luck_bias) # cell 池正向行 weight>0(订阅件装配时稀有正向 kinds 权重 +N——池结构零触碰)
var clear_rows: Array[Variant] = _pool_rows("clear_default")
# ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布)
var draft := {}
@@ -83,7 +83,7 @@ func _normalize_eligible(eligible: Array) -> Array[Dictionary]:
return list
# 偏运硬币权重投影(底稿 §3:只动池内权重、雷安仍同池同分布;luck_bias=0 逐位返回原行集——off 态零扰动)
# 订阅加权权重投影(底稿 §3:只动池内权重、雷安仍同池同分布;luck_bias=0 逐位返回原行集——off 态零扰动)
# 合成序:先经图级偏斜投影再叠件级加法——合成序锚=图级先、件级后
#(环境事实先定型、玩家构筑叠其上;两投影均为池内权重面,雷安同池与取号次数零变化)
func _biased_cell_rows(bias: int) -> Array:
+11 -11
View File
@@ -124,8 +124,8 @@ func _reveal_safe(evt: Dictionary, snap: Dictionary, ri: int) -> void:
# dig_reveal 施加面(序=计数器〔连击计数——乘区收集后执行=本格兑付用旧值〕→ 揭示入队〔地质学家/
# 跳格采掘 reveal_tiles 订阅面,授予段末统一掷格兑付〕→ 追加产出〔深挖钻头 v2 双份制:grant_material
# 命中=配额内追加第二份〕→ 奥术矢〔魔能共鸣——每战首次开安全格追加魔法矢;任意真族单次生效〕)
# 跳格采掘 reveal_tiles 订阅面,授予段末统一掷格兑付〕→ 追加产出〔双份制:grant_material
# 命中=配额内追加第二份〕→ 奥术矢(每战首次开安全格追加魔法矢;任意真族单次生效))
func _reveal_safe_apply(dr_hits: Array, evt: Dictionary, ri: int) -> void:
for h in dr_hits:
if String(h.get("kind", "")) == "counter_add":
@@ -163,7 +163,7 @@ func _reveal_mine(evt: Dictionary, snap: Dictionary, ri: int) -> void:
reward_grant.grant_routed(reward_grant.feat_yield_adjust(snap, false, 0, 0), "negative", {"source_class": "mine_hit", "cell": evt.cell, "ri": ri, "halve_pct": int(applied["halve_pct"])})
for h in mh_hits:
if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "reflect":
var nct := damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹",
var nct := damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_halve", "反弹伤害",
{"channel": "board", "base_value": int(h.get("value", 0)), "target_rule": DamageController.TARGET_ALL_ALIVE,
"ri": ri, "origin_cell": evt.cell})
reward_grant.submit(nct, false)
@@ -229,7 +229,7 @@ func _terrain_batch(ri: int) -> void:
# 封印判定批+战封印军规开单段(封印流:seal_judged 批后统一开单:轮末每面封印命中对最前存活敌
# 结算 board_typed 必中伤害〔复用零新配方;「封印所在巢穴敌」简化为 default_target 最前存活敌——
# 台账披露待追认〕;完美布防=本轮封印批全中(hit>0 且 miss==0)时封印伤 ×mult——全对推理奖励)。
# 台账披露待追认〕;封印全中倍率=本轮 hit>0 且 miss==0 时封印伤 ×mult——全对推理奖励)。
# 伤害单据案C 暂存战斗段末兑付。
func _seal_batch(ri: int) -> void:
for evt in facts.seals:
@@ -237,7 +237,7 @@ func _seal_batch(ri: int) -> void:
_seal_strike_batch(ri)
# seal_settle 发射+施加(战封印 open_ticket + 完美布防 modify_ticket 改写窗口:封印伤基值×完美倍率后
# seal_settle 发射+施加(战封印 open_ticket + 封印全中倍率 modify_ticket 改写窗口:封印伤基值×完美倍率后
# 逐封印开单)
func _seal_strike_batch(ri: int) -> void:
var fs_hits := 0
@@ -262,7 +262,7 @@ func _seal_strike_batch(ri: int) -> void:
# 战封印军规基值决议(attack 通道 open_ticket 首命中取基值;seal_mult modify_ticket 连乘;
# 完美布防=miss==0 且 hit>0 时基值×倍率。空旗集=空字典〔不开单合法态〕)
# 封印全中倍率=miss==0 且 hit>0 时基值×倍率。空旗集=空字典〔不开单合法态〕)
func _seal_strike_value(fs_fx: Array, fs_hits: int, fs_misses: int) -> Dictionary:
var banner_hits: Array[Variant] = []
for h in fs_fx:
@@ -276,11 +276,11 @@ func _seal_strike_value(fs_fx: Array, fs_hits: int, fs_misses: int) -> Dictionar
if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "seal_mult":
seal_mult *= maxi(1, int(h.get("value", 1)))
if fs_misses == 0 and fs_hits > 0 and seal_mult > 1:
strike_value *= seal_mult # 完美布防:同轮全中封印伤倍增(全对推理奖励)
strike_value *= seal_mult # 封印全中:同轮全中封印伤倍增(全对推理奖励)
return {"value": strike_value}
# round_end 发射+施加(每轮结算收口:熔火之躯反打单开立〔open_ticket 即时执行〕+积层清零〔counter_add
# round_end 发射+施加(每轮结算收口:轮末反打单开立〔open_ticket 即时执行〕+积层清零〔counter_add
# set〕;停手侧 AP 已在 request_stop 记账,finish_round_stop 按转存率转存 RAP)+施加面:grant_gp〔爆破会计/收盘——escrow 战斗域
# 缓冲,随 gp_dropped 上抛〕/grant_ap mode:queue〔封印回响——返 1 AP 下轮,queue_ap_delta 通道与轮始
# 加成同面〕+段界收口(转存率入参、清图奖励、揭示兑付、段界结账)
@@ -348,7 +348,7 @@ func _judge_seal(evt: Dictionary, ri: int) -> void:
# 封印命中(资格以危险格实例为键去重——排雷玩法 诚实重复上报的对侧;seal_hit 发射序:
# 殉爆票据 → 施加面 → 雷区图谱额外兑付 → 基线正向兑付)
# 标中雷反打票据 → 施加面 → 标中雷额外兑付 → 基线正向兑付)
func _judge_seal_hit(evt: Dictionary, ri: int) -> void:
var cell: Vector2i = evt.cell
if facts.seal_consumed.has(cell):
@@ -359,7 +359,7 @@ func _judge_seal_hit(evt: Dictionary, ri: int) -> void:
"terrain_kind": minesweeper.harmful_adjacent(cell)})
_seal_burst_tickets(fh_hits, cell, ri)
_seal_hit_effects(fh_hits, cell, ri)
# 雷区图谱(封印流——标中雷额外获得一份格面资源:封印开始产出对标开格;
# 标中雷追加(封印流——标中雷额外获得一份格面资源:封印开始产出对标开格;
# 复用 grant 单点,额外兑付走同一 grant_routed 正向路由)
for h in fh_hits:
if String(h.get("kind", "")) == "grant_material":
@@ -370,7 +370,7 @@ func _judge_seal_hit(evt: Dictionary, ri: int) -> void:
reward_grant.grant_routed(snap, "positive", {"source_class": "seal_hit", "cell": cell, "ri": ri, "highlight": "seal_hit"})
# 标中殉爆票据(行 triggers open_ticket channel blast value_from magnitude——每标中一面封印全体存活敌
# 标中雷反打票据(行 triggers open_ticket channel blast value_from magnitude——每标中一面封印全体存活敌
# 吃 N 群伤,无类型走 blast 通道;封印爆裂〔专长〕同面消费)
func _seal_burst_tickets(fh_hits: Array, cell: Vector2i, ri: int) -> void:
for h in DamageController.hits_of(fh_hits, "open_ticket"):
+3 -2
View File
@@ -1,6 +1,7 @@
## 关卡节点图参数档 schema(节点图生成器核心批 卡① T1):五旋钮+战斗保底+岔口去路数的行式数据表。
## 键 = level_index(关卡行集单点),消费面经 ConfigDB.run.level_gen_params.row_by_level_index() 读取;生成器只吃参数档、不认关卡号(契约 6)。
## 数值全 provisional(定值批落终值);域校验沿关卡节点图生成.md v1.1 调参表已锁条款——层深 L1≥6 递增增幅变大、
## 数值全 provisional(定值批落终值);取值域与字段契约单点=本文件域校验(概念与设计意图指针=
## 关卡节点图生成.md v1.5 参数契约节)——层深 L1≥6 递增增幅变大、
## 配比四类合计 1、出口 L1 恒 1/L2-L3 1-3、岔口去路数恒 2/3、战斗保底≥0。
class_name LevelGenParams
extends Resource
@@ -66,7 +67,7 @@ func _validate_row_shape(row: Dictionary, L: int, key: String, layers_by_level:
var bd: float = float(row.get("branch_density", -1.0))
if bd < 0.0 or bd > 1.0:
errs.append({"category": "domain", "message": "%s 分支密度 %f 越域 [0,1]" % [key, bd]})
# 域并单点(关卡节点图生成.md v1.4 调参表——生成器侧域校验退役,唯此一处)
# 取值域单点=本文件域校验(概念与设计意图指针=关卡节点图生成.md v1.5 参数契约节)
var ftw: float = float(row.get("fork_third_way_pct", -1.0))
if ftw < 0.0 or ftw > 100.0:
errs.append({"category": "domain", "message": "%s fork_third_way_pct %f 越域 [0,100]" % [key, ftw]})
+2 -2
View File
@@ -17,7 +17,7 @@ const UNLOCK_COUNTER_KEYS := ["adventure_cleared", "boss_kill_count", "vision_us
const DIMENSIONS := ["info", "risk", "action", "plan", "counter"]
# enum 九 kind 全部条件触发/机制修正类;mine_reveal/ap_grant 语义不变。
# cell_removal = 爆破工兵包中立移除格;mine_hit_halve 系固定
# cell_removal = 中立移除危险格通道;mine_hit_halve 系固定
# 「减半」语义不消费 magnitude(反弹 N 由 reflect_damage 行 magnitude 承载)。
# 主动件 kind 词族(reveal_tiles/preview/grant_ap/remove_mines/
# refresh_pick——效果引擎 ACTIVE_KINDS 同名闭集);被动件不走 effect_kinds 词族
@@ -119,7 +119,7 @@ func _validate_row_head(row: Dictionary, iid: String) -> Array[Variant]:
## 订阅形态域:triggers 事件订阅行判据与行级触发词表校验(被动件形态;与 effect_kinds 互斥校验
## 面=passive 行(单形态——效果行为只走订阅);active_charged 行两形合法并存
## (effect_kinds=主动分发通道 + triggers=受理闸订阅,掘进蓄能先例))
## (effect_kinds=主动分发通道 + triggers=受理闸订阅,积点解锁先例))
func _validate_row_triggers(row: Dictionary, iid: String) -> Array[Variant]:
var errs: Array[Variant] = []
if row.has("triggers"):
+122 -20
View File
@@ -31,6 +31,8 @@ const SLOT_STEP_Y := 58 # 同层槽位纵向步距
const HIT_PAD := 6.0 # 图上命中外扩(指宽容差)
const CONFIRM_WINDOW_S := 3.0 # 放弃确认态窗口(秒——超时回落)
const COL_HP_BAR_BACK := UiKit.HP_BAR_BACK # 迷你血条深底(对暗面板可辨;值单点收 UiKit)
const DRAG_THRESHOLD := 8.0 # 拖动距离阈值(布局决策 5 provisional——超阈值进拖拽态,拖拽不触发选路)
const WHEEL_STEP := 60.0 # 滚轮横滚单档步进(provisional 实现期定值;方向映射=代拍板:下/右=向右前进向)
## 内部状态件(u2 子域拆件——契约 9 职责轴):_MapState=选路屏运行态与图绘制态(单一变化原因=
## 选路呈现)。layer_decor 域字典承载换层装饰三件(图标贴图/石纹面/tint 层号——装饰域归并形态)。
@@ -39,6 +41,7 @@ class _MapState extends RefCounted:
var runner = null
var title: Label
var canvas: Control
var viewport: LevelMapViewport # 卷轴视口承载件(布局决策 5——画布宿主/裁切/clamp/跟随/指引)
var level_label: Label
var gp_value: Label
var hp_value: Label
@@ -54,6 +57,8 @@ class _MapState extends RefCounted:
var _cand_ids: Array[int] = [] # 本停点候选 id 集(图上命中合法集;探针直读冻结——住宿主)
var _content_size := Vector2(620, 240) # 图内容包络(open_choices 按图拟合——画布 min 尺寸随它;探针直读冻结)
var _hover_id := -1 # 悬停预承诺候选(-1 = 无;探针直读冻结)
var _drag_active := false # 拖拽态(超阈值至抬杆——拖拽不触发选路,布局决策 5)
var _drag_press_pos := Vector2(-1, -1) # 拖距判定基准(按下点画布局部坐标;-1 = 无按下跟踪)
var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开;探针直读冻结)
var status_badge: Button # 状态面板入口角标(顶栏 GP 徽右侧;连接归组合根;探针直读冻结)
var _layout_cache := {"nodes": [], "canvas_size": Vector2.ZERO, "pos": {}} # 布局缓存(_layout 纯函数于 (nodes, canvas)——键=nodes 内容等价+画布尺寸,图态换代或画布尺寸变即自然失效;探针直读冻结)
@@ -74,15 +79,15 @@ func _init() -> void:
base.add_child(tex)
_map.layer_decor.noise_rect = tex # 世界侧 tint 承载面(换层色板渐变挂点——§2.7 切片档)
base.add_child(UiKit.vignette_rect())
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
base.add_child(center)
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 10)
center.add_child(box)
_build_top_bar(box)
_build_canvas(box)
_build_status_bar(box)
# 三段纵排页(布局决策 5 卷轴视口:顶栏/标题在上、图区视口扩张充盈、底栏在下——
# 视口为窗口屏为框,画布随图生长可超屏)
var page := VBoxContainer.new()
page.set_anchors_preset(Control.PRESET_FULL_RECT)
page.add_theme_constant_override("separation", 10)
base.add_child(page)
_build_top_bar(page)
_build_canvas(page)
_build_status_bar(page)
visible = false
@@ -122,14 +127,21 @@ func _build_top_bar(box: VBoxContainer) -> void:
box.add_child(_map.title)
## 节点图画布域(图上点击 = 唯一提交通道——布局决策 2;mouse_filter STOP 接悬停/点击)
## 节点图区(卷轴视口承载——布局决策 5):视口件为窗口(裁切/offset/clamp/跟随/指引),
## 画布为其子件;图上点击 = 唯一提交通道(布局决策 2),拖拽/滚轮判定留在画布 _gui_input
## (事件流同源——悬停/选路同源,视口件不做父子事件路由)
func _build_canvas(box: VBoxContainer) -> void:
_map.canvas = Control.new()
_map.canvas.custom_minimum_size = Vector2(620, 240)
_map.canvas.mouse_filter = Control.MOUSE_FILTER_STOP
_map.canvas.draw.connect(_draw_graph.bind(_map.canvas))
_map.canvas.gui_input.connect(_on_canvas_input)
box.add_child(_map.canvas)
_map.canvas.mouse_exited.connect(_on_canvas_mouse_exited)
_map.viewport = LevelMapViewport.new()
_map.viewport.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_map.viewport.size_flags_vertical = Control.SIZE_EXPAND_FILL
box.add_child(_map.viewport)
_map.viewport.setup(_map.canvas, _viewport_anchor, Vector2(1280, 800)) # 兜底尺寸=设计基座(实况窗口以布局为准)
## 底部状态栏域(合并单条:亮米面板一站读齐=头像+生命+属性+本关加成/流派件——demo ④;
@@ -297,6 +309,9 @@ func open_choices(cands: Array, runner: LevelRunner) -> void:
_fit_canvas(_map.runner.nodes())
_apply_level_tint(int(_map.runner.level_row.get("level_index", 0)))
visible = true
# 视口锚定(布局决策 5):入口停点(尚无当前节点)=首锚即达;其余停点/兑付回图=平滑跟随
# (停点切换与兑付确认回图——减动效开由视口件跳终态)
_map.viewport.reanchor(_map.runner.current_node_id() >= 0)
if not was_visible:
# 独立屏淡入(spec Transitions:约 200ms;alpha 淡入非明度变化——art-bible §2 明度恒定)
modulate.a = 0.0
@@ -323,6 +338,7 @@ func _fit_canvas(nodes: Array) -> void:
SLOT_BASE_Y + maxslot * SLOT_STEP_Y + NODE_HALF.y + 24.0) # +24 = 类型文字标签行
_content_size = _content_size.max(Vector2(620, 256)) # 下限(抬高容纳标签行)
_map.canvas.custom_minimum_size = _content_size
_map.canvas.size = _content_size # 同帧收敛(视口锚点/包络数学与 _layout 同源读画布尺寸——容器延迟收敛不等锚点)
# 换层色板渐变挂点(art-bible §2.7 下探过渡切片档——关卡结算确认→下一关→独立屏淡入链路上的
@@ -356,20 +372,70 @@ func apply_status(payload: Dictionary) -> void:
# ---- 图上命中(布局决策 2——唯一提交通道;探针 press_candidate 同路径) ----
# 拖拽与点击以拖动距离阈值区分(布局决策 5):按下起跟踪拖距,超阈值进拖拽态逐帧平移视口,
# 抬杆不提交;未超阈值的完整点击(按下→抬杆)=提交沿——命中候选即提交,语义零改动。
func _on_canvas_input(ev: InputEvent) -> void:
if _map.runner == null or not visible:
return
if ev is InputEventMouseMotion:
var nid := _candidate_at(ev.position) # gui_input 事件坐标 = 接收控件局部坐标(push_input 同成立)
if nid != _hover_id:
_hover_id = nid
_map.canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW
if ev is InputEventMouseButton and ev.pressed and _wheel_pan_step(ev.button_index) != 0.0:
_map.viewport.pan_by(Vector2(_wheel_pan_step(ev.button_index), 0))
elif ev is InputEventMouseMotion:
_on_canvas_motion(ev)
elif ev is InputEventMouseButton and ev.button_index == MOUSE_BUTTON_LEFT:
_on_canvas_click(ev)
## 悬停预承诺与拖拽态推进沿(画布局部坐标——gui_input 事件流同源)
func _on_canvas_motion(ev: InputEventMouseMotion) -> void:
if _drag_active:
_map.viewport.pan_by(ev.relative) # 拖拽态逐帧平移(视口件执行 clamp——本屏只判定)
return
if _drag_press_pos.x >= 0.0 and ev.position.distance_to(_drag_press_pos) >= DRAG_THRESHOLD:
_drag_active = true
_drag_press_pos = Vector2(-1, -1)
if _hover_id != -1: # 进拖拽态清悬停预承诺(拖拽非选路意图)
_hover_id = -1
_map.canvas.mouse_default_cursor_shape = Control.CURSOR_ARROW
_map.canvas.queue_redraw()
elif ev is InputEventMouseButton and ev.button_index == MOUSE_BUTTON_LEFT and ev.pressed:
var nid2 := _candidate_at(ev.position)
if nid2 >= 0:
_submit_choice(nid2)
_map.viewport.pan_by(ev.relative) # 跨阈值帧位移计入平移(拖距全长=平移全长)
return
var nid := _candidate_at(ev.position) # gui_input 事件坐标 = 接收控件局部坐标(push_input 同成立)
if nid != _hover_id:
_hover_id = nid
_map.canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW
_map.canvas.queue_redraw()
## 点击沿与拖拽抬杆(未超阈值的完整点击=提交沿——布局决策 2 语义零改动)
func _on_canvas_click(ev: InputEventMouseButton) -> void:
if ev.pressed:
_drag_press_pos = ev.position
_drag_active = false
return
_drag_press_pos = Vector2(-1, -1)
if _drag_active:
_drag_active = false # 拖拽抬杆=平移结束,不触发选路提交(布局决策 5)
return
var nid2 := _candidate_at(ev.position)
if nid2 >= 0:
_submit_choice(nid2)
## 滚轮横滚步进(代拍板:下/右=向右前进向——视线移向深层,画布左移;上/左反向;非滚轮=0)
func _wheel_pan_step(button_index: int) -> float:
if button_index == MOUSE_BUTTON_WHEEL_DOWN or button_index == MOUSE_BUTTON_WHEEL_RIGHT:
return -WHEEL_STEP
if button_index == MOUSE_BUTTON_WHEEL_UP or button_index == MOUSE_BUTTON_WHEEL_LEFT:
return WHEEL_STEP
return 0.0
## 拖距跟踪器生命周期清理(按下即武装——鼠标离画布且未入拖拽态时解除,
## 防丢失 release 沿后的残留 armed 态把后续 motion 误判成拖拽激活)
func _on_canvas_mouse_exited() -> void:
if not _drag_active:
_drag_press_pos = Vector2(-1, -1)
# 候选命中:位置 = 布局公式(与 _draw_graph 同源),矩形 = 节点方块 + 外扩容差;非候选格不响应
@@ -413,6 +479,42 @@ func press_candidate(index: int) -> void:
_submit_choice(nid)
# ---- 视口锚点(布局决策 5:当前节点居中;入口停点尚无当前节点=锚入口组包围盒居中) ----
## 锚点画布坐标提供方(构造注入视口件——本屏供数,视口件只消费)
func _viewport_anchor() -> Vector2:
if _map.runner == null:
return _content_size / 2.0
var nodes: Array[Variant] = _map.runner.nodes()
var pos := _layout(nodes, _map.canvas)
var cur_id: int = _map.runner.current_node_id()
if cur_id < 0:
return _entry_group_center(nodes, pos) # 入口停点(尚未走出第一步)
var cur := _node_by_id(cur_id, nodes)
if cur.is_empty():
return _content_size / 2.0
return pos[cur]
func _entry_group_center(nodes: Array, pos: Dictionary) -> Vector2:
# 入口组 = 全部无入边节点(与 _assign_layers 入口判据同源);多入口取包围盒居中
var inbound := {}
for node in nodes:
for tid in _out_ids(node, nodes):
inbound[tid] = true
var lo := Vector2(INF, INF)
var hi := Vector2(-INF, -INF)
for node in nodes:
if inbound.has(_id_of(node)):
continue
var p: Vector2 = pos[node]
lo = lo.min(p)
hi = hi.max(p)
if lo.x > hi.x:
return _content_size / 2.0 # 空图兜底(无入口——不达路径,防御位)
return (lo + hi) / 2.0
func _id_of(node: Dictionary) -> int:
# 编号显示经行进器读面(编号空间解析单一出口——单链 = 下标 / 图 = node_depth)
return _map.runner.node_id_of(node)
@@ -0,0 +1,190 @@
## 卷轴视口承载件(level-map.md 布局决策 5——关卡画面选路屏):画布宿主与裁切、平移状态
## 与 clamp、锚点跟随与平滑滚动、指引件(屏缘渐隐带+平移提示 chip)。
## 纯呈现态:平移位置零核心写、不进存档(重进本屏按锚点规则重新定位);跟随只随停点切换/
## 兑付回图发生(零自动推进)。拖拽/滚轮判定留画布 _gui_input(事件流同源——悬停/选路同源),
## 本件只管裁切/offset/clamp/跟随/指引;画布包络=画布实际尺寸(open_choices 拟合后同帧收敛)。
class_name LevelMapViewport
extends Control
const SCROLL_DUR := 0.3 # 平滑滚动时长(布局决策 5 provisional 0.3s 单段缓动)
const FADE_BAND_W := 70.0 # 屏缘渐隐带宽(布局决策 5/像素级判据②)
const CHIP_SIZE := Vector2(240, 30) # 平移提示 chip(像素级判据③——图区底部居中)
const CHIP_CORNER := 6 # chip 圆角(布局决策 5)
const CHIP_BOTTOM_GAP := 24.0 # chip 距视口底缘(效果图档同族取值)
const CHIP_TEXT := "◄ 拖拽画布 / 滚轮平移 ►" # 14px(FS_HINT 档——布局决策 5)
const EDGE_EPS := 0.5 # 包络边界判定容差(浮点平移累积)
var _canvas: Control
var _anchor_provider: Callable # () -> Vector2 锚点画布坐标(screen 提供——当前节点/入口组)
var _viewport_size := Vector2.ZERO # 窗口尺寸兜底(setup 注入设计基座——实况窗口以控件 size 为准,布局晚于构造)
var _offset := Vector2.ZERO: # 画布左上角在视口件坐标系的位置(纯呈现态——直驱画布 position)
set(value):
_offset = value
if _canvas != null:
_canvas.position = _offset
var _view_tween: Tween
var _guided_visible := false # 指引件可见态(画布超窗才暗示——浅图无平移余量即隐藏)
## 一次交齐注入(code-conventions §1.6 拆分交接:画布宿主、锚点来源、窗口兜底尺寸)
func setup(canvas: Control, anchor_provider: Callable, viewport_size: Vector2) -> void:
_canvas = canvas
_anchor_provider = anchor_provider
_viewport_size = viewport_size
clip_contents = true
add_child(canvas)
_build_guidance()
func _notification(what: int) -> void:
# 窗口实况尺寸落定/变化即重锚(首开布局晚于 open_choices 的锚定——隐藏子树容器排序
# 首个可见帧才传播,4.7 行为;实况窗口以本件 size 为准,落定即收敛锚点)
if what == NOTIFICATION_RESIZED and _canvas != null and _anchor_provider.is_valid():
reanchor(false)
## 锚点跟随(布局决策 5:锚点居中;smooth=停点切换/兑付回图平滑滚动,减动效开=跳终态;
## 重入先杀在飞 tween 再建新——快速连续停点切换防叠加)
func reanchor(smooth: bool) -> void:
if _canvas == null or not _anchor_provider.is_valid():
return
var target := _clamped_center_offset(_anchor_provider.call())
_kill_scroll_tween()
if not smooth or ArmedTimer.motion_still():
_offset = target
_sync_guidance()
return
_view_tween = create_tween()
_view_tween.tween_property(self, "_offset", target, UiKit.motion(SCROLL_DUR)) \
.set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
_sync_guidance()
## 手动平移(画布 _gui_input 判定后调用——拖拽沿逐帧/滚轮步进;纯呈现零核心写)
func pan_by(delta: Vector2) -> void:
if _canvas == null:
return
_kill_scroll_tween()
var win := _window_size()
_offset = Vector2(
_axis_pan_offset(_offset.x, delta.x, _canvas.size.x, win.x),
_axis_pan_offset(_offset.y, delta.y, _canvas.size.y, win.y))
_sync_guidance()
## 视口中线的画布坐标(锚点居中判据投影面——浅图=画布中心,深图=夹紧域内当前注视点)
func viewport_center_canvas_pos() -> Vector2:
return _window_size() / 2.0 - _offset
## 视口是否钉在画布包络边界(任一超窗轴 offset 顶边——clamp 判据面)
func is_clamped_at_edge() -> bool:
var win := _window_size()
if _canvas == null:
return false
return _axis_at_edge(_offset.x, _canvas.size.x, win.x) \
or _axis_at_edge(_offset.y, _canvas.size.y, win.y)
func _kill_scroll_tween() -> void:
if _view_tween != null and _view_tween.is_valid():
_view_tween.kill()
_view_tween = null
## 单轴锚定 offset:画布不超窗=居中分支(夹紧域退化——布局决策 5);
## 超窗=锚点夹紧在画布包络内,锚点落视口中线(offset = 半窗 − 锚点)
func _axis_center_offset(anchor: float, canvas_len: float, half_win: float) -> float:
if canvas_len <= half_win * 2.0:
return half_win - canvas_len / 2.0
return half_win - clampf(anchor, half_win, canvas_len - half_win)
func _clamped_center_offset(anchor: Vector2) -> Vector2:
var half := _window_size() / 2.0
return Vector2(
_axis_center_offset(anchor.x, _canvas.size.x, half.x),
_axis_center_offset(anchor.y, _canvas.size.y, half.y))
## 单轴平移收敛:居中轴零平移余量不动;超窗轴 offset 收敛在 [半窗−画布, 0](不出露屏底空档)
func _axis_pan_offset(cur: float, delta: float, canvas_len: float, win_len: float) -> float:
if canvas_len <= win_len:
return cur
return clampf(cur + delta, win_len - canvas_len, 0.0)
func _axis_at_edge(offset: float, canvas_len: float, win_len: float) -> bool:
if canvas_len <= win_len:
return false
return offset >= -EDGE_EPS or offset <= win_len - canvas_len + EDGE_EPS
## 实况窗口优先(容器布局晚于构造——注入值只兜构造帧数学)
func _window_size() -> Vector2:
if size.x > 0.0 and size.y > 0.0:
return size
return _viewport_size
## 指引件(布局决策 5/像素级判据②③):屏缘渐隐带盖在画布之上+图区底部居中提示 chip;
## 可见性=画布超窗(_sync_guidance 随锚定/平移刷新)
func _build_guidance() -> void:
var band_l := TextureRect.new()
band_l.name = "FadeL"
band_l.texture = _fade_gradient(false)
band_l.set_anchors_preset(Control.PRESET_LEFT_WIDE)
band_l.offset_right = FADE_BAND_W
band_l.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
band_l.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(band_l)
var band_r := TextureRect.new()
band_r.name = "FadeR"
band_r.texture = _fade_gradient(true)
band_r.set_anchors_preset(Control.PRESET_RIGHT_WIDE)
band_r.offset_left = -FADE_BAND_W
band_r.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
band_r.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(band_r)
var chip := PanelContainer.new()
chip.name = "PanHint"
chip.add_theme_stylebox_override("panel",
UiKit.panel_flat(UiKit.HP_BAR_BACK, UiKit.STONE, CHIP_CORNER, 1, Vector4(12, 5, 12, 5)))
chip.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
chip.offset_left = -CHIP_SIZE.x / 2.0
chip.offset_right = CHIP_SIZE.x / 2.0
chip.offset_top = -CHIP_SIZE.y - CHIP_BOTTOM_GAP
chip.offset_bottom = -CHIP_BOTTOM_GAP
chip.custom_minimum_size = CHIP_SIZE
chip.mouse_filter = Control.MOUSE_FILTER_IGNORE
var chip_label := UiKit.label(CHIP_TEXT, UiKit.FS_HINT, false, UiKit.WARM)
chip_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
chip_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
chip.add_child(chip_label)
add_child(chip)
_sync_guidance()
## 屏缘渐隐渐变(色取屏底同族 BG_ROOT——代拍板:不透明端 0.9 单段衰减)
func _fade_gradient(transparent_at_left: bool) -> GradientTexture2D:
var grad := Gradient.new()
grad.offsets = PackedFloat32Array([0.0, 1.0])
grad.colors = PackedColorArray([Color(UiKit.BG_ROOT, 0.9), Color(UiKit.BG_ROOT, 0.0)])
var tex := GradientTexture2D.new()
tex.gradient = grad
tex.fill_from = Vector2(1, 0.5) if transparent_at_left else Vector2(0, 0.5)
tex.fill_to = Vector2(0, 0.5) if transparent_at_left else Vector2(1, 0.5)
tex.width = int(FADE_BAND_W)
return tex
func _sync_guidance() -> void:
var win := _window_size()
var over := _canvas != null and (_canvas.size.x > win.x or _canvas.size.y > win.y)
if over == _guided_visible:
return
_guided_visible = over
for nm in ["FadeL", "FadeR", "PanHint"]:
var node := get_node_or_null(NodePath(nm)) as Control
if node != null:
node.visible = over
@@ -0,0 +1 @@
uid://dgin6f6214cbg
+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])
+94
View File
@@ -0,0 +1,94 @@
# level_map_shot_probe.gd — 关卡画面视口 golden 采集探针(卷轴视口批 golden 扩项)
# 两 Shot 面(2026-11-05 用户裁:只对拍关卡最开始与关卡 Boss 节点,中间过程不对拍):
# lmap_entry.png = 入口停点(关卡最开始——锚点=入口组包围盒中心夹紧)
# lmap_boss.png = 中段停点锚定末列(Boss 紫赤描边列入屏——viewport_center 夹紧右缘)
# 采集确定性 = 固定种子单关直构(LevelRunner.start_level_external defer_entry / resume_state,
# 零 RNG 消耗路径)+ 探针隔离档(无断点、出厂设置 1280×800);运行=窗口化(--headless 禁——
# 渲染闸机器面吃真实渲染帧)。截图经 user args 目录落盘,基线换代走 golden_baseline_refresh.sh --lmap。
# 运行:Godot_console.exe --path game res://probe/level_map_shot_probe.tscn -- <shots_dir>
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
var host := Control.new()
add_child(host)
host.size = get_viewport().get_visible_rect().size
var screen = load("res://presentation/views/level_map_screen.gd").new()
screen.set_anchors_preset(Control.PRESET_FULL_RECT)
host.add_child(screen)
for i in range(6):
await get_tree().process_frame # 布局收敛(视口实况尺寸落定→resize 重锚)
# Shot①:入口停点(关卡最开始)
var runner = _make_runner(true, -1)
screen.open_choices(_cand_nodes(runner), runner)
await _settle()
_shot(out_dir, "lmap_entry.png")
# Shot②:中段停点锚定末列(布局 x 最深的有出边节点——id 不按深度排,锚定后 Boss 列入屏)
var boss_stop := _deepest_with_outs(screen, runner)
runner = _make_runner(false, boss_stop)
screen.open_choices(_cand_nodes(runner), runner)
await _settle()
_shot(out_dir, "lmap_boss.png")
print("[level_map_shot_probe] PASS — 两 Shot 落盘(entry+boss → %s)" % out_dir)
get_tree().quit(0)
func _settle() -> void:
for i in range(8):
await get_tree().process_frame
await get_tree().create_timer(0.5).timeout # 平滑滚动终态余量(reanchor tween 0.3s)
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)
var ok: bool
if entry:
ok = runner.start_level_external(3, Callable(runner, "rest_heal_amount"), true)
else:
ok = runner.start_level_external(3, Callable(runner, "rest_heal_amount"),
false, {"node_id": node_id, "visited_ids": [node_id]})
if not ok:
push_error("[level_map_shot_probe] 状态注入失败(entry=%s node=%d)" % [str(entry), node_id])
get_tree().quit(1)
return runner
func _cand_nodes(runner) -> Array:
var ids: Array = runner.next_candidate_ids()
var out: Array = []
for node in runner.nodes():
if ids.has(runner.node_id_of(node)):
out.append(node)
return out
func _deepest_with_outs(screen, runner) -> int:
var pos: Dictionary = screen.layout_probe(runner.nodes())
var best := -1
var best_x := -INF
for node in runner.nodes():
if runner.node_out_ids(node).is_empty():
continue
var x: float = pos[node].x
if x > best_x:
best_x = x
best = runner.node_id_of(node)
return best
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))
if err != OK:
push_error("[level_map_shot_probe] 截图落盘失败 %s(err=%d)" % [fname, err])
get_tree().quit(1)
+1
View File
@@ -0,0 +1 @@
uid://j4ax48si4nq5
+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://probe/level_map_shot_probe.gd" id="1"]
[node name="LevelMapShotProbe" type="Node"]
script = ExtResource("1")
+400
View File
@@ -0,0 +1,400 @@
# level_map_viewport_probe.gd — 卷轴视口行为探针(卷轴视口批 R1–R8 验收载体)
# 判的是 level-map.md 布局决策 5 的视口行为四条:浅图整幅居中无裁切(R1)、停点切换/兑付回图
# 当前节点居中锚定——平滑滚动与减动效瞬切(R2/R7)、拖拽/滚轮手动平移与 clamp 包络——
# 首末列可达(R3/R4/R5)、拖拽不触发选路提交(R6)、指引件可见性与几何(R8,像素级判据②③)。
# 真实入口 = 合成拖拽沿/滚轮沿/点击沿经 push_input 进引擎输入管线打在真实画布 _gui_input 上
# (probe_ui_util.push_drag_at/push_wheel_at/push_click_at);状态注入 = 直调核心层写口构造
# 单关停点(LevelRunner.start_level_external——defer_entry=入口停点 / resume_state=断点续玩
# 重建到任意停点,零 RNG 消耗;test-conventions §5.4 自动化测试直构口径),零 main 冒险流程
# 依赖:真实屏 = LevelMapScreen 实例 + 真实单关 runner。
# 运行:Godot_console.exe --headless --path . res://probe/level_map_viewport_probe.tscn
extends Node
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
const ViewportScript := preload("res://presentation/views/level_map_viewport.gd")
const SEED_L1 := 910211 # 浅图停点种子(L1 单链——画布 846.5×256 全幅入窗)
const SEED_L3 := 910212 # 深图停点种子(L3 图——画布超 1280 窗宽,滚动视口生效面)
var _problems: Array[String] = []
var _checks := 0
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()
add_child(host)
host.size = get_viewport().get_visible_rect().size # 显式置底座(父非 Control 锚点不解析)
var screen = load("res://presentation/views/level_map_screen.gd").new()
screen.set_anchors_preset(Control.PRESET_FULL_RECT)
host.add_child(screen)
await get_tree().process_frame
await get_tree().process_frame # 布局收敛先行(视口实况窗口尺寸就位后再开停点——锚点数学以实况窗口为准)
var vp = _find_viewport(screen)
if vp == null:
_problems.append("卷轴视口承载件未挂入关卡屏(LevelMapViewport 实例缺位)")
_finish()
return
await _probe_shallow_no_crop(screen, vp)
await _probe_deep_entry_clamp(screen, vp)
await _probe_wheel_pan(screen, vp)
await _probe_drag_pan(screen, vp)
await _probe_clamp_bounds_and_columns(screen, vp)
await _probe_drag_no_submit(screen, vp)
await _probe_follow_anchor(screen, vp)
await _probe_smooth_scroll_reduced_motion(screen, vp)
await _probe_guidance_chrome(screen, vp)
await _probe_shallow_guidance_hidden(screen, vp)
_finish()
## 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
await get_tree().process_frame
var canvas: Control = vp.get_child(0) as Control
if canvas == null:
_problems.append("R1 画布实例缺位(视口首子件非 Control)")
return
_check("R1 浅图画布全幅入窗(canvas=%s window=%s)〔卷轴批 V1〕" % [canvas.size, vp.size],
canvas.size.x <= vp.size.x + 0.5 and canvas.size.y <= vp.size.y + 0.5)
var want: Vector2 = (vp.size - canvas.size) / 2.0
_check("R1 居中分支 offset 定位(want=%s got=%s)〔卷轴批 V1〕" % [want, canvas.position],
canvas.position.distance_to(want) <= 0.5)
_check("R1 画布矩形在窗内无裁切〔卷轴批 V1〕", vp.get_global_rect().encloses(canvas.get_global_rect()))
## 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
await get_tree().process_frame
var center: Vector2 = vp.viewport_center_canvas_pos()
var half: float = vp.size.x / 2.0
var canvas_len: float = _canvas_len(screen)
_check("R5 入口锚定后视口中线在夹紧域内(center.x=%.1f ∈ [%.1f, %.1f])〔卷轴批 V5〕" %
[center.x, half, canvas_len - half],
center.x >= half - 0.5 and center.x <= canvas_len - half + 0.5)
# 入口停点候选 = 入口节点——包围盒中心夹紧即锚点(本档图入口分布跨图,中心落夹紧域)
var nodes: Array = _runner.nodes()
var pos: Dictionary = screen.layout_probe(nodes)
var ids: Array = _runner.next_candidate_ids()
var lo := INF
var hi := -INF
for node in nodes:
if ids.has(_runner.node_id_of(node)):
lo = minf(lo, pos[node].x)
hi = maxf(hi, pos[node].x)
var want: float = clampf((lo + hi) / 2.0, half, canvas_len - half)
_check("R5 入口组包围盒中心=锚点(bbox=%.1f center=%.1f want=%.1f)〔卷轴批 V5〕" %
[(lo + hi) / 2.0, center.x, want], absf(center.x - want) <= 0.5)
## R4 滚轮横滚移动视口(下=向右前进向——代拍板映射;窗内平移不钉边界,与 R5 两极 true 构成翻转)
func _probe_wheel_pan(screen, vp) -> void:
var before: float = vp.viewport_center_canvas_pos().x
var pos: Vector2 = vp.get_global_rect().get_center()
ProbeUiUtil.push_wheel_at(self, pos, true)
await get_tree().process_frame
await get_tree().process_frame
var after: float = vp.viewport_center_canvas_pos().x
_check("R4 滚轮下单档平移 +60(before=%.1f after=%.1f)〔卷轴批 V4〕" % [before, after],
absf(after - before - 60.0) <= 0.5)
_check("R4 窗内平移不钉边界(is_clamped_at_edge=false)〔卷轴批 V4〕", not vp.is_clamped_at_edge())
ProbeUiUtil.push_wheel_at(self, pos, false)
await get_tree().process_frame
await get_tree().process_frame
_check("R4 滚轮上回移 −60〔卷轴批 V4〕",
absf(vp.viewport_center_canvas_pos().x - before) <= 0.5)
## R3 拖拽平移移动视口(窗内位移量=拖拽位移,无 clamp 干扰)
func _probe_drag_pan(screen, vp) -> void:
# 先滚入窗内区(两档 +120——避开左包络边界的 clamp 吸收),再拖拽核位移
var pos: Vector2 = vp.get_global_rect().get_center()
ProbeUiUtil.push_wheel_at(self, pos, true)
ProbeUiUtil.push_wheel_at(self, pos, true)
await get_tree().process_frame
await get_tree().process_frame
var before: float = vp.viewport_center_canvas_pos().x
ProbeUiUtil.push_drag_at(self, pos, Vector2(-40, 0))
await get_tree().process_frame
await get_tree().process_frame
var after: float = vp.viewport_center_canvas_pos().x
_check("R3 拖拽左移 −40 平移量逐位一致(画布随手——视线 +40,before=%.1f after=%.1f)〔卷轴批 V3〕" % [before, after],
absf(after - before - 40.0) <= 0.5)
# 复位到左包络边界,给后续探针稳定起点
ProbeUiUtil.push_drag_at(self, pos, Vector2(2000, 0))
await get_tree().process_frame
await get_tree().process_frame
## R5 clamp 边界与首末列可达(拖拽两极)
func _probe_clamp_bounds_and_columns(screen, vp) -> void:
var nodes: Array = _runner.nodes()
var pos: Dictionary = screen.layout_probe(nodes)
var first_node := _extreme_node(pos, nodes, true)
var last_node := _extreme_node(pos, nodes, false)
var vp_rect: Rect2 = vp.get_global_rect()
# 右极:拖拽超包络 → 视口钉左缘,首列(深度 0)入屏
ProbeUiUtil.push_drag_at(self, vp_rect.get_center(), Vector2(2000, 0))
await get_tree().process_frame
await get_tree().process_frame
var center: float = vp.viewport_center_canvas_pos().x
var half: float = vp.size.x / 2.0
_check("R5 拖拽超包络收敛夹紧域(center.x=%.1f)〔卷轴批 V5〕" % center,
center >= half - 0.5 and center <= _canvas_len(screen) - half + 0.5)
_check("R5 右极视口钉边界(is_clamped_at_edge=true)〔卷轴批 V5〕", vp.is_clamped_at_edge())
_check("R5 首列(深度 0)中心入屏〔卷轴批 V5〕",
vp_rect.has_point(_node_screen_pos(screen, pos, first_node)))
# 左极:反向超包络 → 视口钉右缘,末列(含 Boss 紫赤描边列)入屏
ProbeUiUtil.push_drag_at(self, vp_rect.get_center(), Vector2(-2000, 0))
await get_tree().process_frame
await get_tree().process_frame
_check("R5 左极视口钉边界(is_clamped_at_edge=true)〔卷轴批 V5〕", vp.is_clamped_at_edge())
_check("R5 末列中心入屏(拖拽两极可达——布局决策 5)〔卷轴批 V5〕",
vp_rect.has_point(_node_screen_pos(screen, pos, last_node)))
## 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:
_problems.append("R6 可居中停点不足(=%d)" % stops.size())
return
_runner = _make_runner_at_stop(3, SEED_L3, _runner.node_id_of(stops[0]))
screen.open_choices(_cand_nodes(_runner), _runner)
await get_tree().process_frame
await get_tree().process_frame
var nodes: Array = _runner.nodes()
var cands: Array = _runner.next_candidate_ids()
var target := {}
for node in nodes:
if cands.has(_runner.node_id_of(node)):
target = node
break
if target.is_empty():
_problems.append("R6 候选节点缺位(候选=%s)" % str(cands))
return
var gpos: Vector2 = _node_screen_pos(screen, screen.layout_probe(nodes), target)
var submitted: Array = []
var on_route := func(nid: int) -> void: submitted.append(nid)
screen.route_chosen.connect(on_route)
ProbeUiUtil.push_drag_at(self, gpos, Vector2(120, 24))
await get_tree().process_frame
await get_tree().process_frame
_check("R6 拖拽沿起止候选上方零提交(submitted=%s)〔卷轴批 V6〕" % str(submitted), submitted.is_empty())
_check("R6 拖拽后本屏未关〔卷轴批 V6〕", screen.visible)
var gpos2: Vector2 = _node_screen_pos(screen, screen.layout_probe(nodes), target) # 拖拽平移后重算投影(画布随视口移动)
ProbeUiUtil.push_click_at(self, gpos2)
await get_tree().process_frame
await get_tree().process_frame
_check("R6 完整点击(未超阈值)仍提交选路——阈值区分另一侧〔卷轴批 V6〕", submitted.size() == 1)
screen.route_chosen.disconnect(on_route)
## 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())
return
_runner = _make_runner_at_stop(3, SEED_L3, _runner.node_id_of(stops[1]))
screen.open_choices(_cand_nodes(_runner), _runner)
await get_tree().create_timer(0.6).timeout # 平滑滚动终态(0.3s 单段缓动+余量)
var sx: float = _node_screen_pos(screen, screen.layout_probe(_runner.nodes()), stops[1]).x
var want: float = vp.get_global_rect().get_center().x
_check("R2 停点切换后当前节点投影=视口中线(got=%.1f want=%.1f±2)〔卷轴批 V2〕" % [sx, want],
absf(sx - want) <= 2.0)
## 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)
if stops.size() < 3:
_problems.append("R7 可居中停点不足(=%d)" % stops.size())
return
# 起点:锚在停点 A(画面已停稳)
_runner = _make_runner_at_stop(3, SEED_L3, _runner.node_id_of(stops[0]))
screen.open_choices(_cand_nodes(_runner), _runner)
await get_tree().create_timer(0.6).timeout
var start: float = vp.viewport_center_canvas_pos().x
# 停点切换 A→B(屏幕保持可见=兑付回图同路径)——平滑滚动
var target_b: float = screen.layout_probe(_runner.nodes())[stops[1]].x
_runner = _make_runner_at_stop(3, SEED_L3, _runner.node_id_of(stops[1]))
screen.open_choices(_cand_nodes(_runner), _runner)
await get_tree().create_timer(0.05).timeout # 中间帧采样(300ms 单段缓动早段)
var mid: float = vp.viewport_center_canvas_pos().x
var total := absf(target_b - start)
_check("R7 平滑滚动中间帧在途中(start=%.1f mid=%.1f target=%.1f)〔卷轴批 V7〕" % [start, mid, target_b],
total > 4.0 and absf(mid - start) > 0.5 and absf(mid - target_b) > 0.5
and (mid - start) * (target_b - start) > 0.0) # 同向在途
await get_tree().create_timer(0.5).timeout
_check("R7 平滑滚动终态到位〔卷轴批 V7〕",
absf(vp.viewport_center_canvas_pos().x - target_b) <= 2.0)
# 减动效置位(UiKit.motion 通道单点——设置面开关写同一值):停点切换 B→C 位置即达
UiKit.motion_scale = 0.5
var target_c: float = screen.layout_probe(_runner.nodes())[stops[2]].x
_runner = _make_runner_at_stop(3, SEED_L3, _runner.node_id_of(stops[2]))
screen.open_choices(_cand_nodes(_runner), _runner)
await get_tree().process_frame
await get_tree().process_frame
_check("R7 减动效开=位置即达(got=%.1f target=%.1f)〔卷轴批 V7〕" %
[vp.viewport_center_canvas_pos().x, target_c],
absf(vp.viewport_center_canvas_pos().x - target_c) <= 2.0)
UiKit.motion_scale = saved_scale
## 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"))
if band_l == null or band_r == null or chip == null:
_problems.append("R8 指引件缺位(FadeL/FadeR/PanHint)〔卷轴批 V8〕")
return
_check("R8 深图渐隐带可见〔卷轴批 V8〕", band_l.visible and band_r.visible)
_check("R8 渐隐带宽 70px(L=%.1f R=%.1f)〔卷轴批 V8〕" % [band_l.size.x, band_r.size.x],
absf(band_l.size.x - 70.0) <= 0.5 and absf(band_r.size.x - 70.0) <= 0.5)
_check("R8 平移提示 chip 可见且 240×30(size=%s)〔卷轴批 V8〕" % chip.size,
chip.visible and absf(chip.size.x - 240.0) <= 0.5 and absf(chip.size.y - 30.0) <= 0.5)
_check("R8 chip 图区底部居中(水平中线对窗中线±2)〔卷轴批 V8〕",
absf(chip.get_global_rect().get_center().x - vp.get_global_rect().get_center().x) <= 2.0)
## R8 浅图指引件隐藏(无暗示必要)
func _probe_shallow_guidance_hidden(screen, vp) -> void:
_runner = _make_runner_entry(1, SEED_L1)
screen.open_choices(_cand_nodes(_runner), _runner)
await get_tree().process_frame
await get_tree().process_frame
var band_l: Control = vp.get_node_or_null(NodePath("FadeL"))
var chip: Control = vp.get_node_or_null(NodePath("PanHint"))
_check("R8 浅图画布不超窗指引件隐藏〔卷轴批 V8〕",
band_l != null and not band_l.visible and chip != null and not chip.visible)
# ---- 驱动与查证工具(状态注入面——直调核心层写口) ----
## 入口停点 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)
if not runner.start_level_external(level_index, Callable(runner, "rest_heal_amount"), true):
_problems.append("状态注入失败:start_level_external(%d, %d) 未受理" % [level_index, seed_val])
return runner
## 中段停点 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)
if not runner.start_level_external(level_index, Callable(runner, "rest_heal_amount"),
false, {"node_id": node_id, "visited_ids": [node_id]}):
_problems.append("状态注入失败:断点停点 %d 未受理" % node_id)
return runner
func _cand_nodes(runner) -> Array:
var ids: Array = runner.next_candidate_ids()
var out: Array = []
for node in runner.nodes():
if ids.has(runner.node_id_of(node)):
out.append(node)
return out
## 深图可居中停点选取(有出边且画布 x 落在夹紧域内的节点——锚定不贴边、当前节点投影可居中;
## 按 x 降序去重取第 idx 个——同层同 x 只计一个,层距 122 保证停点间滚动位移充分)
func _centerable_stops(screen, vp) -> Array:
var nodes: Array = _runner.nodes()
var pos: Dictionary = screen.layout_probe(nodes)
var half: float = vp.size.x / 2.0
var canvas_len: float = _canvas_len(screen)
var by_x_desc: Array = nodes.duplicate()
by_x_desc.sort_custom(func(a, b) -> bool: return pos[a].x > pos[b].x)
var picked: Array = []
var seen_x := {}
for node in by_x_desc:
if _runner.node_out_ids(node).is_empty():
continue
var x: float = pos[node].x
if x < half + 20.0 or x > canvas_len - half - 20.0 or seen_x.has(x):
continue
seen_x[x] = true
picked.append(node)
return picked
func _find_viewport(screen) -> Control:
return _find_by_script(screen, ViewportScript)
func _find_by_script(root: Node, script: Script) -> Control:
if root.get_script() == script:
return root as Control
for child in root.get_children():
var found := _find_by_script(child, script)
if found != null:
return found
return null
func _canvas_len(screen) -> float:
return screen.canvas_probe()["min_size"].x # 画布包络宽(open_choices 拟合值——画布尺寸即它)
func _node_screen_pos(screen, pos: Dictionary, node: Dictionary) -> Vector2:
return screen.canvas_probe()["origin"] + pos[node]
func _extreme_node(pos: Dictionary, nodes: Array, first: bool) -> Dictionary:
var best := {}
var best_x := INF if first else -INF
for node in nodes:
var x: float = pos[node].x
if (first and x < best_x) or (not first and x > best_x):
best_x = x
best = node
return best
func _finish() -> void:
# oracle: design/gdd/ux/level-map.md:3 (判定上报通道——验证函数组)
if _checks == 0:
_problems.append("零断言执行(断言数下限闸——三硬闸③)")
if _problems.is_empty():
print("[level_map_viewport_probe] PASS — 卷轴视口行为断言全绿(%d 项)" % _checks)
get_tree().quit(0)
else:
for p in _problems:
printerr("[level_map_viewport_probe] FAIL — " + p)
get_tree().quit(1)
@@ -0,0 +1 @@
uid://cprbakccv06re
+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://probe/level_map_viewport_probe.gd" id="1"]
[node name="LevelMapViewportProbe" type="Node"]
script = ExtResource("1")
+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
+115 -1
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,113 @@ 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 逐帧平移)
static func push_drag_at(main, pos: Vector2, delta: Vector2, steps: int = 4) -> void:
var down := InputEventMouseButton.new()
down.position = pos
down.global_position = pos
down.button_index = MOUSE_BUTTON_LEFT
down.pressed = true
main.get_viewport().push_input(down, true)
for i in range(1, steps + 1):
var cur := pos + delta * float(i) / float(steps)
var mv := InputEventMouseMotion.new()
mv.position = cur
mv.global_position = cur
mv.relative = delta / float(steps)
main.get_viewport().push_input(mv, true)
var up := InputEventMouseButton.new()
up.position = pos + delta
up.global_position = pos + delta
up.button_index = MOUSE_BUTTON_LEFT
up.pressed = false
main.get_viewport().push_input(up, true)
# 真输入滚轮沿(单档滚轮事件经 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
ev.global_position = pos
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 已撤;槽模板注入隔离命名空间,防碰真实槽
# user://save_slot_N.json——真实档红线)。创建=激活=暂行开营地(卡③ Q3 乙落点);
# 驱动后写盘目标=隔离槽文件(持久化断言读 user://<tag>_slot_1.json)。返回 false=新档钮缺失(驱动中断)。
+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:
+31 -1
View File
@@ -87,6 +87,14 @@ func _ready() -> void:
evf.position = fog_pos
evf.global_position = fog_pos
main.get_viewport().push_input(evf, true)
# 完整点击=press+release 成对(卷轴视口批阈值语义:拖距跟踪器由按下起武装,
# 单 press 沿会留 armed 态污染后续 motion 判定——判据语义不变:雾下点击零提交)
var evf2 := InputEventMouseButton.new()
evf2.button_index = MOUSE_BUTTON_LEFT
evf2.pressed = false
evf2.position = fog_pos
evf2.global_position = fog_pos
main.get_viewport().push_input(evf2, true)
await get_tree().process_frame
await get_tree().process_frame
ro.route_chosen.disconnect(on_route)
@@ -114,6 +122,14 @@ func _ready() -> void:
evc.position = gpos
evc.global_position = gpos
main.get_viewport().push_input(evc, true)
# 完整点击=press+release 成对(卷轴视口批阈值语义:提交沿=未超阈值的抬杆——
# 判据语义不变:图上真实点击提交选路;单 press 沿不再是提交沿)
var evr := InputEventMouseButton.new()
evr.button_index = MOUSE_BUTTON_LEFT
evr.pressed = false
evr.position = gpos
evr.global_position = gpos
main.get_viewport().push_input(evr, true)
await get_tree().process_frame
await get_tree().process_frame
if main.ui.modals.run.level_screen.visible or main.run_scope.runner.awaiting_choice():
@@ -226,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:
@@ -254,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"):
@@ -267,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"):
@@ -281,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,字形
@@ -347,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()
@@ -363,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")
@@ -376,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"]:
@@ -471,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"):
@@ -489,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 带封印+
@@ -531,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 残留)"
@@ -542,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 丙案)")
@@ -730,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]
+1 -1
View File
@@ -72,7 +72,7 @@ func _initialize() -> void:
if attune_mode == "on":
# 贪心画像:Slots=3(槽位判定源=树同调位节点点亮数,模拟画像=三节点
# 全点亮常量);装配序 =
# 表序 passive 优先至 Slots-1 + 首个 active(slots=3 → [淬毒刃纹, 标中殉爆, 掘进蓄能],
# 表序 passive 优先至 Slots-1 + 首个 active(slots=3 → 表序头三件占满示例,
# provisional 画像归调参);Y = 营地升级 Lv1 表值 1。autoplay 默认不刷——Y 仅注入容量,
# 刷新消费归玩家策略面(钓鱼归玩家)。
var slots: int = 3
+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")
+2 -4
View File
@@ -411,14 +411,12 @@ static func _on_step_row(row: Dictionary, _runner, st: Dictionary, collect_rows:
# 封印经济聚合(自结算流水聚合,零新随机源):
# ⚠ 行侧判据考证实录:结算段对存续增益实例每轮复发行 effect 行且复用实例 source_class
#(battle_orchestrator 状态列举行)——source_class=seal_hit 的行 ≠ 标中事件。事件行唯一判据
# = 授予行专带 highlight="seal_hit"(实测 87 行 == 受理封印 87 动作,1:1 精确);
# 雷区图谱追加分grant 无高光,以 display_name 后缀补捕(计入事件与合计——同封印第二份兑付)。
# = 授予行专带 highlight="seal_hit"(实测 87 行 == 受理封印 87 动作,1:1 精确)。
# 满层拒收行(reward_ineffective)零兑付不入。标空事件=归因行(type=seal_miss 每格恰一次);
# 标空惩罚合计只入一次性兑付行(flat 直伤 damage/heal + gp_loss escrow 行)——sapped 授予行
# 与其状态复发行行侧不可区分不入合计(登记注记,判读侧按事件数补算)。
if row.source_class == "seal_hit" and row.type != "reward_ineffective":
if String(row.get("highlight", "")) == "seal_hit" \
or String(row.get("display_name", "")).contains("雷区图谱"):
if String(row.get("highlight", "")) == "seal_hit":
st.seal_hit += 1
st.seal_hit_gain += int(row.value)
if row.type == "seal_miss":
+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/
+7 -5
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`;`level_map_viewport_probe.gd:_probe_drag_no_submit` |
| 关卡画面视口平移 | 关卡画面(选路枢纽) | 合成拖拽沿与滚轮沿经 `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`(窗壳形态段) |
**旅程段七:数据配置与跨系统契约**
+8 -4
View File
@@ -4,17 +4,21 @@
`windows/` 与 `mac/` 子目录各持一套,平台内部照常严格比对,跨平台不互比;
平台选择实现单点 = `tools/gate_platform.sh` 的 `gate_platform_id`,比较器 `tools/golden_compare.py`
自动拼平台子目录)。本目录现持:`windows/`(棋盘三图 `board_L1_dungeon` / `board_L2_underdark` /
`board_L3_deep_underdark`,裁剪区 900×760 + 启动态菜单 `menu_boot`,全屏)、
`level_map`(关卡画面全屏,历史基线留根——现行闸件未对拍它)、
`board_L3_deep_underdark`,裁剪区 900×760 + 启动态菜单 `menu_boot`,全屏 + 关卡视口两 Shot
`lmap_entry` / `lmap_boss`——2026-11-05 用户裁两 Shot 面:只对拍关卡最开始(入口停点)与
关卡 Boss 节点(末列锚定),中间过程不对拍)、
`level_map`(关卡画面全屏,历史基线留根——现行闸件未对拍它,由两 Shot 面接替)、
`palette_baseline/`(色板基线,纯文件内容比对与平台无关)。
比对判据与工具见 `tools/golden_compare.py`
(逐像素灰度差 >16 的像素占比 ≤2%)。
(逐像素灰度差 >16 的像素占比 ≤2%)。两 Shot 基线采集=`res://probe/level_map_shot_probe.tscn`
(固定种子单关直构——确定性采集),换代入口=`tools/golden_baseline_refresh.sh --lmap`;
他平台(mac)的关卡视口基线尚未冻结——渲染闸对本平台缺基线记 SKIP 不判红,冻结义务归批末呈报。
**未建锚面备案**:营地/设置/战斗三屏与战末结算/三选一/图例/调试浮窗四浮层无改前 golden 锚;这些呈现面的等价证据承载=现役探针/单元测试断言面+实机目检(体验验证清单 ㊵ 节)。
## 时效判定纪律
基线冻结于 2026-09-30。**判定先于动作**:每代换代面(视觉/素材/布局改动)
基线冻结于 2026-09-30;关卡视口两 Shot(lmap_entry/lmap_boss)冻结于 2026-11-05——两 Shot 面口径(只对拍入口停点与 Boss 节点)见上文现持清单行。**判定先于动作**:每代换代面(视觉/素材/布局改动)
跑批时先跑一次渲染闸看差异落不落在裁剪区与容差内——
- **判有效**(changed 远低于 2% 阈值):基线**留在位**,不得顺手重制;
Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

@@ -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()
@@ -236,7 +239,7 @@ func _smoke_global_variant() -> void:
Kit.check("生存族下调:heal 4→3 / defense 11→7(v=30 构造例)", wmap.get("heal_minor", -1) == 3 and wmap.get("ac_up", -1) == 7)
Kit.check("进攻族上浮:attack 14→15 / blast 16→18 / pierce 8→9 / magic 8→9(v=30 构造例,等比上浮守恒)", wmap.get("attack_up", -1) == 15 and wmap.get("spark_aoe", -1) == 18 and wmap.get("pierce_strike", -1) == 9 and wmap.get("magic_bolt", -1) == 9)
eco.roller.board_skew = 30
var combo_rows: Array = eco.roller._biased_cell_rows(2) # 件级加算 2=偏运硬币行 value(reward_roll bias_rewards 命中面同值)
var combo_rows: Array = eco.roller._biased_cell_rows(2) # 件级加算 2=订阅行 value(reward_roll bias_rewards 命中面同值)
var cw := _rows_weight_map(combo_rows)
Kit.check("合成序:图级偏斜先(乘法面)件级 luck_bias 后(加法 +2×四稀有类)——合计 77+8", eco.roller._pool_weight(combo_rows) == 77 + 4 * 2)
Kit.check("合成序构造例:skew(heal 3)+2=5 / gp 2+2=4 / pierce 9+2=11 / magic 9+2=11(图级定型后玩家构筑叠加)", cw.get("heal_minor", -1) == 5 and cw.get("gp_coin", -1) == 4 and cw.get("pierce_strike", -1) == 11 and cw.get("magic_bolt", -1) == 11)
@@ -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)
@@ -674,7 +681,7 @@ func _smoke_tree_rules() -> void:
ctx_fz2.level_bucket = lvl_shared # 挂同一 level 桶引用(新形态跨战持久:无 export/seed,同字典即持久)
Kit.check("锋:限次跨战持久(level 桶共享引用——替代旧副本搬运链)〔无状态化 V4〕",
(fz2.dispatch(ctx_fz2, "mine_hit", {}, Callable()) as Array).is_empty())
# 锋·惊巢反制效果行换代断言(迭代36 u13/wa12——_grant_routed 减半分支效果行=战斗日志
# 锋·挖掘惩罚减半效果行换代断言(迭代36 u13/wa12——_grant_routed 减半分支效果行=战斗日志
# step_row 真实生产面;display_name 玩家可见,禁字「挖雷」由此行清零钉住)
var orch_nc = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777003))
orch_nc.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
@@ -687,17 +694,17 @@ func _smoke_tree_rules() -> void:
# heal 负向行表值 power=6(极性字典行值——负向 power 走表值不走快照)→ halve 50% ceil=3
orch_nc.reward_grant().grant_routed({"cell_type": "heal", "power": 10}, "negative",
{"source_class": "mine_hit", "cell": Vector2i.ZERO, "ri": 0, "halve_pct": 50})
# 受理判据=惊巢反制效果行落 step_row(halve 分支先于 dispatch 发行——flat 行 _apply_flat_row 分派
# 受理判据=减半效果行落 step_row(halve 分支先于 dispatch 发行——flat 行 _apply_flat_row 分派
# 恒返 false,返回值不承载受理语义;行发行即分支受理)
var nc_hit := ""
for r in nc_rows:
if String((r as Dictionary).get("display_name", "")).contains("惊巢反制"):
if String((r as Dictionary).get("display_name", "")).contains("挖掘惩罚减半"):
nc_hit = String((r as Dictionary).get("display_name", ""))
break
Kit.check("锋:惊巢反制 mine_hit 减半分支受理(wa12 前提——halve_pct=50 效果行落 step_row)〔wa12〕",
Kit.check("锋:挖掘惩罚减半 mine_hit 减半分支受理(wa12 前提——halve_pct=50 效果行落 step_row)〔wa12〕",
nc_hit != "")
Kit.check("惊巢反制效果行==「惊巢反制——危险格挖掘惩罚减半(6→3)」零「挖雷」(u13 换代)〔wa12〕",
nc_hit == "惊巢反制——危险格挖掘惩罚减半(6→3)" and not nc_hit.contains("挖雷"))
Kit.check("减半效果行==「挖掘惩罚减半(6→3)」零「挖雷」(u13 换代)〔wa12〕",
nc_hit == "挖掘惩罚减半(6→3)" and not nc_hit.contains("挖雷"))
# 守侧五场景(契约 12):编排 rig——begin_battle 携 sense_guard,授予段 _grant_seg._judge_seal 直注
var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777001))
Kit.check("守侧 rig:战斗编排启动(tree_rules=sense_guard 携带)",
@@ -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)
Loaded 100 of 167 files, more files were not shown because too many files have changed in this diff. Show more