Compare commits

...
37 Commits
Author SHA1 Message Date
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 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 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 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 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
72 changed files with 3835 additions and 878 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 目录未隔离那类在册环境坑)走异常分级处置,不算未闭合
+5 -5
View File
@@ -1,6 +1,6 @@
# 美术圣经(Art Bible)— Dungeon Sweeper
> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**;现行为 v3.42,历次修订明细见 git 提交史
> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**;现行为 v3.43,历次修订明细见 git 提交史
>
> **Art Director Sign-Off(AD-ART-BIBLE)**:历次批均未签核——此为 Lean/Solo 模式(非 PHASE-GATE)下的现行治理状态;full 门禁签核时机待用户表态。
> 风格基调:地牢题材但**非暗黑风**——轻快明亮的幻想冒险风,全年龄;像素风;角色 2~3 头身。
@@ -215,7 +215,7 @@
- **换色合并登记五处(用户裁,部件色落值)**|按本表「不过门即触发换色合并、禁加色绕行」执行,五处**零新增额度**(各件原拟新增候选一律不取):① 主角铲刃浅铁灰(原拟 `#A8AEB8`)→ 并入 `FOG_MEMORY_INK #C4C4C4`,**部件取值 = `#C4C4C4`**;② 主角提灯铜色(原拟 `#C08A3A`)→ 并入 `GOLD_DEEP #B07A14`,**部件取值 = `#B07A14`**;③ 食人魔酋长暖褐皮(原拟 `#B08050` / `#A97C4E`)→ 并入 `暖石 #B8A582`,**部件取值 = `#B8A582`**;④ 石君王灰皮(原拟 `#9A968C` / `#949089`,按**次级皮**口径——板甲视同体块主色、灰皮属次级皮故不受 L 0.22–0.45 约束)→ 并入 `STONE #786E62`,**部件取值 = `#786E62`**;⑤ 轻甲护甲图标木色 `#8A5A34`(深素底下 2.72:1 不过门;原拟新增木色亮档 `#C49A5E`)→ 并入 `WARM_FAINT #CDB88C`,**部件取值 = `#CDB88C`**(8.08:1)。**一色两岗代价已随裁定接受**:① 的 `#C4C4C4` 语义岗 = 迷雾记忆墨(§6.5)、② 的 `#B07A14` 属金族(§4.2 金只说收益与兑现)——两者均作角色随身件材质色用,不进信息层与语义物件岗;③④⑤ 无语义岗冲突。验收:机检〈五处部件取值点全部改指合并目标色,四枚原拟新增候选值零入板〉
- **换色合并登记三处**|按本表「不过门即触发换色合并、禁加色绕行」执行,三处零新增额度:①地台顶缘受光线 `#C4AC84` 已并入 `#C9AF83`(掩尘枚 `BRICK`,与 §8.2 高墙口沿高光同枚)——源常量已删除(全仓零引用,退役断言测试串为预期例外),绘制点全部改指 `#C9AF83`;②地台描边 `#5A4C3A` 已并入 `#4C4038`(代码 `PLAZA_EDGE`,ui_kit 唯一落点)——源常量已删除,绘制点全部改指;③舞台底 `#3E342A`(代码 `STAGE_BG`)→ 并入 `#3E362C`(状态槽底)——源常量已删除(常量表零键),背景资产接管该绘制面,程序化平色缺图回退改指 `SLOT_BACKDROP`(`#3E362C`)。程序化回退绘制与已落合并值的贴图保持一致。验收:机检〈①②两枚源常量全仓零引用,绘制点全部改指合并目标色〉
**对比硬指标(可测)**:数字可读性由**格底明度分档**承载——棋盘数字亮墨 **L≥0.75** 对**已开格底 L 0.08–0.14** ⇒ 直画对比 **≥4.5:1 恒成立**(WCAG AA,样张逐组合抽测冻结);**色牌后备**:色牌素色恒定 **L≥0.70**、牌上数字深墨 **L≤0.027** ⇒ 任意牌墨组合 ≥4.5:1(实测样例:#33261B/#F5EAD3 = 12.3:1)。未开格底槽(含全部图类型)相对亮度 **L≥0.30**(墙顶面受光档)、HSV 饱和 **≤0.55**、避 Boss 禁入色相带;已开格底走世界底层档(见上)。物件识别不依赖格底(§5.1 轮廓通道),封印印记、金色不作识别依赖。验收:机检〈亮墨对已开格底 ≥4.5:1;未开格底 L≥0.30、饱和 ≤0.55;任意牌墨组合 ≥4.5:1〉 ←原二;地形变异 AC-9
**对比硬指标(可测)**:数字可读性由**格底明度分档**承载——棋盘数字亮墨 **L≥0.75** 对**已开格底 L 0.08–0.14** ⇒ 直画对比 **≥4.5:1 恒成立**(WCAG AA,样张逐组合抽测冻结);**色牌后备**:色牌素色恒定 **L≥0.70**、牌上数字深墨 **L≤0.027** ⇒ 任意牌墨组合 ≥4.5:1(实测样例:#33261B/#F5EAD3 = 12.3:1)。未开格底槽(含全部图类型)与类型微光、已开格底之间存在**相对亮度与相对饱和关系**——禁止底槽吞没微光环、禁止底槽与已开格底同档;本节只定这层关系,阈值与准入判定由图类型色板互辨抽测的执行件承载(用法登记见 [art-pipeline-tools](art-pipeline-tools.md))。已开格底走世界底层档(见上)。物件识别不依赖格底(§5.1 轮廓通道),封印印记、金色不作识别依赖。验收:机检〈亮墨对已开格底 ≥4.5:1;未开格底对微光环与对已开格底的相对亮度关系经互辨抽测准入;任意牌墨组合 ≥4.5:1〉 ←原二;地形变异 AC-9
**色板增量纪律(数值状态 = 待定现用)**:全作色板硬上限 **48 色**(防色板无限膨胀的封口条款;**额度已用尽、余量 0**,后续余量以色板检查器实报为准);新色入板须同时过两道门:①§4.2 语义检查(不占语义色段/禁入带);②**与既有任一色的 RGB 三通道差禁止全部 <8/255**——即三通道里至少一通道 ≥8/255 即过门,「三通道差全部 <8」才算不过门(条文原写的「三通道差 ≥8/255」有「逐通道全过」与「禁止全灭」两种互斥读法,前者下现行色板 247 对不达标、与本节自认的「现行即执法」合规状态不能同真,故取后者;用户裁)。**额度已用尽:后续任何新增须先压缩或合并既有色**——按本节「额度不足时呈报作者」口径呈报,**压缩或合并哪几枚由作者裁,本节不代裁、不预指对象**。反例实证:#3A2E22 ↔ #362B20 三通道差均 <5,肉眼不可辨,同板共存只增噪。不过门即触发「换色合并」(把意图色并进最近既有色),**禁加色绕行**;加色义务同时登记于本表修订。**「待定现用」态释义**:数值**现行即执法**(色板检查器按此拦,§8.3)——非候选(不存在"等冻结"空窗)、非死规(后续需求引入时可变);调整走本节修订并登记,改值 = 修订不是违约。验收:机检〈色板总数 ≤48;新色入板过两道门〉
@@ -392,8 +392,8 @@
- **情绪与色域**:承载 §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, 203, 1280, 164)`、底锚 y367**(桩身 150px、门柱 164px 终稿锁定值,落带内〔围墙带 (0,203,1280,164)〕)。
- **色彩与氛围依据**:营地场景的冷暖分工(冷的大面积后退、暖的小面积前进)、空气透视三档与大面积用色规则 = `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,**浮层覆盖,不挤压画面**)。
- **事实源边界**:解锁条件/价格/存档写入归元进度 GDD 与 `MetaProgression`;素材命名与路径归 `asset-path-map`;清单登记归 `art-assets-checklist` §3.5。
@@ -401,7 +401,7 @@
### 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%),改探针口径须同步本件。
+75 -31
View File
@@ -1,7 +1,7 @@
# 营地场景 · 分层生图提示词卡 v1.5
> **性质**:营地场景图(1280×800,一张可点击场景图 + 十张分层:背景层 1 + 围墙层 1 + 八个物件层)的出图执行侧文件。**场景级资产,与角色级 [art-prompts](art-prompts.md) 并列,两文件互不覆盖**。
> **依据**:色彩与氛围 = `reports/提案-营地色彩与氛围脚本-2026-10-23.md`(本卡不复述色值);坐标与层序 = 本卡 §2.3(运行期单一事实源 = `game/data/tables/camp_scene_layout.tres`);物件命名与消费链 = [asset-path-map](asset-path-map.md);画法与交付形态 = [art-bible](art-bible.md) §6.2。冲突时色彩以色彩脚本为准,画法以 art-bible 为准。
> **依据**:色彩与氛围 = [art-bible](art-bible.md) §2.8 与 §6.2(营地整体偏暖、安全温馨,与冒险中的地下城形成对比;本卡不复述色值);坐标与层序 = 本卡 §2.3(运行期单一事实源 = `game/data/tables/camp_scene_layout.tres`);物件命名与消费链 = [asset-path-map](asset-path-map.md);画法与交付形态 = [art-bible](art-bible.md) §6.2。画法与用色冲突时以 art-bible 为准。
> **边界**:只管营地场景图的分层出图。角色、图标、格面走 art-prompts.md 与脚本管线,不需要提示词。
> **建立**:2026-10-25 · 状态:十条提示词成文(背景层 1 + 围墙层 1 + 八个物件层);2026-11-05 按地平线 y367 重排,背景层与围墙层待按 §4 新判据重出,门扇随门洞压扁重画
@@ -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,24 +39,53 @@ 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 倍,保证整数倍降采样)
**地平线**:出图 y734(终稿 y367,画面高度 45.9%)——山脚到此为止,其下全是营地地面。山脚轮廓必须起伏错落、与草地顶部的明度接近,**不可读成一条平直的横向色带**(旧稿在 y321/y551 出现两条 1px 硬切分带,判红)
**地面分两区**:
- **营地外**(出图 y734 及以上,山脚到地平线):冷调灰绿草地,质地均匀偏低对比,中间一条土路从门洞位置向远山收窄
- **营地内**(出图 y734 到画面底):踩实的裸土,灰褐、去饱和、**与外面的草地明显不同色相**;有拖拽脚印、碎石与踩踏斑块,**无草、无路、无小径、无车辙**
- 两区的分界线正好被围墙层(y406–734)完全遮住,画面里看不到硬分界
**地面明度**:营地内地面中段档 30%–45%(比天空暗、比黑带明显浅),禁整片压暗
**画什么**:天空、远山、营地外的草地与土路、营地内的踩实地面
**不画什么**:木围墙、门柱、门扇、七个物件、角色、接地阴影——围墙与门柱是 §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. The hills sit in the upper middle of the frame: their lowest foot lands at y=734 of 1600, which is 45.9 percent of the frame height measured from the top edge, and their peaks stay well below the reserved empty top strip. The hill foot must be IRREGULAR — rolling bumps of unequal width, some reaching lower than others — so the boundary never reads as one straight horizontal line. HILLS TO GROUND: at y=734 of 1600 the hills end and the ground begins, and the two must join invisibly — the ground takes the same pale cool tone as the hill foot where they meet, then darkens slowly and evenly towards the bottom of the frame. There must be NO separate horizontal stripe, NO flat band of a third colour, NO hard one-pixel colour step anywhere: a viewer must not be able to point at a straight line and call it the horizon. OUTSIDE GROUND: from the hill foot down to the ground line at y=734 the land outside the camp is open meadow — cool pale grey-green grass, flat and low in contrast, a little lighter and cooler than the camp floor, marked only by a few very simple flat grass tufts; no trees, no bushes, no flowers. GATE PATH: inside this meadow only, one dirt path starts at the ground line y=734 within the opening between x=1040 and x=1520, runs up into the hills and narrows until it disappears between them. It must stay ABOVE the ground line — there is no path, trail, road or track of any kind below y=734. Painted in a muted desaturated earth tone, never golden or warm yellow; it shows through the empty gate opening of the wall layer above. CAMP FLOOR: every pixel below the ground line y=734 is the floor inside the camp — trodden bare packed earth in desaturated warm grey-brown, mid value around 30 to 45 percent luminance, deliberately DIFFERENT in hue from the cool green meadow above, so the two zones never read as the same material. It has NO grass, NO vegetation, NO path, NO trail and NO track: only dragged footprints, short scuff marks, scattered small pebbles and beaten patches of trodden earth, denser towards the bottom edge and calm in the middle. A few tones only; no gradient, no dithering. The camp floor is completely empty of 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 straight horizon line drawn as a hard colour step or a third flat band; a visible horizontal seam where the hill foot meets the meadow; trees, forests or foliage in the distance; a dense carpet of plants, a lawn, or vegetation covering the whole foreground; any plant detail taller than a few pixels; 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 anywherLine truncated
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 坐标直接对齐);墙横贯画在出图 y406–734 这条带内(桩身 y434–734,门柱 y406–734),其余为纯色底 `#F2F2F2`。落盘由 S4 后处理裁成终稿外接矩形 `(0, 203, 1280, 164)` / 出图 `(0, 406, 2560, 328)` 的细条,**不做全幅 1280×800 落盘**(全幅约 3.9 MB 显存,细条约 0.35 MB)。
@@ -72,13 +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 |
| 地平线(山脚/地面分界) | **y 367** | **y 734** |
| 远景片 `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 |
| 门洞内小路(画在 bg 层) | x 520–760 穿过门洞,向上收窄没入远山,向下展宽贯通至画面底 y800 | x 1040–1520 穿过门洞,向上收窄没入远山,向下展宽贯通至 y1600 |
| 门洞内小路(画在远景片草地内) | x 520–760 穿门洞,自接缝 y367 向上收窄,到山脚 y202 消失 | x 1040–1520,自 y734 向上收窄,到 y403 消失 |
---
@@ -159,17 +192,27 @@ 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
- 明度:地面中段域 **L 0.30–0.45**(相对亮度);整片低于 0.30 即判红(旧稿偏暗、观感发闷)
- 冷色像素占比 ≥55%;本层不应出现任何暖色高饱和像素
- 机检:y734 上下各 8px 窄带的行均色差 ≤6/255——山脚接地面处禁出现 1px 阶跃(旧稿 y321 处行均色差约 37/255)
- 目检:顶部约 1/16 完全空旷;**地面自出图 y734 起**,山脚轮廓起伏错落、与地面顶部明度衔接——**禁出现平直的横向色带分界**(旧稿 y321/y551 两条 1px 硬切即判红);不含围墙、门柱、门扇、物件、角色
- 目检:地面**有可辨细节**(稀疏草簇、石子、车辙、脚印),中段留白;不能是一整片平色
- 目检:门洞 x1040–1520 处可见一条**向远处收窄的土路**(位于 y734 以上、山脚之间),非纯色块;**y734 以下属营地内地面,禁出现任何路面、路径或车辙**
- 机检:y734 以下,画面中线 200px 带与左右各 250px 带的平均色差 ≤6/255(营地内无路带)
- 机检:营地内地面(y734 以下)与营地外草地(y560–730)的平均色 (R−B) 差 ≥10/255(两种材质色相可辨)
- 机检/目检:全幅**无任何独立物件/图标/徽章/标记**
**远景片 `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` 可去底、奶油描边闭合
@@ -196,7 +239,8 @@ A campfire made of a ring of grey stones around crossed stacked logs with a warm
4. **帐篷**:251×170 沿用现终稿,落位 (982, 335),遮住墙下部 32px(覆盖围墙可接受)
5. **笔记本**:随帐篷跟随,落在帐篷左下方外侧、两者不重叠——`x = 帐篷 x − 112`、`y = 帐篷底 + 10`
6. **宝箱离底**:贴图底 y744,距画面底 56px;其余物件同步上移 46px
7. **地面分两区**:营地外(y734 以上)=冷调灰绿草地+一条向远山收窄的土路;营地内(y734 至画面底)=踩实裸土、与草地异色相、无草无路无车辙;两区分界被围墙层完全遮住
8. **布告板位置**:(845, 224, 130, 140),底 345 落在墙内,不再悬空
9. **描边颜色**:奶油色追认——去底脚本「奶油描边膨胀 3px 当屏障」的技术前提随现值锁定(§1 注记同源);改深灰褐须换去底方案全量重出,不裁
10. **营地环境子板色值**:不前置拍板——量化场景实际用色后按 art-bible §4.1 两道门登记(语义检查+RGB 三通道差 ≥8/255),本卡仍不复述色值
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. **实机目检归用户,只登记不阻塞**——真人检测项登记进实现报告目检总清单与体验验证清单即完成批内义务;禁把等目检当停工理由或完成前置
+4 -2
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**:玩家可达性与观感;真实实例化场景、驱动真实输入。
-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` 键名、值为整数点数——键名与量纲是数据导出与分析工具的兼容契约,改动须连动分析工具面。没生效也要留痕:两类"未生效"行如实记录合法例外,呈现时聚合计数,不逐行刷屏。
任何一行找不到来源即断言失败——宁可返回错误,也不让"合并到看不出来源"的呈现混过数据层。但有些行天然没有棋盘来源,断言为此留了两层豁免,豁免集合是封闭清单,改动同样走本文修订。
+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
+91
View File
@@ -0,0 +1,91 @@
# 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:
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):
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:
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")
+389
View File
@@ -0,0 +1,389 @@
# 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:
_checks += 1
if not ok:
_problems.append(desc)
func _ready() -> void:
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:
_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:
_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:
# 重建到中段可居中停点(候选=停点出边——候选节点居屏中可拖可达)
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:
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:
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:
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):
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):
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:
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=新档钮缺失(驱动中断)。
+28 -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)
@@ -122,11 +129,11 @@ func _step_camp_back_to_menu() -> void:
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("③营地→菜单(回退)", "菜单")
@@ -150,7 +157,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 返回后选档未隐藏或开始层未恢复(关闭链失守)")
@@ -228,7 +235,7 @@ func _step_end_back_to_menu() -> void:
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:
@@ -254,8 +261,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 +294,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〕")
@@ -323,11 +332,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 +371,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 +382,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 +394,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 +406,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 +431,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 +456,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〕")
@@ -463,7 +474,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 +491,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
+76 -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,6 +50,9 @@ 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 不遮挡;浮层角标遮挡判据见设置面段)
@@ -67,7 +72,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 +90,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 +105,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 +117,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 +129,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 +139,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 +184,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,7 +200,11 @@ 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
@@ -213,7 +236,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 +262,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 +277,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 +288,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 +296,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 确认路径失守(应删档+关窗)")
@@ -343,7 +366,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("设置面关闭后未恢复开始层")
@@ -377,7 +400,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 +486,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 +503,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 +547,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 +569,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 +584,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 打开时输入未锁定")
@@ -612,7 +641,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 +655,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
@@ -665,7 +696,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:
# 豁免:放弃路径收口时终局「返回」后仍挂三选一/关卡推进残留态(停点链),
@@ -693,7 +724,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,6 +826,7 @@ func _ready() -> void:
exit_code = 1
if not _problems.is_empty():
exit_code = 1
_recorder.dump_js() # 收尾落盘(大门段/开场行段点击在循环退出后——落盘必须在 quit 前)
get_tree().quit(exit_code)
@@ -821,7 +853,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 +883,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 +935,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
@@ -1027,8 +1061,8 @@ 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()
# 停手走真实点击路径(AP 条「停手」钮 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体)
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.ap_bar._btn_stop)
# ---- T1 D1:锁期/重放断言(每 run 一次,脚本化场景——演播期豁免的断言化承载) ----
+16
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():
+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/
+1 -1
View File
@@ -160,7 +160,7 @@ tests/
| 四柜台拆解后五界面入口改挂与写口零回归 | 营地五界面 | 五界面意图信号 → `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`(篝火/开合段) |
| 关卡画面视口跟随 | 关卡画面(选路枢纽) | 停点切换与兑付回图经真实屏流程;读面 `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`(结构禁词段) |
| 营地刷新单口与场景根 HUD 同步 | 营地刷新总线 | 写口尾 → `CampRefreshBus.refresh()` → 现开界面刷新+场景根 HUD 刷新 | `ui_struct_probe.gd:_ready`(顶栏位段);`ui_click_probe.gd:_ready`(写口尾刷新段) |
+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

@@ -236,7 +236,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)
@@ -674,7 +674,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 +687,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 携带)",
@@ -11,12 +11,33 @@ const Kit := preload("res://tests/unit/test_kit.gd")
const SRC_DIR := "res://data/tables"
const BAD_OVERFLOW_DIR := "user://effect_kind_fixture_overflow"
const BAD_SEGMENTS_DIR := "user://effect_kind_fixture_segments"
const BAD_SPIKE_DIR := "user://effect_kind_fixture_spike"
const BAD_TRADEKEY_DIR := "user://effect_kind_fixture_tradekey"
# 注入探针:死通道 do.kind(甲案摘名两族——各一探针行)
const PROBE_OVERFLOW := ' {\n "feat_id": "kind_overflow_probe",\n "display_name": "通道探针·溢出",\n "rarity": "common",\n "triggers": [ { "on": "dig_reveal", "when": [], "do": { "kind": "overflow", "value": 50 } } ],\n "desc": "夹具探针——甲案退役词 overflow"\n},\n'
const PROBE_SEGMENTS := ' {\n "feat_id": "kind_segments_probe",\n "display_name": "通道探针·连珠",\n "rarity": "common",\n "triggers": [ { "on": "dig_reveal", "when": [], "do": { "kind": "attack_segments", "cap": 2, "divisor": 3, "value": 0 } } ],\n "desc": "夹具探针——甲案退役词 attack_segments"\n},\n'
# 注入探针:改写族退役词(改写族清退批 T2)——spike kind 出域与 trade 键出域(各一探针行)
const PROBE_SPIKE_MODE := ' {
"feat_id": "kind_spike_probe",
"display_name": "词表探针·甲",
"rarity": "common",
"triggers": [ { "on": "dig_reveal", "when": [], "do": { "kind": "spike", "value": 2 } } ],
"desc": "夹具探针——改写族退役 kind spike"
},
'
const PROBE_TRADE_KEY := ' {
"feat_id": "kind_tradekey_probe",
"display_name": "键闭探针·乙",
"rarity": "common",
"triggers": [ { "on": "dig_reveal", "when": [], "do": { "kind": "grant_ap", "trade_hit": 5, "value": 50 } } ],
"desc": "夹具探针——改写族退役 do 键 trade_hit"
},
'
func _inject(dir: String, probe: String) -> bool:
var path := dir + "/feats.tres"
var f := FileAccess.open(path, FileAccess.READ)
@@ -65,5 +86,11 @@ func _init() -> void:
if not _assert_rejected(BAD_SEGMENTS_DIR, PROBE_SEGMENTS, "effect_kind_retired_test/②attack_segments 出域"):
quit(2)
return
print("[effect_kind_retired_test] 双探针均正确拒绝(甲案词表退役——死通道误写加载致命)〔架构整改 V3〕")
if not _assert_rejected(BAD_SPIKE_DIR, PROBE_SPIKE_MODE, "effect_kind_retired_test/③词表闭面:kind=spike 出域拒绝"):
quit(2)
return
if not _assert_rejected(BAD_TRADEKEY_DIR, PROBE_TRADE_KEY, "effect_kind_retired_test/④do 键闭面:trade_hit 出域拒绝"):
quit(2)
return
print("[effect_kind_retired_test] 四探针均正确拒绝(甲案词表退役+改写族清退——退役词/键误写加载致命)〔架构整改 V3〕〔改写族清退 T2〕")
quit(1)
@@ -212,9 +212,10 @@ func _test_negative_cases() -> void:
# ---- V11:效果类型闭集零新增(满级特效本期不开新词)----
# 实数订正(L2 在册):卡片 F11「24 种」系设计文档事实快照,
# 现行 31;本套件零扩签,钉现行数(将来任何加词即转红提示复核)。
# 32→31:改写族清退批摘 spike(CI run 87 追绿——词表合法收窄的断言随动)。
func _test_effect_kinds_closed() -> void:
Kit.check("效果引擎效果类型闭集 32 种(甲案词表退役——overflow/attack_segments 摘名)〔锻造工坊批 V11〕",
EffectEngineScript.EFFECT_KINDS.size() == 32)
Kit.check("效果引擎效果类型闭集 31 种(甲案词表退役 overflow/attack_segments+改写族清退摘 spike)〔锻造工坊批 V11〕",
EffectEngineScript.EFFECT_KINDS.size() == 31)
Kit.check("效果引擎事件词表在册 21 种(charge_full 位摘名——蓄力层删键批)〔锻造工坊批 V11〕",
EffectEngineScript.EVENTS.size() == 21)
@@ -221,7 +221,10 @@ func _test_layout_cache(db) -> void:
await process_frame # 换代图真实绘制路径重算(缓存随内容变失效重写)
var pc1: Dictionary = screen.layout_probe(chain)
var pc2: Dictionary = screen.layout_probe(chain)
# 第三判据原=新旧图 id0 位置差——改前靠容器延迟收敛的 origin 中间态偶然成立(换图首探针
# 画布未及重排,origin≠0);视口化批画布同帧收敛(canvas.size 恒=内容包络)后两图 origin
# 恒等、id0 同层同槽位置恒等,前提失义。等强换键=重算字典键数恰为新图节点数(旧缓存
# 63 键,换代不重算必不成立),同一性判据(核心失效重算面)不动。
Kit.check("布局缓存换代:新图查询返回新实例非旧缓存(失效重算生效)〔levelmap批 V2〕",
not is_same(p1, pc1) and is_same(pc1, pc2)
and (pc1[chain[0]] as Vector2) != (p1[nodes[0]] as Vector2))
not is_same(p1, pc1) and is_same(pc1, pc2) and pc1.size() == chain.size())
screen.queue_free()
+21 -10
View File
@@ -1,15 +1,9 @@
# 试玩问题记录
- 一开始不能让角色的成长系数太高.比如在没经过升级之前,你的成长系数是跟不上关卡怪物难度提升的;但是后续能力解锁之后,成长系数才能匹配得上
- UI优化,第二第三关的棋盘看着太累了;不知道是因为亮度还是对比度的原因,看着非常费劲,非常不舒服;
- 第一关的第二个节点数值有问题
- 现在的立绘太丑了,像素化的粒度太低了,导致根本看不清立绘; 看是否可以改回用较为高清的图片?
还有一个问题是大小的问题.我们或许可以调整一下不同怪物的大小,因为现在地精的图片看起来比熊地精还要大,这并不合理;
- 修改地图的奖励类型
战斗节点增加不同的地图类型,比如有些图踩雷受到更多的伤害,有些图标雷可以造成更多的伤害,有些图伤害会减半需要凑更多清图奖励(这些都是暂时的设定)
@@ -22,10 +16,27 @@
- 3选1奖励中有一个refresh,任何说明都没有,就一个refresh, 明显的一个问题物品,但是却没有处理;
- "奇物门票" 这他妈是专场门票,而且你他妈应该有一个更好的名称,而不是直接"门票"; 我真是操了你妈;
而且里面的解释非常弱智
- 确认选择按钮旁边有一个"免费选择,点刷新不消耗AP" 我真是操你妈了,要你这种傻逼解释;
你他妈这是游戏的UI吗?设计像你妈坨屎;
- 封印回响这个专长描述也是错误的,怎么是本轮AC+1呢
- 封印回响这个专长描述也是错误的,怎么是本轮AC+1呢
## 2026-11-04 营地相关问题记录
1. 背景图存在着很多问题上半部分的内容太长了,我们应该预留更多的地面细节给我们的可点击的地方;
但是现在远景占据了太多的位置;
远景应该压缩到只有1/3的高度(现在占据了2/3)
近景的地面太朴素且太暗了;
远景压缩之后,围墙,门,告示板记得也同步上调;
2. 围墙贴图没对齐就算了, 过于矮? 高度应该>门
3. 物品太靠下了; 整体可以往上调, 让最下面的宝箱和最底下保持着一个32px的距离
4. 钥石说好是一条顶栏的,为什么现在是搞了一个类似浮动框的形式展示?
为什么我设计定义的东西你没有进行实现,而且还自作主张地做了一个没有经过我同意的展示方式
5. 点击大门开始新冒险,直接回到了开始菜单界面;
你是否真的做过完整的测试?
我这句话问了都白问,因为你做了完整的测试的话,就不可能会这么简单的逻辑闭环都没有完成
6. 开始新冒险的是和否反过来了.否在左边,是在右边; 完全的反直觉; 完全的弱智
@@ -0,0 +1,62 @@
# QA harness 三批——缺陷与覆盖遗漏报告(2026-11-06)
> **接收方**:待办清单「QA harness 三批缺陷与覆盖遗漏收口」条目(P0)。
> **来源**:合并 d3c85390(批 1 探针真输入化+覆盖登记/批 2 覆盖总览面板+本体枚举器/批 3 自动试玩扩展)三批实施过程的真实跑批读数。
> **口径**:本报告只列**发现且未解决**的缺陷与测试遗漏(实施期已自修的环境/机制坑不在列,见 qa-harness 讨论文档 §10–§12);明细可查出处逐条标注。
## 一、产品缺陷(2 类)
### A1 开局被开始层盖住(大门开局开始层滞留盖屏+输入锁死)
- **现象**:大门确认开局冒险后,开始层(start_overlay)滞留可见;它以栈内模态身份直入输入锁谓词(`game/presentation/modal_flow.gd:77` 的 `start_visible_get` 项),战斗全程 `locks_input()==true`,玩家无法操作。
- **发现**:C 自动试玩首跑(journey_player)即机器复现——上报 2 条(同缺陷双指纹),归因字段在案(`game/tests/.gates/coverage/journey_report.js`)。
- **现状**:**营地交互修复批已修未合**(camp-interact 分支 0977a548,main.gd 大门两出口补隐藏句)——该分支合回 master 后本缺陷即消,C 的对应上报自然消失。
- **归属**:随 camp-interact 合回即消,无需新批。
### A2 终局面板关闭路径引擎报错噪声
- **现象**:`game/presentation/views/battle_end_overlay.gd:194` 的 `get_meta(&"celebrate_tw", null)` 在 meta 缺失时仍触发引擎 SCRIPT ERROR 打印(当前引擎版对显式 null 缺省值照样报错);关闭动线功能不受影响,属持续输出的日志噪声。
- **发现**:批 1 探针直跑日志在案(screen_flow_probe 终局段每次触发)。
- **修法**(一行级,未实施):改 `has_meta()` 前置判断。归属=需求性工程条目(改游戏行为),走写卡通道。
## 二、测试覆盖遗漏(6 类)
### B1 停手钮(AP 条)零真输入覆盖
- **现象**:两探针与 C 均未以真输入点过「停手」钮——emit 时代信号直发绕过 disabled 掩盖了驱动空洞;停手使能位=alive∧digging 投影(`game/presentation/views/ap_bar_view.gd:147`、`hud_presenter.gd:92`)。
- **登记处**:覆盖面板控件下钻(ui_click 探针 seen 91/命中 27 的缺口之一);`probe_hits_ui_click.js` 零停手键。
### B2 视图层测试方法面未真输入化
- **现象**:弃图刷新确认链(press_refresh_for_probe/press_confirm_for_probe)、地图候选(press_candidate)、节点与三选一停点(press_continue/press_card/press_confirm)等驱动走视图方法口,非输入管线——保「逻辑通」不保「玩家点击可达」。
- **登记处**:待办清单「测试面真输入化分期与存量断言逐条精锚」条目①余项;C 的两方法口在归因字段如实标注通道。
### B3 存量 996 条断言判据出处精锚
- **现象**:链式锚已保可核(capability_audit 链断 0),逐条出处粒度精锚未做。
- **登记处**:同上条目②,原样在案。
### B4 本体对账缺口 212 条/62 类
- **现象**:游戏本体实体(data/tables 行集、schema 词表与字段、场景按钮、自绘命中类、屏模态清单,共 865 实体)在测试断言文料中零关联 212 条——「游戏做了但从未被验证」的候选面。
- **诚实边界**:grep 关联有假阳假阴(断言测了实体但文本没带 id=假缺口;文本带 id 但断言弱=假覆盖)——**须人工逐条复核后再立事项**,禁直接照单开工。
- **登记处**:覆盖面板第四节(`tools/coverage_report.html`);`game/tests/.gates/coverage/ontology_gaps.js`。
### B5 控件级零真输入命中缺口(ui_click 64 处/screen_flow 97 处)
- **现象**:探针真输入登记的「存在但零命中」控件缺口——大头=未被探针打开过的屏(完备性边界在案:控件级视图只对被打开过的屏负责),其中混有 B1 类真空洞。
- **登记处**:覆盖面板第三节;`probe_hits_ui_click.js`/`probe_hits_screen_flow.js` 的 seen−hit 差集。
### B6 C 判据收紧轮未做
- **现象**:全三关推进约 8 分钟(冒险段帧预算 60000 兜底);契约条款「连 3 批零真缺陷→判据收紧一轮」的触发权与执行归用户复核轮。
- **登记处**:`journey_report.js` meta(上报数/静默数/fatal/屏到达进回执)+qa-harness 讨论文档 §5。
## 三、处置归属(汇总)
| 组 | 项 | 通道 |
|---|---|---|
| 随合即消 | A1 | camp-interact 分支合回,无需新批 |
| 写卡通道 | A2、B1、B2 | 需求性工程条目(改游戏行为/测试驱动)→ 需求卡片 |
| 既有义务 | B3、B6 | 衔接条目②+用户复核轮 |
| 人工复核 | B4、B5 | 面板逐条复核后按性质分流(真缺口立事项/假缺口面板豁免清单核销) |
@@ -1,147 +0,0 @@
# 提案:营地色彩与氛围脚本(2026-10-23)
> **性质**:设计讨论段过程稿。**只确立「营地该长什么样、什么风格、什么感觉」,不出图、不改 `art-bible` 主色板。**
> 上游依据:`art-bible` §2.8(营地情绪)、§4.1 色板分层与对比硬指标、§4.2 适用域条款、§6.1 战斗背景后退条款、§6.2 营地场景、§8 定值单点纪律;外部调研见 §4。
> 下游:本脚本是 §13 生图提示词的色彩依据;色值候选在此**单点**提出,待作者追认后按 §4.1「场景环境子板」条款登记(环境子板每屏一套、≤32 色、允许跨屏不共享、不占 48 token 额度)。
---
## 0. 四问速览(只读这一段也能定)
| 问 | 答 |
|---|---|
| **① 现状差在哪** | 营地整屏发黄发闷。实测:天空黄度 51、地面黄度 81(黄度 =(R+G)/2−B,越大越黄)。**占屏约七成的大面积——天空、远山、地面——全是暖色**,找不到一块冷色来平衡。 |
| **② 建议做什么** | 给营地补一张**环境子板**,按「**冷的大面积后退、暖的小面积前进**」重新分工:天与山与地走冷灰,暖色只留给物件(火、木、石),火焰是唯一高饱和。 |
| **③ 为什么选它** | 这是空气透视的通行做法(远处更淡、更冷、对比更弱,近处更暗、更暖、对比更强),且 `art-bible` §4.2 已写明**环境层不受色相独占约束**——远山走冷色完全合规,不需要动任何暖色条款。 |
| **④ 怎么验证** | 出图后按 §2.5 的面积占比与黄度阈值实量:天空黄度 ≤20、地面黄度 ≤35;暖色面积 ≤30%。这两条可机检,不靠眼估。 |
---
## 1. 诊断:黄不是设计意图,是三个缺口叠加
### 1.1 事实(出图实测,非推测)
| 版本 | 天空 RGB | 天空黄度 | 地面 RGB | 地面黄度 |
|---|---|---|---|---|
| 首版方向稿 | (249, 229, 188) 奶黄 | 51 | (157, 119, 57) 暖褐 | 81 |
| 修正天空后 | (217, 221, 202) 灰白 | 17 | (146, 128, 97) 仍偏褐 | 40 |
天空改冷后黄度掉到 17,画面立刻不那么"尿黄"——**说明元凶是大面积的色温,不是物件本身**。地面只降到 40,因为当时提示词里还挂着棋盘色 `裸土 #B39A78`。
### 1.2 三个缺口
**缺口一:营地没有自己的环境子板,大面积色是借来的。**
`art-bible` §4.1 主色板里能撑大面积的暖色只有两个——`掩尘 #C9AF83`、`裸土 #B39A78`。但这两色的语义是**棋盘格底**(未开格/已开格),营地场景直接拿来当地面和石头用,等于把棋盘色板铺满了占屏七成的面积。§4.1 那组硬指标(L≥0.30、饱和 ≤0.55)是**棋盘数字可读性**指标,搬到场景大面积上只剩弱约束,管不住色相。
**缺口二:「全作最暖」被泛化到了不该暖的地方。**
§6.2 写的是「承载 §2.8——全作最暖:烛光黄 `#F6C667` × 木色 `#8A5A34`」。这两个色的本体是**火焰与木构**,是"人留下的东西"。它被理解成了"整屏都要暖",于是天空、远山、地面全部跟着暖。
**缺口三:两条构图口径把仅有的冷色来源拿掉了。**
作者口径「近景不要草」删掉了暖调场景里**唯一的天然冷色平衡源(绿植)**;「不要远景地面/地平线」又删掉了山脚那片本可以做成冷灰绿的地带。剩下来的大面积只剩天、山、地——三者全暖,必然发黄。
### 1.3 与美术圣经核对:没有任何条款要求营地大面积是暖的
| 条款 | 原文要点 | 对本议题的含义 |
|---|---|---|
| §4.2 适用域 | **纯环境氛围(背景/装饰/远景/非语义物件)不受色相独占约束**,只受 §3.4 焦点预算与低饱和后退约束 | 远山、天空走冷色**完全合规**,不被任何语义色独占条款拦 |
| §6.1 战斗背景 | 背景整体**低对比自然后退**,禁止抢焦点 | 营地远景可沿用同款"后退"逻辑,冷灰正是后退手段 |
| §4.1 色板分层(2026-10-22 作者裁) | 场景环境子板:**每屏一套、≤32 色、允许跨屏不共享**;新增环境色须过 §4.2 语义检查 + 与既有任一色 RGB 三通道差 ≥8/255 | 营地可以**单独拥有一套环境色**,不必挤占棋盘主色板,也不占 48 token 额度 |
| §2 明度分层(2026-09-29 v3.0) | 空间纵深走明度分层允许域(光照氛围/色相饱和/形体承载),非情绪压暗 | 空气透视的"远景趋近天空明度、对比变弱"与既有明度分层同族,不冲突 |
**结论**:要改的不是美术圣经的暖色条款,而是**补一张营地环境子板**,并把"全作最暖"收回它本来的位置——暖的是物件,不是整屏。
---
## 2. 修正后的定位:营地应该长什么样
### 2.1 一句话氛围定位
> **白天、干爽、有风的开阔营地。暖的是「人留下的东西」(火、木头、皮革、石头),冷的是「野外的东西」(天、远山、地面)。玩家第一眼看到的是篝火和物件,天与山只是安静地退在后面。**
### 2.2 三个感觉词(写提示词与验收都照这三条判)
| 感觉词 | 落到画面上是什么 | 反例(出现即判红) |
|---|---|---|
| **干爽** | 降饱和、无湿气;地面是干实土不是泥泞 | 整屏油润、饱和过高、像黄昏金光 |
| **安稳** | 暖色集中在篝火与木构上,是"人味",不是整屏染色 | 天空、远山也发暖 → 变成"整屏黄疸" |
| **空旷** | 远景只有天与山,无地平线、无杂物、无树 | 远景长出树、屋、地平线 |
### 2.3 冷暖分工(核心规则,一句话记住)
**冷=大面积=后退;暖=小面积=前进。**
- 冷(后退层):天空、远山、地面 —— 冷灰白/冷灰蓝/中性灰褐,低饱和
- 暖(前进层):火焰、木构、石头、绳垫、毯子 —— 只在这一层出现暖色
- **唯一高饱和=篝火火焰**(§3.4 焦点预算:首焦唯一,其余全部让位)
### 2.4 空气透视三档(远近各层怎么拉开)
**空气透视**=空气隔在中间,越远的东西看起来越淡、越冷、对比越弱、细节越少;近处相反,更暗、更暖、对比更强、细节更多。
| 层 | 明度 | 色温 | 饱和 | 对比 | 细节 |
|---|---|---|---|---|---|
| **远**(天空/远山) | 最亮,趋近天空 | **冷** | 最低 | 最弱 | 只剩剪影,无单体树 |
| **中**(木围墙/门/布告板) | 中 | 中性偏冷(木色压灰) | 低 | 弱 | 简化,无个体木纹 |
| **近**(地面/七件) | **最暗** | **暖(仅此层)** | 中(火焰为唯一高饱和) | 最强 | 完整、清晰、可读 |
> 注:本表的明度关系是**营地环境子板内部**的纵深关系,与棋盘的世界底/中层档互不干扰(环境子板允许跨屏不共享)。
### 2.5 面积占比(把"别太黄"变成可机检的数)
| 色温区 | 建议占比 | 判据 |
|---|---|---|
| 冷色面积(天+山+地) | **≥ 55%** | 低于则整屏偏暖 |
| 暖色面积(物件与木构) | **≤ 30%** | 高于则"黄疸" |
| 火焰高饱和 | **≤ 3%** | 高于则抢焦(违反 §3.4 焦点预算) |
配套黄度阈值(出图后实量):**天空黄度 ≤20、地面黄度 ≤35**。
### 2.6 色值候选方向(待作者定,§4.1 是数值单点事实源)
只给方向不给死值——定色值须由作者拍板,且新色要过两道门(§4.2 语义检查 + 与既有任一色 RGB 三通道差 ≥8/255):
| 大面积 | 方向 |
|---|---|
| 天空 | 冷灰白、微带蓝灰,全画明度最高 |
| 远山 | 冷灰蓝,越远越淡越冷(空气透视),趋近天空明度 |
| 地面 | 中性灰褐,降饱和,全画明度最低,**绝不带金黄/琥珀** |
| 木构(中景围墙) | 木色 `#8A5A34` 压一档饱和走中性偏冷——它是中景,必须后退 |
| 物件(近景) | 木色 `#8A5A34`、掩尘 `#C9AF83` 保持本色;火焰烛光黄 `#F6C667` 为唯一高饱和 |
---
## 3. 与美术圣经的关系:哪些不动、哪些要新建
| 项 | 处理 | 理由 |
|---|---|---|
| §6.2「全作最暖:烛光黄 × 木色」 | **不动**,但收回适用范围=**物件层**,不含天空/远山/地面 | 两色本体是火与木,泛化到整屏是把"物件色"当成了"环境色" |
| §4.1 主色板(掩尘/裸土等) | **不动**;营地地面**停止挪用** `裸土 #B39A78` | 那是棋盘格底色,语义不同域 |
| §4.2 适用域条款 | **不动**,本脚本正是依据它给环境层定冷色 | 环境层本就不受色相独占约束 |
| 营地环境子板 | **新建**(本脚本 §2.6 为候选) | §4.1 色板分层条款已开通道:每屏一套、≤32 色、不占 48 额度 |
| §4.1 硬指标(L≥0.30、饱和 ≤0.55) | **不动**,仅说明它是棋盘可读性指标、不搬到场景大面积 | 避免把格底指标误当场景指标 |
---
## 4. 外部调研依据
### 4.1 三条通行原则(多来源一致)
1. **空气透视**:越远越亮(趋近天空明度)、越冷(偏蓝)、对比越弱、细节越少;近处相反——暗、暖、高对比、有细节。(来源:Fiveable《Atmospheric perspective》、ArtExamples《Atmospheric Perspective》、科普中国「空气透视」条目——后者原文:"近处色彩偏暖,远处色彩偏冷""最远处冷暖不分而形成为一片蓝灰色")
2. **冷暖分工**:暖色前进、冷色后退。像素画环境里"背景多走冷、前景多走暖,以此拉开纵深"。(来源:PixelPaletteSwap《Color Theory for Pixel Art》;Peace on Paper《Warm vs Cool Tricks》技巧一"暖前景、冷背景")
3. **饱和即焦点**:高饱和只留给焦点,大面积必须降饱和。(来源:Pixel-Editor《Color Theory for Pixel Art》"Keep saturated colors sparse")
### 4.2 同类游戏参照
**暖而不脏**的 cozy 像素阵营:Stardew Valley、Moonlighter、Eastward、Forager、Littlewood、Spirittea、Potion Permit(来源:The Cozy Gaming Nook 两次 cozy 像素专题)。
**其中 Stardew Valley 最值得对照**:它同样是"暖乡土"调子,靠**大面积绿色植被**做冷色平衡(来源:ImageToPixel 的分析——Stardew 走"golden hour"暖调、无抖动、细描边,但画面里绿植占比极大)。
**对本项目的直接推论**:营地按作者口径**禁草**、**禁远景地面**,等于把 Stardew 赖以平衡的绿植整块拿掉。所以冷色平衡只能由**天空与远山**承担——这两块必须明确走冷,否则整屏无解。
---
## 5. 待裁(由作者定,不擅自动)
1. **营地环境子板色值**(§2.6 仅给方向)——定色值属 `art-bible` §4.1 的活,且新色须过 §4.2 语义检查 + RGB 三通道差 ≥8/255 两道门。
2. **面积占比与黄度阈值**(§2.5)——是否作为营地场景图的机检判据写进验收表。
3. **木围墙的色温**——中景走"中性偏冷"要比物件木色压一档饱和,是否与 §6.2「木色 `#8A5A34`」的物件用法冲突(我的判断:不冲突,压饱和不是换色,但需作者确认)。
@@ -0,0 +1,45 @@
# 普查:效果通道与事件词零现役(2026-11-06)
> **接收方**:待办清单「效果通道与事件词的零现役普查」条——本报告是该条的产出合同。
> **结论**:需要你处理的事只有 **2 件**(第一节);扫描面内其余全部属预留埋点,**全部保留、零处理项**(第二节只留计数供存档)。词表零改动、内容层零孤儿、零代码改动。
## 一、需要你裁断的两件事
### 1. 注释叙述订正 5 处——做不做
代码注释把三件**未设计入表**的专长(掘进蓄能/深挖钻头/爆破工兵包)写成了在役叙述,后来读代码或写卡的人会被误导(迭代 53 已发生过一次:spike 承接句按注释误判「零新代码」)。订正方式=把注释改成「预留、未设计入表」,不改任何逻辑。
| 位置 | 现行叙述 | 事实 |
|---|---|---|
| effect_engine.gd OPS 头注 | mod_zero「掘进蓄能」、gte_ref/lt_ref「深挖钻头配额」 | 两专长零在表 |
| effect_engine.gd EFFECT_KINDS 头注 | spike 独立 kind 在词表 | 实走 modify_ticket 的 mode 通道 |
| reward_grant.gd:130 | 「counter_spend 订阅=掘进蓄能积点账本」 | 掘进蓄能零在表 |
| action_economy.gd:82 | 「深挖钻头付费开掘」 | 深挖钻头零在表 |
| treasure_item_table.gd:20 | 「cell_removal = 爆破工兵包中立移除格」 | 爆破工兵包零在表 |
### 2. 秘宝表旧词「迁移完成」收尾——做不做、何时做
秘宝表 schema 词表里留着 9 个旧目录时代的词面(git 查证:迭代 22 秘宝批把旧目录 12 行全量换代成现役 12 件后,旧词面按 schema 自己注的「旧词保留共存至迁移完成」留下)。按关联效果列名:
| 旧词承载的效果 | 类型 |
|---|---|
| 雷区揭示/奖励预览/行动点授予(3 个旧词形) | 新旧词共存——现役等价能力已由新词形承载 |
| 踩雷伤害减半/受击反弹伤害/开局前几轮行动点加成/爆破工兵包中立移除格/奖励摇号权重偏置/换图刷新加算区(6 个) | 旧内容已裁、通道位预留 |
裁断点:这批「迁移完成」收尾要不要做、什么时候做。**词面现在不动**;收尾属于单独一批改动,做的时候再设计。
## 二、无需处理项汇总(零裁决点,计数存档)
扫描面内以下词面全部判定**保留**(预留埋点——引擎架构=词表先行、内容后接,加一件已有效果类型的专长/秘宝只写数据行零代码):
- **事件词预留 4**:armor_roll(事件岗)/reward_roll/treasure_used/seal_settle——发射点全在役运行,等订阅内容;armor_roll 另有掷点流名岗在役,同词两用。
- **效果类型词预留 10**:spawn_spell(锻造工坊法杖设计在册待接线);bias_rewards/counter_spend(架构在役机制承载位——偏运合计查询口/受理闸模式);add_ticket_rider/yield_mult/apply_status/bias_wallet/refresh_pick/preview/spike 词面(预留位)。
- **事实词预留 6**:is_first_dig_of_battle/terrain_kind(载荷特判支撑在)/target_armor_class/seals_count/nest_count/battle_index。
- **算子词预留 7**:neq/gt/ends_with/has/mod_zero/gte_ref/lt_ref(实现函数全在)。
- **channel 反弹行族**:open_ticket 反弹域零数据行(预留)。
其余全部事件词/效果词/事实词/算子词的数据面订阅行在役(feats/treasure 数据行+天赋树规则合成行),零孤儿。
## 三、普查方法(存档)
四张词表封闭在 effect_engine.gd 常量:事件 21/效果类型 31/事实 18/算子 13;邻接面=秘宝表 schema 旧词层 10 词+channel 域。数据面使用按行结构键(`"on"`/`"kind"`/`"fact"`/`"op"`/`"channel"`)统计自 `game/data/tables/*.tres` 全域+天赋树规则合成行+秘宝表主动件域;注释裸词面不计。skills.tres(敌技)的 when/fact 走独立系统,不入本普查面。
@@ -0,0 +1,203 @@
# 读数:三关色相 × 五组微光互辨抽测(2026-11-05)
> 接收方:待办清单「三关色相 × 五组微光互辨抽测——三关图类型色板终值冻结的前置抽测」条。
> 执行依据:reports/提案-三关色相互辨抽测方案-2026-10-29.md(S1/S2 步)。
> 样图:artwork/mockups/glow-discrimination-sheet-2026-11-05.png(不入 git)。
## 机检汇总
| 项 | 判定 | 说明 |
|---|---|---|
| D-dungeon | PASS | 铺底代表值门(底型瓦片实测顶面受光档;亮度/饱和/禁入带,近色只报读数):RGB=(180, 144, 105) L=0.307 饱和=0.417 色相=31.2° 近色对=13(在役素材色,非新增) |
| D-underdark | PASS | 铺底代表值门(底型瓦片实测顶面受光档;亮度/饱和/禁入带,近色只报读数):RGB=(186, 193, 210) L=0.532 饱和=0.114 色相=222.5° 近色对=4(在役素材色,非新增) |
| D-deep_underdark | PASS | 铺底代表值门(底型瓦片实测顶面受光档;亮度/饱和/禁入带,近色只报读数):RGB=(150, 158, 186) L=0.345 饱和=0.194 色相=226.7° 近色对=9(在役素材色,非新增) |
| A-stair-dungeon-素砖 | PASS | 明度阶梯单调递减:165.242→145.992→130.196→114.266→92.362 |
| A-stair-dungeon-裂纹 | PASS | 明度阶梯单调递减:165.242→145.992→130.196→114.266→92.362 |
| A-stair-dungeon-碎石斑 | PASS | 明度阶梯单调递减:164.957→144.992→129.269→113.266→91.434 |
| A-stair-underdark-素砖 | PASS | 明度阶梯单调递减:161.080→141.829→125.179→109.389→87.485 |
| A-stair-underdark-裂纹 | PASS | 明度阶梯单调递减:164.152→144.902→129.106→112.461→90.557 |
| A-stair-underdark-碎石斑 | PASS | 明度阶梯单调递减:158.152→138.829→123.106→106.461→84.557 |
| A-stair-deep_underdark-素砖 | PASS | 明度阶梯单调递减:145.437→126.186→109.748→93.746→71.914 |
| A-stair-deep_underdark-裂纹 | PASS | 明度阶梯单调递减:147.509→128.259→112.536→95.818→73.986 |
| A-stair-deep_underdark-碎石斑 | PASS | 明度阶梯单调递减:142.509→123.186→106.748→90.746→68.914 |
| A-CR | PASS | 组间合成对比最弱对 ≥1.20(90 对):最弱 underdark/HEAL/PIERCE CR=1.224 |
| B | PASS | 格底-微光可辨底线(45 行三通道差不全<8):违例 0 行:无 |
| C | PASS | 绘制参数校准(45 行模型差≤±2/255):违例 0 行:无 |
| GRAY | PASS | 灰度副本段零彩色像素:命中 0 |
| EDGE | PASS | 四缘 10px 带内零内容像素:命中 0 |
## S1 机检 D:铺底代表值(底型瓦片实测顶面受光档)五门
| 关 | RGB | L(线性域) | 饱和 | 色相 | 近色对 | 判定 |
|---|---|---|---|---|---|---|
| dungeon | (180, 144, 105) | 0.307 | 0.417 | 31.2° | 13 | 过 |
| underdark | (186, 193, 210) | 0.532 | 0.114 | 222.5° | 4 | 过 |
| deep_underdark | (150, 158, 186) | 0.345 | 0.194 | 226.7° | 9 | 过 |
## 表一 逐对表(45 行)
| 关 | 底型 | 微光组 | 带内合成均值 | 格心素底均值 | 模型带内均值 | 模型差 | 三通道差 | 色相差° | 判定 |
|---|---|---|---|---|---|---|---|---|---|
| dungeon | 素砖 | BUFF | (193, 166, 76) | (145, 116, 84) | (193, 166, 76) | 0 | [48, 50, 8] | 21 | 过 |
| dungeon | 素砖 | HEAL | (126, 159, 76) | (145, 116, 84) | (126, 159, 75) | 1 | [19, 43, 8] | 73 | 过 |
| dungeon | 素砖 | PIERCE | (100, 140, 122) | (145, 116, 84) | (100, 140, 122) | 0 | [45, 24, 38] | 149 | 过 |
| dungeon | 素砖 | MAGIC | (135, 105, 145) | (145, 116, 84) | (135, 105, 145) | 0 | [10, 11, 61] | 136 | 过 |
| dungeon | 素砖 | AOE | (155, 76, 70) | (145, 116, 84) | (155, 76, 70) | 0 | [10, 40, 14] | 43 | 过 |
| dungeon | 裂纹 | BUFF | (193, 166, 76) | (143, 115, 83) | (193, 166, 76) | 0 | [50, 51, 7] | 21 | 过 |
| dungeon | 裂纹 | HEAL | (126, 159, 76) | (143, 115, 83) | (126, 159, 76) | 0 | [17, 44, 7] | 73 | 过 |
| dungeon | 裂纹 | PIERCE | (100, 140, 122) | (143, 115, 83) | (100, 140, 122) | 0 | [43, 25, 39] | 149 | 过 |
| dungeon | 裂纹 | MAGIC | (135, 105, 145) | (143, 115, 83) | (135, 105, 145) | 0 | [8, 10, 62] | 136 | 过 |
| dungeon | 裂纹 | AOE | (155, 76, 70) | (143, 115, 83) | (155, 76, 70) | 0 | [12, 39, 13] | 43 | 过 |
| dungeon | 碎石斑 | BUFF | (192, 166, 75) | (147, 118, 85) | (192, 166, 75) | 0 | [45, 48, 10] | 21 | 过 |
| dungeon | 碎石斑 | HEAL | (125, 158, 75) | (147, 118, 85) | (125, 159, 75) | 1 | [22, 40, 10] | 73 | 过 |
| dungeon | 碎石斑 | PIERCE | (99, 139, 122) | (147, 118, 85) | (99, 139, 122) | 0 | [48, 21, 37] | 149 | 过 |
| dungeon | 碎石斑 | MAGIC | (134, 104, 144) | (147, 118, 85) | (134, 104, 144) | 0 | [13, 14, 59] | 136 | 过 |
| dungeon | 碎石斑 | AOE | (154, 75, 70) | (147, 118, 85) | (154, 75, 70) | 0 | [7, 43, 15] | 43 | 过 |
| underdark | 素砖 | BUFF | (172, 164, 100) | (104, 110, 128) | (172, 164, 100) | 0 | [68, 54, 28] | 170 | 过 |
| underdark | 素砖 | HEAL | (105, 157, 100) | (104, 110, 128) | (105, 157, 100) | 0 | [1, 47, 28] | 119 | 过 |
| underdark | 素砖 | PIERCE | (78, 137, 147) | (104, 110, 128) | (78, 137, 147) | 0 | [26, 27, 19] | 42 | 过 |
| underdark | 素砖 | MAGIC | (114, 102, 169) | (104, 110, 128) | (114, 102, 169) | 0 | [10, 8, 41] | 33 | 过 |
| underdark | 素砖 | AOE | (134, 73, 94) | (104, 110, 128) | (134, 73, 95) | 1 | [30, 37, 34] | 126 | 过 |
| underdark | 裂纹 | BUFF | (175, 167, 104) | (113, 120, 137) | (175, 167, 104) | 0 | [62, 47, 33] | 170 | 过 |
| underdark | 裂纹 | HEAL | (108, 160, 104) | (113, 120, 137) | (108, 160, 104) | 0 | [5, 40, 33] | 119 | 过 |
| underdark | 裂纹 | PIERCE | (82, 141, 150) | (113, 120, 137) | (82, 141, 150) | 0 | [31, 21, 13] | 42 | 过 |
| underdark | 裂纹 | MAGIC | (117, 105, 173) | (113, 120, 137) | (117, 105, 173) | 0 | [4, 15, 36] | 33 | 过 |
| underdark | 裂纹 | AOE | (137, 76, 98) | (113, 120, 137) | (137, 76, 98) | 0 | [24, 44, 39] | 126 | 过 |
| underdark | 碎石斑 | BUFF | (169, 161, 98) | (124, 131, 149) | (169, 161, 98) | 0 | [45, 30, 51] | 170 | 过 |
| underdark | 碎石斑 | HEAL | (102, 154, 97) | (124, 131, 149) | (102, 154, 97) | 0 | [22, 23, 52] | 119 | 过 |
| underdark | 碎石斑 | PIERCE | (76, 135, 144) | (124, 131, 149) | (76, 135, 144) | 0 | [48, 4, 5] | 42 | 过 |
| underdark | 碎石斑 | MAGIC | (111, 99, 167) | (124, 131, 149) | (111, 99, 167) | 0 | [13, 32, 18] | 33 | 过 |
| underdark | 碎石斑 | AOE | (131, 70, 92) | (124, 131, 149) | (131, 70, 92) | 0 | [7, 61, 57] | 126 | 过 |
| deep_underdark | 素砖 | BUFF | (157, 148, 86) | (76, 80, 102) | (157, 148, 86) | 0 | [81, 68, 16] | 175 | 过 |
| deep_underdark | 素砖 | HEAL | (90, 141, 86) | (76, 80, 102) | (90, 141, 86) | 0 | [14, 61, 16] | 123 | 过 |
| deep_underdark | 素砖 | PIERCE | (64, 121, 133) | (76, 80, 102) | (64, 121, 133) | 0 | [12, 41, 31] | 47 | 过 |
| deep_underdark | 素砖 | MAGIC | (99, 86, 155) | (76, 80, 102) | (99, 86, 155) | 0 | [23, 6, 53] | 29 | 过 |
| deep_underdark | 素砖 | AOE | (119, 57, 81) | (76, 80, 102) | (119, 57, 81) | 0 | [43, 23, 21] | 122 | 过 |
| deep_underdark | 裂纹 | BUFF | (159, 150, 89) | (84, 89, 111) | (159, 150, 89) | 0 | [75, 61, 22] | 175 | 过 |
| deep_underdark | 裂纹 | HEAL | (92, 143, 89) | (84, 89, 111) | (92, 143, 89) | 0 | [8, 54, 22] | 123 | 过 |
| deep_underdark | 裂纹 | PIERCE | (66, 124, 136) | (84, 89, 111) | (66, 124, 136) | 0 | [18, 35, 25] | 47 | 过 |
| deep_underdark | 裂纹 | MAGIC | (101, 88, 158) | (84, 89, 111) | (101, 88, 158) | 0 | [17, 1, 47] | 29 | 过 |
| deep_underdark | 裂纹 | AOE | (121, 59, 84) | (84, 89, 111) | (121, 59, 84) | 0 | [37, 30, 27] | 122 | 过 |
| deep_underdark | 碎石斑 | BUFF | (154, 145, 84) | (91, 97, 121) | (154, 145, 83) | 1 | [63, 48, 37] | 175 | 过 |
| deep_underdark | 碎石斑 | HEAL | (87, 138, 83) | (91, 97, 121) | (87, 138, 83) | 0 | [4, 41, 38] | 123 | 过 |
| deep_underdark | 碎石斑 | PIERCE | (61, 118, 130) | (91, 97, 121) | (61, 118, 130) | 0 | [30, 21, 9] | 47 | 过 |
| deep_underdark | 碎石斑 | MAGIC | (96, 83, 152) | (91, 97, 121) | (96, 83, 152) | 0 | [5, 14, 31] | 29 | 过 |
| deep_underdark | 碎石斑 | AOE | (116, 54, 78) | (91, 97, 121) | (116, 54, 78) | 0 | [25, 43, 43] | 122 | 过 |
## 表二 组间表(90 行)
| 关 | 底型 | 组对 | 合成对比 | 灰度亮度a | 灰度亮度b | 判定 |
|---|---|---|---|---|---|---|
| dungeon | 素砖 | BUFF/HEAL | 1.270 | 165.242 | 145.992 | 过 |
| dungeon | 素砖 | BUFF/PIERCE | 1.583 | 165.242 | 130.196 | 过 |
| dungeon | 素砖 | BUFF/MAGIC | 1.979 | 165.242 | 114.266 | 过 |
| dungeon | 素砖 | BUFF/AOE | 2.510 | 165.242 | 92.362 | 过 |
| dungeon | 素砖 | HEAL/PIERCE | 1.247 | 145.992 | 130.196 | 过 |
| dungeon | 素砖 | HEAL/MAGIC | 1.559 | 145.992 | 114.266 | 过 |
| dungeon | 素砖 | HEAL/AOE | 1.977 | 145.992 | 92.362 | 过 |
| dungeon | 素砖 | PIERCE/MAGIC | 1.250 | 130.196 | 114.266 | 过 |
| dungeon | 素砖 | PIERCE/AOE | 1.585 | 130.196 | 92.362 | 过 |
| dungeon | 素砖 | MAGIC/AOE | 1.268 | 114.266 | 92.362 | 过 |
| dungeon | 裂纹 | BUFF/HEAL | 1.270 | 165.242 | 145.992 | 过 |
| dungeon | 裂纹 | BUFF/PIERCE | 1.583 | 165.242 | 130.196 | 过 |
| dungeon | 裂纹 | BUFF/MAGIC | 1.979 | 165.242 | 114.266 | 过 |
| dungeon | 裂纹 | BUFF/AOE | 2.510 | 165.242 | 92.362 | 过 |
| dungeon | 裂纹 | HEAL/PIERCE | 1.247 | 145.992 | 130.196 | 过 |
| dungeon | 裂纹 | HEAL/MAGIC | 1.559 | 145.992 | 114.266 | 过 |
| dungeon | 裂纹 | HEAL/AOE | 1.977 | 145.992 | 92.362 | 过 |
| dungeon | 裂纹 | PIERCE/MAGIC | 1.250 | 130.196 | 114.266 | 过 |
| dungeon | 裂纹 | PIERCE/AOE | 1.585 | 130.196 | 92.362 | 过 |
| dungeon | 裂纹 | MAGIC/AOE | 1.268 | 114.266 | 92.362 | 过 |
| dungeon | 碎石斑 | BUFF/HEAL | 1.282 | 164.957 | 144.992 | 过 |
| dungeon | 碎石斑 | BUFF/PIERCE | 1.598 | 164.957 | 129.269 | 过 |
| dungeon | 碎石斑 | BUFF/MAGIC | 2.001 | 164.957 | 113.266 | 过 |
| dungeon | 碎石斑 | BUFF/AOE | 2.536 | 164.957 | 91.434 | 过 |
| dungeon | 碎石斑 | HEAL/PIERCE | 1.247 | 144.992 | 129.269 | 过 |
| dungeon | 碎石斑 | HEAL/MAGIC | 1.561 | 144.992 | 113.266 | 过 |
| dungeon | 碎石斑 | HEAL/AOE | 1.979 | 144.992 | 91.434 | 过 |
| dungeon | 碎石斑 | PIERCE/MAGIC | 1.252 | 129.269 | 113.266 | 过 |
| dungeon | 碎石斑 | PIERCE/AOE | 1.587 | 129.269 | 91.434 | 过 |
| dungeon | 碎石斑 | MAGIC/AOE | 1.267 | 113.266 | 91.434 | 过 |
| underdark | 素砖 | BUFF/HEAL | 1.248 | 161.080 | 141.829 | 过 |
| underdark | 素砖 | BUFF/PIERCE | 1.549 | 161.080 | 125.179 | 过 |
| underdark | 素砖 | BUFF/MAGIC | 1.969 | 161.080 | 109.389 | 过 |
| underdark | 素砖 | BUFF/AOE | 2.637 | 161.080 | 87.485 | 过 |
| underdark | 素砖 | HEAL/PIERCE | 1.241 | 141.829 | 125.179 | 过 |
| underdark | 素砖 | HEAL/MAGIC | 1.577 | 141.829 | 109.389 | 过 |
| underdark | 素砖 | HEAL/AOE | 2.112 | 141.829 | 87.485 | 过 |
| underdark | 素砖 | PIERCE/MAGIC | 1.271 | 125.179 | 109.389 | 过 |
| underdark | 素砖 | PIERCE/AOE | 1.702 | 125.179 | 87.485 | 过 |
| underdark | 素砖 | MAGIC/AOE | 1.339 | 109.389 | 87.485 | 过 |
| underdark | 裂纹 | BUFF/HEAL | 1.245 | 164.152 | 144.902 | 过 |
| underdark | 裂纹 | BUFF/PIERCE | 1.524 | 164.152 | 129.106 | 过 |
| underdark | 裂纹 | BUFF/MAGIC | 1.952 | 164.152 | 112.461 | 过 |
| underdark | 裂纹 | BUFF/AOE | 2.612 | 164.152 | 90.557 | 过 |
| underdark | 裂纹 | HEAL/PIERCE | 1.224 | 144.902 | 129.106 | 过 |
| underdark | 裂纹 | HEAL/MAGIC | 1.567 | 144.902 | 112.461 | 过 |
| underdark | 裂纹 | HEAL/AOE | 2.097 | 144.902 | 90.557 | 过 |
| underdark | 裂纹 | PIERCE/MAGIC | 1.280 | 129.106 | 112.461 | 过 |
| underdark | 裂纹 | PIERCE/AOE | 1.713 | 129.106 | 90.557 | 过 |
| underdark | 裂纹 | MAGIC/AOE | 1.338 | 112.461 | 90.557 | 过 |
| underdark | 碎石斑 | BUFF/HEAL | 1.252 | 158.152 | 138.829 | 过 |
| underdark | 碎石斑 | BUFF/PIERCE | 1.538 | 158.152 | 123.106 | 过 |
| underdark | 碎石斑 | BUFF/MAGIC | 1.981 | 158.152 | 106.461 | 过 |
| underdark | 碎石斑 | BUFF/AOE | 2.658 | 158.152 | 84.557 | 过 |
| underdark | 碎石斑 | HEAL/PIERCE | 1.229 | 138.829 | 123.106 | 过 |
| underdark | 碎石斑 | HEAL/MAGIC | 1.583 | 138.829 | 106.461 | 过 |
| underdark | 碎石斑 | HEAL/AOE | 2.123 | 138.829 | 84.557 | 过 |
| underdark | 碎石斑 | PIERCE/MAGIC | 1.288 | 123.106 | 106.461 | 过 |
| underdark | 碎石斑 | PIERCE/AOE | 1.728 | 123.106 | 84.557 | 过 |
| underdark | 碎石斑 | MAGIC/AOE | 1.341 | 106.461 | 84.557 | 过 |
| deep_underdark | 素砖 | BUFF/HEAL | 1.265 | 145.437 | 126.186 | 过 |
| deep_underdark | 素砖 | BUFF/PIERCE | 1.585 | 145.437 | 109.748 | 过 |
| deep_underdark | 素砖 | BUFF/MAGIC | 2.040 | 145.437 | 93.746 | 过 |
| deep_underdark | 素砖 | BUFF/AOE | 2.734 | 145.437 | 71.914 | 过 |
| deep_underdark | 素砖 | HEAL/PIERCE | 1.253 | 126.186 | 109.748 | 过 |
| deep_underdark | 素砖 | HEAL/MAGIC | 1.613 | 126.186 | 93.746 | 过 |
| deep_underdark | 素砖 | HEAL/AOE | 2.162 | 126.186 | 71.914 | 过 |
| deep_underdark | 素砖 | PIERCE/MAGIC | 1.287 | 109.748 | 93.746 | 过 |
| deep_underdark | 素砖 | PIERCE/AOE | 1.725 | 109.748 | 71.914 | 过 |
| deep_underdark | 素砖 | MAGIC/AOE | 1.340 | 93.746 | 71.914 | 过 |
| deep_underdark | 裂纹 | BUFF/HEAL | 1.263 | 147.509 | 128.259 | 过 |
| deep_underdark | 裂纹 | BUFF/PIERCE | 1.562 | 147.509 | 112.536 | 过 |
| deep_underdark | 裂纹 | BUFF/MAGIC | 2.028 | 147.509 | 95.818 | 过 |
| deep_underdark | 裂纹 | BUFF/AOE | 2.720 | 147.509 | 73.986 | 过 |
| deep_underdark | 裂纹 | HEAL/PIERCE | 1.237 | 128.259 | 112.536 | 过 |
| deep_underdark | 裂纹 | HEAL/MAGIC | 1.606 | 128.259 | 95.818 | 过 |
| deep_underdark | 裂纹 | HEAL/AOE | 2.154 | 128.259 | 73.986 | 过 |
| deep_underdark | 裂纹 | PIERCE/MAGIC | 1.298 | 112.536 | 95.818 | 过 |
| deep_underdark | 裂纹 | PIERCE/AOE | 1.742 | 112.536 | 73.986 | 过 |
| deep_underdark | 裂纹 | MAGIC/AOE | 1.341 | 95.818 | 73.986 | 过 |
| deep_underdark | 碎石斑 | BUFF/HEAL | 1.268 | 142.509 | 123.186 | 过 |
| deep_underdark | 碎石斑 | BUFF/PIERCE | 1.593 | 142.509 | 106.748 | 过 |
| deep_underdark | 碎石斑 | BUFF/MAGIC | 2.055 | 142.509 | 90.746 | 过 |
| deep_underdark | 碎石斑 | BUFF/AOE | 2.753 | 142.509 | 68.914 | 过 |
| deep_underdark | 碎石斑 | HEAL/PIERCE | 1.256 | 123.186 | 106.748 | 过 |
| deep_underdark | 碎石斑 | HEAL/MAGIC | 1.620 | 123.186 | 90.746 | 过 |
| deep_underdark | 碎石斑 | HEAL/AOE | 2.170 | 123.186 | 68.914 | 过 |
| deep_underdark | 碎石斑 | PIERCE/MAGIC | 1.290 | 106.748 | 90.746 | 过 |
| deep_underdark | 碎石斑 | PIERCE/AOE | 1.728 | 106.748 | 68.914 | 过 |
| deep_underdark | 碎石斑 | MAGIC/AOE | 1.339 | 90.746 | 68.914 | 过 |
---
机检 17 项,FAIL 0 项。
目检(S3)归用户,判定见下节。
## S3 目检判定(用户在会话中判定,2026-11-06)
| 条 | 判据(方案 §2.2) | 判定 |
|---|---|---|
| 1 | 正图段 45 格逐格指认微光属哪一组(五选一),45/45 | 过 |
| 2 | 灰度副本段(Rec.709 去色=色弱通道)重做第 1 条,45/45 | 过 |
| 3 | 阶梯序相邻 36 对逐对判可分辨,全对可分 | 过 |
| 4 | 裂纹/碎石斑两种装饰变体底上 30 格:微光仍可指认且装饰不成第二焦点 | 过 |
判定口径=用户回话「没有问题,我觉得是可以分清楚的」,经追问四条覆盖范围后答「四条全过」。
## S4 抽测结论
抽测通过:机检 17 项 FAIL 0 + 目检四条全过。
同批生效的口径订正:铺底代表值改为从三块底型瓦片像素实测(线性域相对亮度取大),不再引用换色脚本的锚槽表值;dungeon 实测 `#B49069`(L=0.307,过门余量 0.007)。样图逐字节未变(sha256 `15d33fabb752566772540f436ca992fc71b0074fd5da71ff398a9b6bbb789087`,与 2026-11-05 出图件一致),故 S3 目检结论继续有效。
@@ -1,140 +0,0 @@
# 需求卡片:卷轴视口批(关卡画面选路屏视口化)
- 来源:[待办清单](../待办清单.md)「超预算图策略升级路径——等比缩放与滚动视口两案待裁」条(状态=文档设计已闭合+待写卡,2026-11-05 用户裁滚动视口案落地)。
- **唯一事实源**:[level-map.md v2.0](../design/gdd/ux/level-map.md) §布局决策 5(行为四条表 :101–104+内嵌视口效果图 :108–153+像素级视觉判据 :154)+[关卡节点图生成.md v1.5](../design/gdd/systems/冒险循环/关卡节点图生成.md) §参数契约(图规模无呈现侧上限)。设计讨论四口子与效果图人审均 2026-11-05 用户裁决在案。
- **验收档:中**(呈现交互面——玩家可见但不损持久状态;无存档写读、无结算公式、无 RNG 触碰;批卡 U0 时确认或改判)。
## 1. 目标与完成定义
关卡画面(选路屏)落卷轴视口范式:画布随图生长可超屏,屏为窗口——①视口锚点=当前节点居中;②停点切换与兑付回图平滑滚动跟随(减动效瞬切);③拖拽+滚轮手动平移(clamp 画布包络、拖拽不触发选路);④屏缘渐隐带+平移提示 chip 指引。
完成定义:验证闸对照表 R1–R9 全部有绿读数;场景探针家族扩展件当轮真实跑批通过;代码域闸全绿;用户实机目检「深度 13 图拖拽可达首末列、推进跟随方向感」登记不阻塞。
## 2. 现状事实(开工自行复核)
- [level_map_screen.gd:26-32](../game/presentation/views/level_map_screen.gd) 冻结公式常量:`LAYER_BASE_X=70`/`LAYER_STEP_X=122`/`SLOT_BASE_Y=52`/`SLOT_STEP_Y=58`/`NODE_ICON_PX=20.0`/`NODE_HALF(36.5,25)`(52×36 的 1.4× 绘制)/`HIT_PAD=6.0`。
- [level_map_screen.gd:66-69](../game/presentation/views/level_map_screen.gd) `_content_size(620,240)`、`_layout_cache`——`open_choices` 按图拟合画布尺寸。
- [level_map_screen.gd:77-82](../game/presentation/views/level_map_screen.gd) 画布由 `CenterContainer`(`PRESET_FULL_RECT`)承载:整幅居中、无裁切无平移——超屏图(L3 层深 13,画布宽 1700.5px)两侧各 ~210px 出屏。
- [level_gen_params.tres:41](../game/data/tables/level_gen_params.tres) L3 `layers=13`——深度 13 是现行定值,非极端情形。
- [level_gen_params.gd:3-4、:69](../game/data/schemas/level_gen_params.gd) 头注「域校验沿关卡节点图生成.md v1.1 调参表已锁条款」「域并单点(关卡节点图生成.md v1.4 调参表)」——owner 文档已 v1.5 换代参数契约节,指针失效待反转(本卡 T4 随动)。
- [ui_kit.gd:231-232](../game/presentation/views/ui_kit.gd) `UiKit.motion(dur)` 呈现层 tween 时长单点包装(减动效面)。
- [ui_struct_probe.gd:204](../game/probe/ui_struct_probe.gd) `_audit("level_map", ...)` 既有结构审计锚;[golden/README.md:8](../game/tests/golden/README.md) level_map 全屏 golden=历史留根、现行闸件未对拍(本批不触渲染闸对拍)。
- 全文件 662 行——超 code-conventions §1.1 的 ~400 行审视线,视口承载拆件有据。
## 3. 设计契约(违反=L3 停机呈报)
- 🔴 **冻结值零改动**:节点图标 20px 烘焙 1:1、通道折线 2/3/4px、画布公式(宽=70+层深×122+44.5、高=52+槽位×58+25+24、下限 620×256)、节点方块 52×36×1.4 绘制、编号 10px——视口批只加行为,不动任何像素冻结件(level-map.md:69、:154)。
- 🔴 **视口纯呈现态**:平移位置零核心写、不进存档(重进本屏按锚点规则重新定位);Core 层零触碰、RNG 零触碰(level-map.md:103 行、:107 视口为纯呈现态句)。
- 🔴 **选路唯一通道**:图上点击提交语义零改动;拖拽超阈值不得触发选路提交(level-map.md:103)。
- 🔴 **零自动推进**:视口跟随只随停点切换/兑付回图发生,不构成任何自动前进路径(level-map.md:141 零自动推进纪律不变)。
- 🔴 **拆分边界纪律**(code-conventions §1.1,2026-10-28 用户裁):每个拆出单元有可陈述职责语句与单一变化原因;拆出单元有集体身份;「过阈值」只作发现信号;拆不出内聚单元=停卡走豁免通道,禁硬拆凑绿。
- 🔴 **禁为机检文本保留无效成员**(code-conventions §1.1⑤):机检键与卡片契约互斥=停卡呈报,禁自造第三条路。
## 4. 范围边界
做:视口承载件、锚点跟随、平滑滚动、减动效瞬切、拖拽/滚轮平移、clamp、渐隐带与提示 chip、探针断言、schema 头注指针反转。
不做:缩放与缩略图(等比缩放案已否决)、其余视口暗示形态(边缘回弹等——布局决策 5 注后置)、顶栏/底栏/放弃按钮/覆盖层改动、生成器与参数表改动、键盘/手柄输入(排 Beta)。
## 5. 关联文档同步面
| 文档 | 关联点 | 同步义务 |
|---|---|---|
| [level-map.md](../design/gdd/ux/level-map.md) v2.0 | 布局决策 5 行为实现与 AC :260–271——实现若发现 spec 缺口 | 设计权威——实现批禁写,缺口逐条列反哺点清单由文档批落 |
| [game/tests/README.md](../game/tests/README.md) 能力清单 | 本卡新增能力行三行(写卡段已同批落表,见 §8 能力行列) | 机械面——实现批禁写;批末收尾核对表上名与落地一致(改名则回表) |
| [level_gen_params.gd](../game/data/schemas/level_gen_params.gd) | 头注 :3-4/:69 指针失效(owner 已 v1.5 参数契约) | 本卡 T4 随批反转指针(非行为改动——`game/` 内实现批可写面) |
| [关卡节点图生成.md](../design/gdd/systems/冒险循环/关卡节点图生成.md) v1.5 | 无——本批实现不改变其任何陈述 | 无同步义务 |
## 6. 文件结构图(行为改动→对照表行互指)
| 文件 | 职责 | 改动性质 |
|---|---|---|
| `game/presentation/views/level_map_viewport.gd` | 新增——卷轴视口承载件:画布宿主与裁切、平移状态与 clamp、锚点跟随、平滑滚动、指引件(拖拽/滚轮判定留画布 `_gui_input`——事件流同源) | 行为改动(R2–R8)·新增 |
| `game/presentation/views/level_map_screen.gd` | 修改——CenterContainer 承载换视口件;停点切换/回图接线 `reanchor()`;`_content_size` 拟合后调 `reanchor(false)` 首锚 | 行为改动(R1+R2/R3 随动)·复用既有文件 |
| `game/data/schemas/level_gen_params.gd` | 修改——头注指针反转(owner 单点改为本文件域校验+参数契约指针) | 非行为改动(R9)·复用既有文件 |
| `game/probe/level_map_viewport_probe.gd` + `.tscn` | 新增——视口行为场景探针(真输入驱动) | 行为改动(R2–R8 验收载体)·新增 |
| `game/probe/probe_ui_util.gd` | 修改——新增拖拽沿与滚轮沿真输入合成工具 `push_drag_at`/`push_wheel_at`(沿 `push_click_at` :717-733 同式;探针共用面唯一落点) | 非行为改动(探针侧工具)·复用既有文件 |
## 7. 任务卡 T0..T4
### T0 读数与基线(0.5 工作日)
- [ ] 核对 `tools/gdstyle_baseline.txt` 有无 `level_map_screen` 相关 S13 键;有则记录登记值(改后只降不升),无则记「无键、按工具默认」
- [ ] 跑场景探针现状读数:`…console.exe --headless --path . res://probe/ui_struct_probe.tscn`(level_map 审计段绿)+ `res://probe/screen_flow_probe.tscn`——读数记入本步提交信息
- [ ] 跑冒烟 `python tools/run_smoke.py`——基线读数在案
### T1 视口承载件与手动平移(1–1.5 工作日)
- [ ] 新增 `level_map_viewport.gd`(接口块见 §10):`setup(canvas, anchor_provider, viewport_size)`、`pan_by(delta)`、`viewport_center_canvas_pos() -> Vector2`、`is_clamped_at_edge() -> bool`;`clip_contents=true`;成员 ≤8
- [ ] `level_map_screen.gd` 承载改造:CenterContainer 换视口件实例;`open_choices` 拟合后调 `reanchor(false)` 首锚(入口停点锚入口组、其余锚当前节点——`anchor_provider` 由 screen 提供画布坐标)
- [ ] 拖拽/滚轮判定落**画布既有 `_gui_input`**(画布已持有鼠标事件流——悬停/选路同源,禁在视口件层做父子事件路由竞争;视口件只管裁切/offset/clamp/跟随):拖动距离阈值(provisional 8px)判定,超阈值进拖拽态逐帧调 `viewport.pan_by()`;滚轮横滚映射 `pan_by`(代拍板:滚轮下=向右/前进向)
- [ ] clamp:`pan_by` 后 offset 收敛在画布包络内(`viewport_center_canvas_pos()` 画布坐标恒在 `[half_view, canvas_size-half_view]` 夹紧域;**画布小于视口=居中分支**——夹紧域退化为居中定位);探针宿主沿 ui_struct_probe :204 同款(组合根真实屏 `main.ui.modals.run.level_screen`),禁合成投喂替代真实路径
- [ ] 探针 `level_map_viewport_probe.gd`:断言 R3(拖拽移视口)、R4(滚轮移视口)、R5(clamp 边界)、R6(拖拽不触发选路——`route_chosen` 零发射);真实入口=probe_ui_util 合成沿走 `push_input`
### T2 锚点跟随与平滑滚动(0.5–1 工作日)
- [ ] `reanchor(smooth)`:目标 offset=锚点画布坐标−视口半宽(居中);`smooth=true` 走单段缓动 tween 时长 `UiKit.motion(0.3)`(provisional);`smooth=false` 即达;重入先杀在飞 tween 再建新(快速连续停点切换防叠加)
- [ ] 减动效:`UiKit.motion` 通道既有减动效语义下滚动跳终态——探针断言 R7(减动效开=位置即达,无 tween 中间帧)
- [ ] level_map_screen.gd 停点切换与兑付回图路径接 `reanchor(true)`——入口停点锚入口节点组(多入口取组包围盒居中)
- [ ] 探针断言 R2(停点切换后当前节点画布坐标投影=视口中线 x=640±2)
### T3 指引件(0.5 工作日)
- [ ] 视口件内渐隐带:屏缘左右各 70px 渐变(色取屏底同族——代拍板渐变 stops);浅图画布不超屏时渐隐带隐藏(无暗示必要)
- [ ] 平移提示 chip:240×30 圆角 6、深底石灰描边、文案「◄ 拖拽画布 / 滚轮平移 ►」14px;图区底部居中(level-map.md:104)
- [ ] 探针断言 R8(渐隐带与 chip 可见性+几何判据——像素级判据②③,level-map.md:154)
### T4 指针反转与收口(0.5 工作日)
- [ ] `level_gen_params.gd` 头注 :3-4/:69 反转:「取值域与字段契约单点=本文件域校验;概念与设计意图=关卡节点图生成.md v1.5 参数契约节」(非行为改动——grep 判据 R9)
- [ ] 全探针最窄回归:ui_struct_probe level_map 段+screen_flow_probe 复跑绿(波内修复只做出红最小单元的最窄确认)
- [ ] 产出物落位:`reports/iter<N>-实现报告-<日期>.md`(各卡状态+实机目检总清单+实现决定记录)+ `reports/iter<N>-反哺点-<日期>.md`(同步面所列逐条落点)——轻闸全绿后、合并前写齐
估时合计:3–4.5 工作日(中值 3.5)。
## 8. 验证闸(改动—验收对照表)
| 需求点 | 改动(文件:行/行为) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 能力行 |
|---|---|---|---|---|---|---|
| R1 浅图整幅居中无裁切+画布入视口件承载(锚 design/gdd/ux/level-map.md:260) | level_map_screen.gd(CenterContainer→viewport 件) | `level_map_viewport_probe.gd:_probe_shallow_no_crop`〔卷轴批 V1〕 | 探针 | 场景探针实例化本屏+open_choices 真实停点 | 画布尺寸≤视口时居中分支 offset 定位、无裁切 | 新增:关卡画面视口跟随 |
| R2 视口锚点:停点切换/回图后当前节点居中 x=640±2(锚 design/gdd/ux/level-map.md:101、:269) | level_map_viewport.gd `reanchor`+screen 接线 | `level_map_viewport_probe.gd:_probe_follow_anchor`〔卷轴批 V2〕 | 探针 | 合成推进停点切换经真实屏流程 | 当前节点画布坐标经 `viewport_center_canvas_pos()` 投影=640±2 | 新增:关卡画面视口跟随 |
| R3 拖拽平移移动视口(锚 design/gdd/ux/level-map.md:103) | 画布 `_gui_input`(level_map_screen.gd)拖拽态调 `pan_by` | `level_map_viewport_probe.gd:_probe_drag_pan`〔卷轴批 V3〕 | 探针 | `ProbeUiUtil.push_drag_at` 拖拽沿走 `push_input` | `_offset` 位移量=拖拽位移(clamp 前) | 新增:关卡画面视口平移 |
| R4 滚轮横滚移动视口(锚 design/gdd/ux/level-map.md:103) | 画布 `_gui_input` wheel 分支调 `pan_by` | `level_map_viewport_probe.gd:_probe_wheel_pan`〔卷轴批 V4〕 | 探针 | `ProbeUiUtil.push_wheel_at` 合成滚轮沿走 `push_input` | `_offset` 位移=滚轮步进映射 | 新增:关卡画面视口平移 |
| R5 clamp 边界与首末列可达:不出露屏底空档、拖拽两极首列与末列(含 Boss)入屏(锚 design/gdd/ux/level-map.md:103、:260) | `pan_by` 收敛逻辑+包络两极 | `level_map_viewport_probe.gd:_probe_clamp_bounds`〔卷轴批 V5〕 | 探针 | 极端拖拽沿(超包络、两极到边) | `viewport_center_canvas_pos()` 恒在夹紧域;浅图居中分支;`is_clamped_at_edge()` 翻转;两极时深度 0 与深度 13 列中心入视口 | 新增:关卡画面视口平移 |
| R6 拖拽不触发选路提交(锚 design/gdd/ux/level-map.md:103) | 画布 `_gui_input` 阈值判定(provisional 8px) | `level_map_viewport_probe.gd:_probe_drag_no_submit`〔卷轴批 V6〕 | 探针 | 拖拽沿起止于候选节点上方 | `route_chosen` 零发射+屏幕未关 | 新增:关卡画面视口平移 |
| R7 平滑滚动与减动效瞬切(锚 design/gdd/ux/level-map.md:102、:188、:227) | `UiKit.motion(0.3)` tween/减动效跳终态 | `level_map_viewport_probe.gd:_probe_smooth_scroll_reduced_motion`〔卷轴批 V7〕 | 探针 | 停点切换+设置面减动效置位 | smooth=true 有 tween 中间帧;减动效开=位置即达 | 新增:关卡画面视口跟随 |
| R8 指引件:渐隐带 70px+chip 240×30 呈现(锚 design/gdd/ux/level-map.md:104、:154) | 视口件指引子件 | `level_map_viewport_probe.gd:_probe_guidance_chrome`〔卷轴批 V8〕 | 探针 | 深图 open_choices 真实停点 | 像素级判据②③(level-map.md:154)——可见性+几何 | 新增:关卡画面视口指引 |
非行为改动行:schema 头注指针反转(level_gen_params.gd:3-4/:69)——判据=全仓无「域并单点=调参表」残留、头注含「参数契约节」指向(锚 design/gdd/systems/冒险循环/关卡节点图生成.md:109);由 §6 文件结构图该行覆盖,T4 落步,实现报告对照表列非行为改动行。
锚行核对义务:开工时行号以当版文件为准(本卡锚基于 v2.0/v1.5 抄录)。
- 测试方式两分法:R1–R8 全走探针自动化(调试注入不适用——本卡零数据面改动,状态经真实停点与真实输入驱动);用户目检项=「深度 13 图拖拽可达首末列、跟随方向感、chip 不碍选路」登记进实现报告实机目检总清单(登记不阻塞)。
- 回归措辞:里程碑步(T 段)出口跑一次场景探针族全套;波内修复只做出红最小单元的最窄确认;新增探针先行孤立验证再并入全套。
- 测试削弱申报:无删旧断言申报;ui_struct_probe level_map 段既有审计判据不改——承载改造若触其结构断言面,适配性调整须在实现报告申报并保持判据语义。
- 独立验收审计:`reports/iter<N>-验收审计-<日期>.md`,由未参与实现的会话执行;本卡验收档=中,审计深度按 test-conventions §8.7 中档执行(对照表逐行全量、判据绑定面抽查、削弱检查逐处、直查例外逐条、最窄复跑以远端 CI 现查读数代证)。
## 9. 异常分级
- L1 自救:探针断言红→改后只跑出红探针的最小单元;闸红只跑红件。
- L2 记录继续:画布 `_gui_input` 内拖拽态与悬停/选路命中分支次序的调整、渐变/chip 视觉微调(provisional 区间内)、S13 读数波动在只降不升约束内的消化。
- L3 停机写报告等用户:触碰 §3 任一 🔴;S13 键上升且无法只降;拆不出内聚单元;机检预算互斥=上游缺陷停卡退回写卡段;拖拽误触选路提交且路由层面消不掉。
## 10. 落位与可复用性预检
- **level_map_viewport.gd(新增)**:不复用 `ScrollContainer`——其滚动条 UI、焦点/滚动语义与本屏「纯鼠标、无滚动条、锚点居中跟随」冲突,且平滑滚动与 clamp 语义仍需全自管;承载裁切用 Control `clip_contents`(引擎内建,项目已有 clip 先例)。唯一面:视口 offset 状态只落本件(screen 的 `_layout_cache` 仍管布局、不管视口);全仓 grep 无第二处视口/平移逻辑。分层:呈现层内部子件——构造注入(canvas/anchor_provider/viewport_size 一次交齐,code-conventions §1.6 拆分交接);读核心只读面经 screen 既有出口,零新增核心触点。
- **probe_ui_util.gd 增补(复用既有文件)**:`push_drag_at`/`push_wheel_at` 落探针共用面唯一落点——禁另起工具文件散落;`push_click_at`(ui_input_probe :717-733 同式)点击沿同式扩展。
- **指引件(渐隐带/chip)**:随视口件承载(同职责:视口呈现行为);不立独立文件——拆出「命不出名字的打包」即凑计数袋。
- **level_gen_params.gd 头注反转(复用既有文件)**:注释单行改动,无新增件。
## 11. 实现规范预算
- **新增 level_map_viewport.gd 三查**:①接口块公开方法 4(setup/pan_by/viewport_center_canvas_pos/is_clamped_at_edge)≤20 ✓;②成员变量 ≤8(_canvas/_anchor_provider/_viewport_size/_offset/_tween/_drag_active/_drag_accum/_guided_visible)≤15 ✓;③无迁移函数(新件零存量函数迁入——screen 侧只是接线改造,hover/命中逻辑零迁移)。
- **存量触面**:level_map_screen.gd——T0 记 S13 键基线;本批改动为承载替换与 reanchor 接线,预期键值持平或只降;达成手段=T4 复跑 S13 对账。
- 指标例外:无申报。
## 12. 代拍板白名单
1. 画布 `_gui_input` 内拖拽态与悬停/选路命中的事件分支次序(红线=R6 判据不破)。
2. 拖拽距离阈值具体值(8px 附近 provisional 区间内定值)。
3. 平滑滚动缓动曲线类型(单段缓动家族内选型)。
4. 渐隐带渐变 stops 与 chip 深底/描边色值(art-bible §4.1 冻结色板同族取值)。
5. 滚轮方向映射(下=向右/前进向)。
超清单决定记入实现报告「实现决定记录」节。
## 13. 待裁点
零待裁点,开工门=U0(四口子与效果图 2026-11-05 用户已裁决;验收档「中」随批卡确认)。
@@ -1,267 +0,0 @@
# 需求卡片·改写族孤儿机制与退役名清退批(2026-11-05)
来源:待办清单条目「damage_controller 按序改写族——内容孤儿机制与退役名注释层」(2026-11-04 登记,来源=会话提及)。
唯一事实源:`design/gdd/systems/跨局循环/锻造工坊.md` v0.13 §满级特效扩展位(本期已按本批口径订正)+本卡「现状事实」所列代码读数;机制处置与注释口径的裁决记录=2026-11-05 会话,用户裁「只要有问题的都要改」,退役内容名一律清退、零现役注册的改写分支一律摘除。
验收档:中(一处玩家可见明细行文案换代+若干不可达分支摘除;不写存档、不改乘区公式与数值表)。
估时:2.25–2.75 个工作日(T0 0.25+T1 0.5+T2 0.5+T3 0.5–1+T4 0.25+T5 0.25 加总)。切分预案:T3 注释面若单轮改不完,按文件切成 T3a(core 八件)/T3b(schemas+sim+tests),两张各自收尾,不并进 T1。
---
## 1 目标与完成定义
做完这五件事算完:
1. 主角先手攻击的四个改写分支摘除——它们零数据表行注册,游戏里从不执行。
2. 连带收窄两处引擎词表:`EFFECT_KINDS` 摘 `spike`、`DO_KEYS` 摘 `trade_hit`/`trade_atk`,并同批补负域探针取证。
3. `game/` 注释面退役内容名清零(42 行/13 文件),只换名不换职责陈述。
4. 三处挂退役件名的玩家可见文案改中性机制文案(其中一处今天可达),一处 sim 死匹配删除。
5. 产出物两份写齐(实现报告+反哺点清单),代码域闸全绿,改前改后对拍逐位一致。
判据总口径:本批除第 4 项文案外全部是非行为改动——零现役注册行=分支不可达=删了也不改变任何一帧输出。这条不靠感觉,T0 用三条 grep 读数坐实。
## 2 设计事实源
- 设计线闭合凭据:`design/gdd/systems/跨局循环/锻造工坊.md` v0.13(2026-11-05)§满级特效扩展位——本批订正该节四处陈述,其中重甲反震与长枪先手连击两条候选的代价档由「零新代码」改判「需新代码」,效果类型闭集词数不再抄进文档。
- 机制处置裁决:本卡所属待办条目「实况」段所载 2026-11-05 会话裁决。
- 代码现状取证:本卡第 3 节全部带 `文件:行号`,无一条凭记忆。
## 3 现状事实
### 3.1 四个改写分支零注册(本批要摘的)
| 分支 | 代码位置 | 触发条件 | 现役注册行 | 退役名注释 |
|---|---|---|---|---|
| 命中交换 | `game/core/damage_controller.gd:297-302`(命中侧)、`:351-353`(增伤侧) | `kind=modify_ticket` 且载荷带 `trade_hit`/`trade_atk` | 0 行(`game/data/tables/` 全域零命中) | :285-286、:341、:350(矿工重锤) |
| 免掷恒折扣 | `:305-309` | `kind=modify_ticket` 且 `mode=armor_rewrite` | 0 行 | :287(专注) |
| 满层必中与倍乘 | `:310-313`(必中置位)、`:368-372`(倍乘) | `kind=modify_ticket` 且 `mode=spike` | 0 行 | :288(厚积薄发,行内含 spike)、:342 与 :368 |
| 轻甲折扣档改善 | `:320-323` +私有函数 `_discount_shift_up`(`:623`) | `kind=modify_ticket` 且 `mode=discount_shift` | 0 行 | :288、:622(猎弱本能) |
穿线键 `spike_ready` 出现在 `:282`、`:310`、`:313`、`:325`、`:369` 五处,随分支一起消失。
克制点数面(`mode=clamp_bonus`)有现役一行注册:`game/data/tables/feats.tres:853-857` 淬毒刃纹(`kind=modify_ticket`/`mode=clamp_bonus`/`delta=2`)——**不摘**,`_ctx_clamp_delta`(`:333-338`)与 `clamp_check` 发射点(`:315`)保持原样。
### 3.2 两处词表在册
- `game/core/effect_engine.gd:30` `EFFECT_KINDS` 含 `"spike"`;该常量实数 32 词(本卡现读,摘后 31)。全仓按 `kind == "spike"` 派发效果的代码**不存在**——`spike` 只在词表与注释里在册(另两处:`:30` 词表、`:452` 「任意真族(任一命中即生效):spawn_spell、spike」注释)。
- `game/core/effect_engine.gd:82` `DO_KEYS`(现 26 键)含 `"trade_atk"`, `"trade_hit"`;执法点在 `:417`(`if not DO_KEYS.has(String(k))` 出域即拒绝)。摘后 24 键。
- `attack_segments` 不属本批:它已随架构审查整改甲案整链退役(`grep -r attack_segments game/` 仅命中 `game/tests/unit/data_config/effect_kind_retired_test.gd:3` 的注释),本批只把它在 GDD 里的错句一并订正(见第 6 节)。
### 3.3 注释面退役名全仓扫描读数
名单来源=`design/content-archive/道具退役素材库.md` 两张表的「原名」列,共 41 名;减去表内仍作为现役 `display_name` 出现的 1 名(淬毒刃纹)=40 名。扫 `game/` 全域 `.gd`/`.tres`/`.md` 命中 **53 行/13 文件**,无一条落在 `.tres` 数据表里。
| 文件 | 注释面行号(本批改) | 字面量行号 | 字面量处置 |
|---|---|---|---|
| `game/core/damage_controller.gd` | 109、138、285、287、288、328、341、350、394、424、622 | — | — |
| `game/core/reward_grant.gd` | 2、130、183、221 | 189、226 | 两处都改中性文案(T4) |
| `game/core/settlement/grant_segment.gd` | 127、128、232、240、265、279、283、351、362、373 | 166 | 玩家可见标签改中性(T4) |
| `game/core/reward_roller.gd` | 1、41、46、86 | — | — |
| `game/core/effect_engine.gd` | 64、65、157、452 | — | 452 随 T1 摘词改写 |
| `game/core/battle_orchestrator.gd` | 258、538 | — | — |
| `game/core/action_economy.gd` | 82 | — | — |
| `game/data/schemas/treasure_item_table.gd` | 20、122 | — | — |
| `game/sim/adventure_monte_carlo.gd` | 75 | — | — |
| `game/sim/level_autoplay.gd` | 415 | 421 | 死匹配删除(T4);415 随删该处注释 |
| `game/tests/unit/core/meta_progression_forge_test.gd` | 677、690 | 694、697、699、700 | 断言串随 T4 文案换代(申报见第 8 节) |
| `game/tests/unit/battle_settlement/variant_placement_test.gd` | 239 | — | — |
| `game/tests/unit/core/treasure_items_test.gd` | — | 31、33、35 | 不动——StubDB 自造夹具名,不宣称内容现役 |
字面量合计 11 行:本批动 8 行(三文件的四处文案/死串=`reward_grant:189`/`:226`、`grant_segment:166`、`level_autoplay:421`,加锻造测试四处断言串 `694`/`697`/`699`/`700`),其余 3 行夹具名保留。注释面 42 行全改,另加 `effect_engine.gd:452` 一句随摘词改写=本批改写 43 处。
### 3.4 玩家可见文案三处
- `game/core/reward_grant.gd:226`:`mine_hit` 减半路径发射的结算明细行标签——`"惊巢反制——危险格挖掘惩罚减半(%d→%d)" % [...]`,载荷 `"effect_id": "nest_counter"`。`惊巢反制` 与 `nest_counter` 都是退役件名(素材库表一 :18),而这行文案是玩家能看见的,且这条路径今天可达(有现役锻造锋行驱动——`meta_progression_forge_test.gd:694-700` 正在钉它)。
- `game/core/reward_grant.gd:189`:`_cell_removal_open` 空集拒绝 reason——`"爆破工兵包:3×3 区域内无可移除危险格(须未揭示且未封印)"`。该通道 `cell_removal` 在册(`game/data/schemas/treasure_item_table.gd:26`、消费点 `reward_grant.gd:160-161`)但零现役数据行(`game/data/` 全域零命中),所以 reason 今天不可达;文案仍挂退役件名。通道本体不进本批(见第 5 节),本批只清文案。
- `game/core/settlement/grant_segment.gd:166`:`mine_hit` 订阅中 `kind=open_ticket` 且 `channel="reflect"` 的行开单,明细标签硬编码 `"惊巢反制·反弹"`,归因串 `"attune_nest_counter"`。`"reflect"` 在 `game/data/tables/` 二十一张表逐张 grep 零命中 → 今天不可达,与 `:189` 同类问题(标签挂退役件名)。`reflect` 渠道本体不进本批(见第 5 节),本批只清标签与归因串。
### 3.5 一处 sim 死匹配
`game/sim/level_autoplay.gd:421` 用 `String(row.get("display_name", "")).contains("雷区图谱")` 捕高光事件,`:415` 是配套注释。`雷区图谱` 零现役行(`game/data/` 零命中)→ 该条件恒 false。
### 3.6 既有测试面读数(决定本批要不要改测试)
- 四个 mode 与两个键在 `game/tests/`、`game/probe/`、`game/sim/` **零命中**(`grep -rn "trade_hit\|trade_atk\|armor_rewrite\|discount_shift\|spike" game/tests/ game/probe/ game/sim/` 空)。摘分支不动任何既有断言。
- `armor_roll` 有既有断言:`game/tests/unit/battle_settlement/roll_multiplier_test.gd:125 _smoke_armor_roll()`——它测的是护甲骰与折扣读数,不是订阅消费。所以掷点面 `:293 _armor_roll()` 与发射调用 `:296 _fx_hits("armor_roll", ...)` 都保留(`armor_roll` 在 `effect_engine.gd:13` 事件词表在册),只删 `:305-309` 消费环,`ar_hits` 局部变量随删。
## 4 设计契约
- 🔴 **契约 1|零现役注册坐实才许摘**:T0 三条 grep 读数必须全为零——`game/data/tables/` 内 `spike`/`armor_rewrite`/`discount_shift`/`trade_hit`/`trade_atk` 各 0 行、`tree_rules` 面 0 行、`effect_kinds` 面 `cell_removal` 0 行(后者只作记录,本批不摘通道)。任一数非零=该分支可达=停卡 L3 呈报,禁直接摘。
- 🔴 **契约 2|注释改写只换名不换职责**:42 行注释逐处保留「这段在做什么、按什么序、为什么这样取」的陈述,把退役件名替换成机制语言。整行删除、成片缩写、或删注释换 gdstyle 行数绿=违规(先例:宿主行数压至 999 时「禁删注释凑行数红线未碰」)。
- 🔴 **契约 3|文案换代禁碰数值与发射序**:`:226` 只改标签字符串,`[int(row.power), half]` 取值、`emit_cb.call` 参数位序、`"effect_id"` 载荷键一律不动;`:189` 只改 reason 前缀,`{"ok": false}` 形态不动。
- 🔴 **契约 4|词表收窄与负域探针同批**:摘 `EFFECT_KINDS` 的 `spike` 与 `DO_KEYS` 的两键,必须同批落 T2 探针;只摘词不取证=本卡未完成。
- 🔴 **契约 5|禁留空壳**:`spike_ready` ctx 键、`ar_hits`/`shift`/`trade_hit` 局部变量、`_discount_shift_up` 函数随分支即删,禁保留空返回、别名或「以后可能用」的成员凑机检文本。
- 🔴 **契约 6|实现批禁写文档**:`design/` 整个目录、`待办清单.md`、`体验验证清单.md`、`game/tests/README.md` 一律不写;要改的文档句子全进第 6 节与反哺点清单。
- 🔴 **契约 7|零新增数值**:本批不引入任何玩法数值,不改 `.tres` 表值。MC 与 verifier 零跑(无数值改动零跑)。
## 5 范围边界
做:3.1 四段分支+穿线键+`_discount_shift_up`;3.2 两处词表摘名;3.3 注释面 43 处;3.4 三处文案;3.5 一处 sim 死匹配;3.6 消费环 `armor_rewrite` 删而发射点留;T2 负域探针;T4 断言串随动。
不做,去向写明:
| 不收件面 | 为什么不做 | 去向 |
|---|---|---|
| `EVENTS` 零订阅词普查(含 `armor_roll` 发射点去留) | 与「改写分支」不同轴,是事件词表整体普查 | 待办清单本批同登条目「效果通道与事件词的零现役普查」 |
| `effect_kinds`/渠道零现役清退(`cell_removal`、`mine_hit_halve`、`open_ticket` 的 `channel="reflect"` 行族) | 通道在册、有消费点、只缺数据行——判「删通道」要连同 schema 词、归因标识与拒绝文案整体裁 | 同上条目,与 iter52 孤儿函数清退批同族 |
| GDD 其余仍提退役内容名的句子(`systems/冒险循环/奇物.md`、`systems/战斗循环/地图战斗资源.md`、`术语表.md`、`00-游戏概念.md`、`03-内容规模.md`) | 属文档面用语,不属代码批 | 反哺点清单,文档批落文 |
| `game/tests/unit/core/treasure_items_test.gd:31/33/35` 三个退役件名夹具 | StubDB 自造行,不宣称该件现役;改名动的是测试面 | 无——保留 |
| `attack_segments` 词表补摘 | 已随甲案退役,本批只在 GDD 订正错句 | 无——已完成 |
| 结算明细标签是否入表(文案硬编码在 `:226`) | 涉及「呈现文案归属」规范裁决,不属本卡 | 反哺点清单,文档批裁 |
## 6 关联文档同步面
| 文档 | 关联点 | 同步义务 |
|---|---|---|
| `design/gdd/systems/跨局循环/锻造工坊.md` §满级特效扩展位 | 闭集词数句、长枪候选代价档、重甲候选代价档、七条前提句 | **已在本批前置文档段落文**(v0.13);实现批禁再写该文件 |
| `design/gdd/systems/跨局循环/锻造工坊.md` 同节 | 重甲反震候选的实现前提:反弹需新开消费点(本批摘 `spike` 后彻底无位) | 设计权威文档,实现批不直改;写入 `reports/<迭代号>-反哺点-<日期>.md`,用户裁后归文档批 |
| `design/gdd/systems/战斗循环/增益状态.md`/`伤害与护甲.md` | 若其中叙述「改写族四拍」的句子里挂了退役件名 | 机械面核对;有则列反哺点,无则实现报告记「零命中」 |
| `game/tests/README.md` 能力表 | 本批无新增能力、无能力退役;T2 探针挂既有行「加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域/效果类型出域」(:177,代表用例含 `effect_kind_retired_test.gd:_assert_rejected`) | 无改动义务;批末核对表上名字与落地一致 |
| `待办清单.md`/`roadmap.md` | 条目状态与迭代行 | 由写卡段与编排段推进,实现批禁写 |
## 7 任务卡
### 文件结构图
| 文件 | 职责 | 改动性质 |
|---|---|---|
| `game/core/damage_controller.gd` | 主角/敌方伤害结算编排 | 非行为改动(摘不可达分支+注释改写)→ 对照表 R1、R4 |
| `game/core/effect_engine.gd` | 效果词表与行校验 | 非行为改动(摘词两枚+注释改写)→ R2、R3、R4 |
| `game/tests/unit/data_config/effect_kind_retired_test.gd` | 负域加载探针宿主 | 新增两个探针常量与两次调用(非行为改动的取证面)→ R2、R3 |
| `game/core/reward_grant.gd` | 奖励与格面兑付 | 行为改动(一处明细行标签+一处拒绝 reason 文案)→ R5、R6 |
| `game/tests/unit/core/meta_progression_forge_test.gd` | 元进度/锻造断言宿主 | 断言串随 R5 换代(申报见第 8 节)→ R5 |
| `game/sim/level_autoplay.gd` | autoplay 驱动件 | 非行为改动(删恒 false 匹配+注释)→ R4、R8 |
| `game/core/settlement/grant_segment.gd` | 授予段结算编排 | 行为改动(`:166` 明细标签文本)→ R7;注释面 10 行改写 → R4 |
| `game/core/reward_roller.gd`、`game/core/battle_orchestrator.gd`、`game/core/action_economy.gd`、`game/data/schemas/treasure_item_table.gd`、`game/sim/adventure_monte_carlo.gd`、`game/tests/unit/battle_settlement/variant_placement_test.gd` | 注释面 | 非行为改动(注释改写)→ R4 |
零新增文件、零新增函数、零新增数据件;唯一的"新增"是既有套件内的两个探针常量。
### T0 基线与读数对账(0.25 工作日)
取用:无。产出:改前基线三件——对拍转储、S13 键读数、契约 1 三条 grep 读数。
- [ ] 跑契约 1 三条 grep,逐条把命中数写进实现报告 T0 节(期望全 0,除 `feats.tres:854` 的 `clamp_bonus` 一行作对照记录)。
- [ ] 读 `tools/gdstyle_baseline.txt` 内 `damage_controller.gd`/`effect_engine.gd`/`reward_grant.gd`/`level_autoplay.gd` 现登记键,抄进实现报告「存量触面」表(第 14 节要求只降不升)。
- [ ] 采改前对拍基线:`game/sim/dump_battle.gd` 六组(L{1,2}×S{42,779401,779501},stdout 单流转储,先例=拆件批 T6 同参)。
- [ ] 采改前明细行读数:`meta_progression_forge_test.gd:_smoke_tree_rules` 现断言串的完整文本抄进实现报告(T4 换代后的比对基准)。
### T1 摘四段死分支与穿线键(0.5 工作日)
取用:T0 的契约 1 读数全零。产出:`_ctx_rewrites` 与 `_hero_attack_ctx` 的收窄返回键集(T2、T4 引用)。
- [ ] `damage_controller.gd`:删 `:297-302`(命中侧交换)、`:351-353`(增伤侧交换)、`:305-309`(免掷覆盖)、`:310-313` 置位块+`:368-372`(spike 倍乘)、`:320-323`(移档调用)与 `:623` 函数 `_discount_shift_up(discount: int, shift: int) -> int`。
- [ ] 同文件:删 `clamp_hits` 收集后残留的 `shift` 局部变量、`ar_hits` 局部变量(发射调用 `:296` 保留但不接收返回值,见 3.6);`spike_ready` 键从 `:282`、`:325` 返回字典与 `:369` 消费处一并去除。
- [ ] 收窄后的签名与键集照此,不得自创:
- `_ctx_rewrites(t: Dictionary, target: Dictionary, armor: String, matchup: String) -> Dictionary` 返回键集 = `{roll_read, threshold, crit, discount, clamp_hits}`。
- `_hero_attack_ctx(t: Dictionary) -> Dictionary` 返回键集 = 现键集去掉 `spike_ready`(`:278-282` 其余键逐字保留)。
- `_hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float` 签名不变,体内删两拍。
- [ ] 头注与分节注释随删随改(`:249-254`、`:264-266`、`:285-290`、`:341-342`、`:368`):改写后的四拍叙事只剩「掷点折扣读数 → 克制点数」两拍,注释口径同步收窄(这条与 T3 同判据,禁再挂退役件名)。
- [ ] 单元侧适配:`game/tests/unit/battle_settlement/damage_controller_test.gd`、`roll_multiplier_test.gd`、`game/tests/unit/contracts/system_contracts_test.gd` 跑一遍;3.6 已证零命中,若真出红,只改装配不改进判据,并把改动写进实现报告「实现决定记录」。
### T2 词表收窄与负域探针(0.5 工作日)
取用:T1 摘除后的 `damage_controller.gd` 键集(探针只需构造表行,不引用控制器内部键)。产出:`effect_kind_retired_test.gd` 两枚新探针常量 `_assert_rejected` 新断言名(R2/R3 引用)。
- [ ] `effect_engine.gd:30` 摘出 `"spike"`(`EFFECT_KINDS` 实数 32→31);`:82` 摘出 `"trade_atk"`, `"trade_hit"`(`DO_KEYS` 19→17 项,实数以摘后 `grep` 行内计数为准)。
- [ ] 同文件 `:452` 组合规则注释改为只列现役:「任意真族(任一命中即生效):`spawn_spell`。」;`:64`/`:65`/`:157` 注释改写并入 T3 判据面。
- [ ] `game/tests/unit/data_config/effect_kind_retired_test.gd`:按既有形态新增两枚探针——`PROBE_SPIKE_MODE`(注入 `{"kind": "modify_ticket", "mode": "spike"}` 行的夹具目录)与 `PROBE_TRADE_KEY`(注入带 `trade_hit` 键的 do 字典)。各建一个 `user://` 夹具目录常量,沿用 `_inject(dir, probe)` 与 `_assert_rejected` 既有函数,不新建套件文件。
- [ ] 断言名与尾缀:`_assert_rejected` 内新增两条检查——`"词表闭面:kind=spike 出域拒绝"` 与 `"do 键闭面:trade_hit 出域拒绝"`;该套件已在 `game/tests/run_all.gd:195` 负面清单在册(`expect=1`、`negative=true`),本步只加探针,不动在册条目与退出码判据。
- [ ] 独立验证先行:只跑该套件(`godot --headless --path game --script res://tests/unit/data_config/effect_kind_retired_test.gd`)出绿,再并入全套。
### T3 注释面退役名改写(0.5–1 工作日,可切 T3a/T3b)
取用:3.3 表逐行清单。产出:注释面 grep 零命中读数(对照表 R4)。
- [ ] 名单唯一来源=`design/content-archive/道具退役素材库.md` 两张表的「原名」列,现场解析;禁把 40 个名字复制成第二份清单文件(第 12 节唯一面)。
- [ ] 逐处改写口径=「这段代码做什么、按什么序、取值从哪来」,内容名换成机制名。示例(禁照抄字句,只照抄口径):
- `:285` 「第一拍矿工重锤命中交换」→ 「命中判定减成换攻击增伤的交换位(现役零注册,随本批摘除后此句消亡)」——摘除后该注释整段随分支删除,改写只适用于 T1 未摘的 11 个文件行。
- `reward_roller.gd:1` 「偏运硬币/board_skew 两路投影」→ 「订阅加权与 board_skew 两路投影」。
- `grant_segment.gd:232` 「完美布防=本轮封印批全中」→ 「封印全中倍率=本轮 hit>0 且 miss==0 时」。
- [ ] 判据 grep:改写后重跑 3.3 那条扫描,注释面命中数必须为 0;读数贴进实现报告。
- [ ] 不碰第 5 节点名保留的三行夹具串。
### T4 玩家可见文案与死匹配(0.25 工作日)
取用:T0 采到的改前明细行读数。产出:新标签文本(R5/R6 引用)。
- [ ] `reward_grant.gd:226` 标签 → `"挖掘惩罚减半(%d→%d)"`;同处归因串 `"effect_id": "nest_counter"` → `"nest_halve"`(依代码规范用语纪律清零退役名;该串全仓仅此一处、不进存档不读表,改动面=本行+测试匹配串随动)。
- [ ] 同文件 `:183` 与 `:221` 注释随 T3 口径改写;`:189` reason → `"中立移除:3×3 区域内无可移除危险格(须未揭示且未封印)"`。
- [ ] `game/core/settlement/grant_segment.gd:166` 明细标签 `"惊巢反制·反弹"` → `"反弹伤害"`,同处归因串 `"attune_nest_counter"` → `"attune_nest_halve"`;`channel == "reflect"` 判据与开单参数字典逐字不动(契约 3)。
- [ ] `meta_progression_forge_test.gd`:`:694` 匹配串、`:697` 标签文案、`:699`/`:700` 期望串四处随动到新标签;判据仍绑 `step_row` 采集的 `display_name`,`nc_hit != ""` 与等值断言的强度不得降低。
- [ ] `level_autoplay.gd:421` 恒 false 的 `contains("雷区图谱")` 条件与该分支一并删,`:415` 注释随删。
- [ ] 窄测:`meta_progression_forge_test`、`round_flow_skill_pool_test`、`damage_controller_test`、`table_structure_guard_test` 四套跑绿。
### T5 产出物落位卡(0.25 工作日)
- [ ] 轻闸全绿后、合并前写齐 `reports/<迭代号>-实现报告-<日期>.md`:各 T 卡状态、T0/T2/T3/T4 四条读数、契约 1 三条 grep、改前改后对拍逐位一致结论、实机目检总清单(本批目检项:结算明细行减半标签一眼可读、无退役件名突兀感——登记不阻塞)、遗留读数义务(零现役通道与零订阅事件词两件,已另登待办条目,见第 5 节)、实现决定记录。
- [ ] `reports/<迭代号>-反哺点-<日期>.md`:第 6 节每条都要有落点,含「明细标签是否入表」这条规范裁决点。
- [ ] 里程碑步出口跑一次全套闸(`tools/run_all_gates.sh --code`);波内出红只做出红最小单元最窄确认。
## 8 验证闸(改动—验收对照表)
| # | 需求点(判据出处行锚) | 改动(文件:行) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 能力行 |
|---|---|---|---|---|---|---|---|
| R1 | 摘四段不可达改写分支(锚 `design/engineering/code-conventions.md:15`) | `damage_controller.gd:297-302/305-309/310-313/351-353/368-372/320-323/623` | `damage_controller_test.gd:_run_recipe_sequence`;`roll_multiplier_test.gd:_smoke_armor_roll`;`round_flow_skill_pool_test.gd:_smoke_full_battle`+dump_battle 六组逐字节比对 | 单元+集成 | `BattleOrchestrator.begin_battle()` → 主角先手开单兑付全链 | `step_row` 明细行文本与 `outcome` 取值逐位=T0 基线;静态判据=契约 1 三条 grep 零注册+摘后键集 grep 零 `spike_ready`。**观测面局限申报**:零现役注册行使分支不可达,无法从真实入口造出命中;禁人为注入表行取证(全项目禁变异类测试) | 既有:伤害单据编排与回放;既有:伤害公式与掷点乘区;既有:轮内流与结算五段全序 |
| R2 | `EFFECT_KINDS` 摘 `spike`(锚 `design/engineering/code-conventions.md:15`) | `effect_engine.gd:30` | `effect_kind_retired_test.gd:_assert_rejected`(新断言「词表闭面:kind=spike 出域拒绝」,尾缀 `expect=1 negative=true`) | 单元(负面,独立进程) | `ConfigDB.load_tables()` → `EffectEngine.validate_trigger_row` | 出域错误载荷含 `effect_kinds 出域` 文案+进程退出码 1 | 既有:加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域/效果类型出域 |
| R3 | `DO_KEYS` 摘 `trade_hit`/`trade_atk`(锚 `design/engineering/code-conventions.md:15`) | `effect_engine.gd:82` | `effect_kind_retired_test.gd:_assert_rejected`(新断言「do 键闭面:trade_hit 出域拒绝」) | 单元(负面,独立进程) | `ConfigDB.load_tables()` → `EffectEngine.validate_trigger_row` | 键出域拒绝错误载荷含 `trade_hit`+进程退出码 1 | 既有:加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域/效果类型出域 |
| R4 | 注释面退役名清零 13 文件 42 行(锚 `design/engineering/code-conventions.md:26`) | 3.3 表全部注释行号 | 静态判据:3.3 那条扫描命令注释面命中 0,读数贴实现报告;`--code` 域内静态扫描与 S11 段不变红;宿主既有套件逐位不变 | 静态(闸件扫描) | `BattleOrchestrator.begin_battle()` 链上宿主套件(改写面不进运行期) | 逐文件 grep 读数(文件名+行号+命中 0)+宿主套件回归 | 既有:换图系刷新链路与效果词表接线;既有:伤害单据编排与回放 |
| R5 | 减半明细行去退役件名(锚 `design/engineering/doc-conventions.md:47`) | `reward_grant.gd:226` | `meta_progression_forge_test.gd:_smoke_tree_rules`(换代断言串,落点 694/697/699/700) | 集成(信号采集) | 锻造锋线满级特效 → `BattleOrchestrator.begin_battle()` → `mine_hit` 减半路径 → `step_row` 信号 | `step_row` 行 `display_name` == 「挖掘惩罚减半(6→3)」,且 `power` 减半数值与发射次数逐字不变;`grep -r nest_counter game/` 零命中 | 既有:天赋树购买与六键合成 |
| R6 | 空集拒绝 reason 去退役件名(锚 `design/engineering/test-conventions.md:116`) | `reward_grant.gd:189` | 静态判据:`grep -c 爆破工兵包 game/core/reward_grant.gd` == 0;`table_structure_guard_test.gd` 与 `extended_row_violation_test.gd:_init` 跑绿 | 静态+单元 | `ConfigDB.load_tables()` → `_cell_removal_open` 所在兑付链(零现役行不可达) | grep 零命中读数+通道校验面绿。**观测面局限申报**:`cell_removal` 零现役行,reason 不可达,禁造行取证 | 既有:加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域/效果类型出域 |
| R7 | 反弹明细标签去退役件名(锚 `design/engineering/doc-conventions.md:47`) | `grant_segment.gd:166` | 静态判据:`grep -rc "惊巢反制·反弹" game/` == 0 且 `grep -rc "attune_nest_counter" game/` == 0;`round_flow_skill_pool_test.gd:_smoke_full_battle` 跑绿 | 静态+集成 | `BattleOrchestrator.begin_battle()` → `mine_hit` 订阅施加链(`channel="reflect"` 二十一张表零命中,今天不可达) | grep 零命中+发射面零变化回归。**观测面局限申报**:同上,禁造行取证 | 既有:轮内流与结算五段全序 |
| R8 | sim 恒 false 匹配删除(锚 `design/engineering/test-conventions.md:116`) | `level_autoplay.gd:415/421` | 静态判据:`game/sim/` 全域 grep `雷区图谱` 零命中;`--code` 域内驱动件相关套件跑绿 | 静态+单元 | `game/sim/level_autoplay.gd` 驱动链(测量面,不进游戏运行) | grep 零命中+套件绿。**观测面局限申报**:恒 false 条件不可达,禁造行取证 | 既有:轮内流与结算五段全序 |
测试方式两分法:R1/R2/R3/R5 归调试接口与真实入口断言;R6/R7/R8 三处的改动点今天不可达(零现役注册行驱动),一律走静态判据+观测面局限申报,禁造行取证,也不设目检;R5 的文本观感归用户目检——目检项「结算明细减半标签文本可读」写进实现报告总清单,登记不阻塞,不作合回前置。
闸口径:本批改动含 `game/` 下非 `.md` 行为载体 → 按 `tools/run_all_gates.sh --code` 选档,所选域全绿才合并;卡内不写死闸件名单。
回归措辞:里程碑步(T 段)出口跑一次全套闸;波内修复只做出红最小单元的最窄确认;新增探针先行孤立验证再并入全套。
测试削弱申报:唯一允许改的旧断言=`meta_progression_forge_test.gd:694/697/699/700` 四处期望串,理由是 R5 文案换代要求名实相符;断言强度(非空判定+等值判定+不含旧词)一条不降,阈值与扫描面不放宽。除此之外任何断言删除、阈值放宽、扫描面缩小都属未申报改动,独立验收审计判红。
独立验收审计:入口=合回前由未参与实现的会话或子代理执行,报告落 `reports/<迭代号>-验收审计-<日期>.md`;本档验收档=中,按 test-conventions §8.7 中档梯度核(R1/R2/R3/R5 四条必须逐条核到判据绑定面)。
## 9 异常分级
- L1 自救:注释措辞不通、局部变量名残留、grep 命令写法——就地改,不动判据。
- L2 记录继续:T1 单元侧适配出现既有断言装配报错(3.6 已预判零命中),按申报范围改装配并记入实现报告「实现决定记录」。
- L3 停机呈报并写报告:
1. 契约 1 任一条 grep 读数非零(分支可达)=上游事实与卡片互斥,停卡退回写卡段。
2. 摘词后加载出现非预期致命(现役行被误判出域)或现役套件出红且原因落在本批摘除面。
3. 机检预算与卡片契约互斥(例:收窄后某函数键数低于登记值触发基线红/留空壳才能凑绿)=停卡退回写卡段,禁自造第三条路。
4. 按纪律无法干净摘除、必须保留中间壳函数才能过闸=停卡呈报走豁免/还债通道,禁硬拆凑绿。
5. R5 断言换代后仍红且原因在数值或发射序(说明文案改动越界)。
## 10 代拍板白名单
允许实现期自行拍板:注释改写的具体中文措辞(守契约 2 即可);负面探针常量与夹具目录命名(沿用既有形态);grep/扫描命令写法;是否把 T3 与 T4 并成一次提交;测试文件内局部变量改名;实现报告小节标题措辞。
超出白名单=记入实现报告「实现决定记录」,不得静默。
## 11 待裁点
零待裁点,开工门=U0。(机制删留与注释口径已由用户在 2026-11-05 会话裁毕;GDD 四处错陈述已在写卡段前置文档段落文 v0.13;「明细标签是否入表」与三件遗留读数不属本卡执行面,已列第 5/6 节去向。)
## 12 落位与可复用性预检
- 复用面:本批零新增文件。负域探针复用 `game/tests/unit/data_config/effect_kind_retired_test.gd` 既有 `_inject`/`_assert_rejected` 骨架——它正是「退役词出域即加载致命」的现成载体,新建套件=重复落点。注释改写复用既有文件与函数,无新 helper。
- 唯一面:退役内容名清单的唯一来源=`design/content-archive/道具退役素材库.md`,判据脚本现场解析,禁在 `game/` 或 `tools/` 落第二份名单;摘词只改 `effect_engine.gd` 两处常量,`treasure_item_table.gd:20` 的注释属说明面(随 T3 改写),不另立词表。
- 分层与交互面:`game/core/damage_controller.gd` 与 `game/core/effect_engine.gd` 的交互仍走既有 `_fx_hits` 查询口与 `validate_trigger_row` 校验口,本批只删消费环、不改接口;`reward_grant.gd` 文案改动不动 `emit_cb` 契约;`game/sim/` 驱动件不回连产品码。Core 零 UI、数值只经 ConfigDB、随机只经 RngService 三条红线本批一律不绕(本批不新增数值与随机消费)。
## 13 禁为机检文本保留无效成员
本卡为清退类:禁止为迎合机检消息文本或计数保留/新增任何无效成员、空壳函数、无用别名或 ctx 冗余键——`spike_ready`、`ar_hits`、`shift`、`trade_hit` 局部变量与 `_discount_shift_up` 一律随分支删净。机检键与本卡契约互斥时,实现段停卡呈报,不得自造第三条路。
## 14 实现规范预算
本批无新增文件,新件三查不适用;执行的是存量触面一查:
- `game/core/damage_controller.gd`:T1 摘除使 `_ctx_rewrites` 与 `_hero_attack_base_value` 的分支数、函数长度下降;`_discount_shift_up` 整函数消亡。预期 S13 键只降不升,T0 抄录现登记值、T5 抄录摘后读数两张对照。
- `game/core/effect_engine.gd`:两处常量字面量变短,方法数与成员变量数不变。
- `game/core/reward_grant.gd`、`game/sim/level_autoplay.gd`:只删文本与一条恒 false 条件,分支数下降或持平。
- 指标例外通道:本卡不申报任何例外——若摘除后仍有键超默认,属存量在册键,沿用 `tools/gdstyle_baseline.txt` 现登记,不新增。
## 15 拆分边界纪律
本卡是清退类,不产生拆出单元;条款照写以防实现期顺手重组:
1. 若实现中确需拆出新私有函数或新段件,每个单元必须能写出一句职责陈述与单一变化原因,并逐件写进实现报告;写不出=凑计数袋,回炉。
2. 「过阈值」只作发现信号,禁作为本批拆分或收拢的动机——本批唯一动机是零现役注册分支的清退。
3. 按纪律无法干净摘除时停卡呈报走豁免/还债通道,禁硬拆凑绿(进第 9 节 L3 第 4 条)。
+3 -3
View File
@@ -5,8 +5,8 @@
## 当前位(只回答「现在该做什么」;三行内随批次换代,禁止在此扩写成状态快照)
- **阶段**:M3——Alpha 版本的完整实现(2026-10-09 立规:未明令宣布完成前停留 M3,迭代只排 M3 需求,M4 不排期;根目录 stage.txt 为旧阶段开关仅存档)。
- **迭代位**:迭代 1–52 全部已完成;各迭代收口与批后收尾沿革查 git 提交信息与 `reports/`。
- **下一步**:无已编迭代在飞——待编池:改写族孤儿(待处置方向裁决)、二三关棋盘观感(待设计+待美术)、EC8 热重载实装(待用户另立条目);测试面真输入化分期与存量断言逐条精锚(待执行,归下批编排)。
- **迭代位**:迭代 1–53 全部已完成;各迭代收口与批后收尾沿革查 git 提交信息与 `reports/`。
- **下一步**:无已编迭代在飞——待编池:效果通道与事件词零现役普查(待写卡)、结算明细文案入表(待文档设计)、二三关棋盘观感(待设计+待美术)、测试面真输入化分期与存量断言逐条精锚(待执行)、QA 覆盖总览面板与自动试玩扩展(待执行);EC8 热重载实装待用户另立条目。
## 里程碑时间线
@@ -86,7 +86,7 @@
| 迭代 50 | 已完成(2026-11-03) | 已退役(git 历史可考) | 2026-11-03 | ≈12.875 个工作日〔用户特批 2026-11-03 超 10 个工作日〕 | 已退役(git 历史可考) |
| 迭代 51 | 已完成(2026-11-04) | 已退役(git 历史可考) | 2026-11-04 | ≈6.625 个工作日 | 已退役(git 历史可考) |
| 迭代 52 | 已完成(2026-11-05) | 已退役(git 历史可考) | 2026-11-04 | ≈9.275 个工作日 | 已退役(git 历史可考) |
| 迭代 53 | 待开工 | reports/需求卡片-卷轴视口批-2026-11-05.md<br>reports/需求卡片-改写族孤儿机制与退役名清退批-2026-11-05.md | 2026-11-05 | ≈6.0 个工作日 | — |
| 迭代 53 | 已完成(2026-11-06) | 已退役(git 历史可考) | 2026-11-05 | ≈6.0 个工作日 | 已退役(git 历史可考) |
- 历史行的「—」= 该列事实不可考:迭代 1~8 早于需求卡片制;编排时点与预计周期不再回溯,细节查 git 提交史。
- 「已退役(git 历史可考)」= 需求卡片或实现报告曾存在,已按「证据只留最新」退役;已完成迭代的实现报告列只填 reports/ 现存路径或本保留字,不填「—」。
+12 -2
View File
@@ -132,6 +132,16 @@ def product_funcs():
return names
def probe_funcs():
# 真实入口锚面补探针共用面(capability E 规则用):真输入合成沿(probe_ui_util 等探针
# 共用件)写进能力行真实入口格后,卡片退役其凭证只能锚代码面——探针件定义与 D 规则
# 同口径认锚(仍要求 func 真实存在,不属放宽)。
names = set()
for p in (REPO / "game" / "probe").rglob("*.gd"):
names.update(re.findall(r"^(?:static\s+)?func\s+([A-Za-z_]\w*)\s*\(", read(p), re.M))
return names
def cards_corpus():
"""reports/需求卡片*.md 全文拼接(双通道的「卡在」锚面);无卡返回空串。"""
if not CARDS_DIR.is_dir():
@@ -266,7 +276,7 @@ def check_entry(no, name, cell, funcs, cards):
for _, method in tokens:
if method not in funcs and not (cards and method in cards):
red(README.relative_to(REPO), no, "真实入口",
"「%s」方法 %s() 无凭证——产品码与 sim 测量件无定义且需求卡片未载" % (name[:16], method))
"「%s」方法 %s() 无凭证——产品码、sim 测量件与探针件均无定义且需求卡片未载" % (name[:16], method))
def check_coverage(cov_tables, names):
@@ -333,7 +343,7 @@ def main():
suites = manifest()
if not suites:
selfcheck.append("run_all.gd manifest 零套件")
funcs = product_funcs()
funcs = product_funcs() | probe_funcs()
if not funcs:
selfcheck.append("产品码零 `func`")
names, referenced = check_capability_rows(cap_tables, suites, funcs, cards_corpus())
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""coverage_enum.py — 本体枚举器(游戏内容↔测试验证对账,B 面板·义务层双源之机器源)
两个文本扫描器+一次比对,全程不依赖跑游戏(交互命中维度除外——由探针覆盖登记另行承载):
扫描器 1(游戏本体枚举)——「游戏有什么」:
game/data/tables/*.tres 行集主键(`"xxx_id": "yyy"` 形态,逐表逐行)
game/data/schemas/*.gd 词表常量与字段名(const 块内的引号 token+var 字段名)
game/presentation/**/*.tscn BaseButton 族节点(scene+节点名)
game/presentation/**/*.gd _gui_input/_input 自绘命中类(脚本名)
game/presentation/views/*.gd 屏与模态清单(*_screen/*_overlay/*_panel 视图类)
扫描器 2(验证关联)——「验证了什么」:复用 tools/extract_checks.py 的断言静态提取
(unit/**+integration/**+system_test.gd 全量),实体 id 在「断言标签+cond+源文件+函数」
文料中出现即记一次关联。
比对=差集:本体存在、验证零关联=缺口清单,按类别分组、标首见批次(本工具生成时间)。
诚实边界(契约 §7 在案):grep 关联有假阳假阴——断言测了实体但文本没带 id=假缺口(人工核销
兜);文本带 id 但断言弱=假覆盖(oracle 改造堵)。推荐约定「断言文本含被测实体 id」提高精度,
不立规约。交互点(按钮/物件被点过没有)不在本工具判据内——那归探针真输入覆盖登记。
用法: python tools/coverage_enum.py [--out <路径>]
产出: game/tests/.gates/coverage/ontology_gaps.js(JSONP,B 面板消费)
"""
from __future__ import annotations
import argparse
import datetime
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
GAME = ROOT / "game"
DEFAULT_OUT = GAME / "tests" / ".gates" / "coverage" / "ontology_gaps.js"
TRES_ID_RE = re.compile(r'"(\w+_id)":\s*"([^"]+)"')
SCHEMA_TOKEN_RE = re.compile(r'"([a-z][a-z0-9_]{2,40})"')
SCHEMA_VAR_RE = re.compile(r"^@?export\s+var\s+(\w+)\s*:", re.M)
TSCN_NODE_RE = re.compile(r'\[node name="([^"]+)"[^>]*type="(\w*Button\w*)"')
GUI_INPUT_RE = re.compile(r"^func (_gui_input|_input)\(", re.M)
VIEW_KIND_RE = re.compile(r"(screen|overlay|panel)$")
def scan_tables() -> list[dict]:
out: list[dict] = []
tables = GAME / "data" / "tables"
for f in sorted(tables.glob("*.tres")):
text = f.read_text(encoding="utf-8", errors="replace")
seen: set[tuple[str, str]] = set()
for m in TRES_ID_RE.finditer(text):
key = (m.group(1), m.group(2))
if key not in seen:
seen.add(key)
out.append({"category": "表行:%s" % f.stem, "id": m.group(2), "id_field": m.group(1)})
return out
def scan_schemas() -> list[dict]:
out: list[dict] = []
schemas = GAME / "data" / "schemas"
for f in sorted(schemas.glob("*.gd")):
text = f.read_text(encoding="utf-8", errors="replace")
tokens = set(SCHEMA_TOKEN_RE.findall(text))
fields = set(SCHEMA_VAR_RE.findall(text))
for t in sorted(tokens):
out.append({"category": "词表/枚值:%s" % f.stem, "id": t, "id_field": "token"})
for fl in sorted(fields):
out.append({"category": "schema字段:%s" % f.stem, "id": fl, "id_field": "field"})
return out
def scan_tscn_buttons() -> list[dict]:
out: list[dict] = []
for f in sorted(GAME.glob("presentation/**/*.tscn")):
text = f.read_text(encoding="utf-8", errors="replace")
for m in TSCN_NODE_RE.finditer(text):
out.append({"category": "场景按钮", "id": "%s:%s" % (f.name, m.group(1)), "id_field": m.group(2)})
return out
def scan_gui_input_classes() -> list[dict]:
out: list[dict] = []
for f in sorted(GAME.glob("presentation/**/*.gd")):
text = f.read_text(encoding="utf-8", errors="replace")
kinds = set(GUI_INPUT_RE.findall(text))
for k in kinds:
out.append({"category": "自绘命中类", "id": f.stem, "id_field": k})
break # 一类一行(一个脚本同具两面也只记一次)
return out
def scan_screens() -> list[dict]:
out: list[dict] = []
for f in sorted((GAME / "presentation" / "views").glob("*.gd")):
if VIEW_KIND_RE.search(f.stem):
out.append({"category": "屏与模态", "id": f.stem, "id_field": "view"})
return out
def assertion_corpus() -> tuple[list[dict], str]:
sys.path.insert(0, str(ROOT / "tools"))
import extract_checks as ec # noqa: PLC0415 —— 复用现役提取机制(契约 §7 指定扩面)
rows, _problems = ec.extract_all(ec.default_sources())
parts: list[str] = []
for r in rows:
parts.append("%s %s %s %s" % (r.get("src", ""), r.get("func", ""),
r.get("label_literal", ""), r.get("cond", "")))
return rows, "\n".join(parts)
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="本体枚举器(游戏内容↔测试验证对账)")
ap.add_argument("--out", default=str(DEFAULT_OUT))
args = ap.parse_args()
entities = scan_tables() + scan_schemas() + scan_tscn_buttons() + scan_gui_input_classes() + scan_screens()
_rows, corpus = assertion_corpus()
categories: dict[str, dict] = {}
for ent in entities:
cat = categories.setdefault(ent["category"], {"total": 0, "matched": 0, "gaps": []})
cat["total"] += 1
if ent["id"] and ent["id"] in corpus:
cat["matched"] += 1
else:
cat["gaps"].append({"id": ent["id"], "id_field": ent.get("id_field", "")})
payload = {
"meta": {
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
"entities_total": len(entities),
"corpus_rows": len(_rows),
"honest_boundary": "grep 关联有假阳假阴——文本没带 id 的假缺口人工核销;交互命中归探针真输入登记",
},
"categories": [
{"category": k, "total": v["total"], "matched": v["matched"], "gaps": v["gaps"]}
for k, v in sorted(categories.items())
],
}
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
js = "/* 本体对账清单——coverage_enum.py 随跑重生成,人工核销走豁免清单(coverage_exemptions.js),不落本文件。 */\n"
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
js += "window.__QA_COVERAGE__.ontology = %s;\n" % json.dumps(payload, ensure_ascii=False, indent=2)
out.write_text(js, encoding="utf-8", newline="\n")
gap_total = sum(len(c["gaps"]) for c in payload["categories"])
print("[ontology] 本体实体 %d / 断言文料行 %d / 缺口 %d(%d 类)→ %s" % (
len(entities), len(_rows), gap_total, len(payload["categories"]), out))
for c in payload["categories"]:
print(" %s:%d 实体 / %d 缺口" % (c["category"], c["total"], len(c["gaps"])))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+11
View File
@@ -0,0 +1,11 @@
/* 覆盖豁免清单——人工维护(S13 基线人工修订同款纪律;面板与工具只读本文件,禁代码生成)。
* 用途:登记「确认不需要真输入覆盖/不需要断言关联」的条目,B 面板对在册条目亮豁免灯、不亮缺口。
* 形态:abilities=能力清单行「能力」列原文(精确匹配);controls=控件登记键或其中缀(包含匹配);
* ontology=本体实体 id(精确匹配)。每条必须带 reason——无理由的豁免条目面板标红示警。 */
window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};
window.__QA_COVERAGE__.exemptions = {
"meta": {"note": "人工维护;改后刷新面板即生效,无生成步骤"},
"abilities": [],
"controls": [],
"ontology": []
};
+184
View File
@@ -0,0 +1,184 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""coverage_receipts.py — 六源回执明细化(B 覆盖总览面板·执行层)
各闸跑批把当场日志落 `.gates/*.log`;本工具在跑批末尾被闸壳调用一次,把六层日志解析成
「名×结果×计数」明细,按闸各写一个 JSONP .js 到 `.gates/coverage/`,供
tools/coverage_report.html 经 <script src> 直载。
六层与日志源(层键=面板消费键,层名=呈现名):
smoke 冒烟测试 smoke_boot.log 摘要「N 项检查,M 项失败」
unit 单元与集成测试 unit.log 每套件「[run_all] PASS/FAIL res://…(exit=N)」
system 系统测试 system_test.log 摘要「N 项检查,M 项失败」
probe 场景探针 probe.log 每探针段「======== 名(段)========」+该段 PASS/FAIL 行
render 渲染闸 render.log golden 清单与对比结果
static 静态扫描 static_scan.log 每个 S 系条目「[static_scan] S# ok/FAIL(…)」
判据口径:
- 明细粒度=「名×结果×计数」,不记逐条断言文本(契约 §4 成本口径);
- 日志缺失=该层 {"result": "not_run"}——面板亮灰灯,不冒充绿;
- 本工具失败不翻转闸判定(观测件非判定件),但必须打红字——静默丢数据等于断言蒸发。
用法: python tools/coverage_receipts.py [--stamp-dir <.gates 路径>]
产出: <stamp-dir>/coverage/gate_<层键>.js × 6
"""
from __future__ import annotations
import argparse
import datetime
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_STAMP = ROOT / "game" / "tests" / ".gates"
SUMMARY_RE = re.compile(r"摘要[::]\s*\[?[^\]]*\]?\s*摘要[::]\s*(\d+)\s*项检查,(\d+)\s*项失败")
SUITE_RE = re.compile(r"\[run_all\] (PASS|FAIL) (res://\S+?\.gd)(?:\s*\(exit=(-?\d+)\))?")
PROBE_SECTION_RE = re.compile(r"^======== (\S+)(?:\s+(.+?))? ========")
PROBE_RESULT_RE = re.compile(r"^\[[^\]]+\] (PASS|FAIL)")
GOLDEN_RE = re.compile(r"\[render_gate\] \[golden\] (PASS|FAIL)((\d+) 屏黄金图")
GOLDEN_CROP_RE = re.compile(r"\[render_gate\] \[golden_crop\] (\S+?) —")
STATIC_RE = re.compile(r"^\[static_scan\] (S\d+(?:/S\d+)*)\s+(ok|FAIL)", re.M)
STATIC_GDSTYLE_RE = re.compile(r"^\[gdstyle_gate\] (ok|FAIL)", re.M)
def read(stamp: Path, name: str) -> str:
p = stamp / name
if not p.is_file():
return ""
return p.read_text(encoding="utf-8", errors="replace")
def parse_summary_layer(log: str) -> dict:
if not log:
return {"result": "not_run"}
m = SUMMARY_RE.search(log)
verdict = "绿" if "[run_smoke] PASS" in log or "[run_system_test] PASS" in log else (
"红" if ("FAIL" in log or "摘要" in log and m and int(m.group(2)) > 0) else "灰")
if m:
return {"result": verdict, "checks": int(m.group(1)), "fails": int(m.group(2))}
return {"result": verdict}
def parse_unit(log: str) -> dict:
if not log:
return {"result": "not_run"}
items = [{"name": m.group(2), "result": m.group(1), "exit": int(m.group(3) or 0)}
for m in SUITE_RE.finditer(log)]
red = [i for i in items if i["result"] == "FAIL"]
verdict = "绿" if "[run_all] all green" in log and not red else ("红" if red or "FAIL" in log else "灰")
return {"result": verdict, "suites_total": len(items), "items": items}
def parse_probe(log: str) -> dict:
if not log:
return {"result": "not_run"}
items: list[dict] = []
cur: dict | None = None
for line in log.splitlines():
sm = PROBE_SECTION_RE.match(line.strip())
if sm:
seg = (sm.group(2) or "").strip()
cur = {"name": sm.group(1), "segment": seg, "result": "灰"}
items.append(cur)
continue
rm = PROBE_RESULT_RE.match(line.strip())
if rm and cur is not None and cur["result"] == "灰":
cur["result"] = rm.group(1)
judged = [i for i in items if i["result"] != "灰"]
verdict = "红" if (any(i["result"] == "FAIL" for i in judged) or "[probe_gate] 全绿" not in log) else "绿"
return {"result": verdict, "probes_total": len(judged), "items": items}
def parse_render(log: str) -> dict:
if not log:
return {"result": "not_run"}
items: list[dict] = []
gm = GOLDEN_RE.search(log)
if gm:
items.append({"name": "golden 逐像素对比", "result": gm.group(1), "screens": int(gm.group(2))})
screens: list[str] = []
for m in GOLDEN_CROP_RE.finditer(log):
if m.group(1) not in screens:
screens.append(m.group(1))
if screens:
items.append({"name": "golden 逐屏裁剪", "result": "PASS", "screens": screens})
verdict = "红" if "FAIL" in log or "[render_gate] 全绿" not in log else "绿"
return {"result": verdict, "items": items}
def parse_static(log: str) -> dict:
if not log:
return {"result": "not_run"}
items = [{"name": m.group(1), "result": m.group(2)} for m in STATIC_RE.finditer(log)]
gm = STATIC_GDSTYLE_RE.search(log)
if gm:
items.insert(0, {"name": "gdstyle", "result": gm.group(1)})
red = [i for i in items if i["result"] != "ok"]
verdict = "红" if red or "FAIL" in log else "绿"
return {"result": verdict, "items_total": len(items), "items": items}
LAYERS = [
("smoke", "冒烟测试", "smoke_boot.log", parse_summary_layer),
("unit", "单元与集成测试", "unit.log", parse_unit),
("system", "系统测试", "system_test.log", parse_summary_layer),
("probe", "场景探针", "probe.log", parse_probe),
("render", "渲染闸", "render.log", parse_render),
("static", "静态扫描", "static_scan.log", parse_static),
]
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="六源回执明细化(B 面板·执行层)")
ap.add_argument("--stamp-dir", default=str(DEFAULT_STAMP))
args = ap.parse_args()
stamp = Path(args.stamp_dir)
out_dir = stamp / "coverage"
out_dir.mkdir(parents=True, exist_ok=True)
failed = False
wrote = 0
for key, label, log_name, fn in LAYERS:
try:
detail = fn(read(stamp, log_name))
detail["meta"] = {
"layer": key,
"label": label,
"source_log": log_name,
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
}
payload = {"receipt": detail}
js = "/* 闸回执明细化——coverage_receipts.py 随每次跑批覆写,人工修订无效。 */\n"
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
js += "window.__QA_COVERAGE__.gate_receipts = window.__QA_COVERAGE__.gate_receipts || {};\n"
js += "window.__QA_COVERAGE__.gate_receipts[%s] = %s;\n" % (
json.dumps(key), json.dumps(payload, ensure_ascii=False, indent=2))
(out_dir / ("gate_%s.js" % key)).write_text(js, encoding="utf-8", newline="\n")
wrote += 1
print("[receipts] %s(%s)=%s" % (label, log_name, detail.get("result", "?")))
except Exception as exc: # noqa: BLE001 —— 观测件逐层容错,红字可见不静默
failed = True
print("[receipts] 红:%s 层解析失败(%s):%r" % (label, log_name, exc))
if wrote == 0:
print("[receipts] 红:六层明细一件未落盘——禁静默")
return 1
# 探针登记文件清单(B 面板 file:// 无法列目录——按现存 probe_hits_*.js 落 manifest 供 <script src>)
probe_files = sorted(p.name for p in out_dir.glob("probe_hits_*.js"))
manifest = {
"meta": {"generated_at": datetime.datetime.now().isoformat(timespec="seconds")},
"probes": probe_files,
}
mjs = "/* 探针登记清单——coverage_receipts.py 随跑批按现存文件覆写,勿手编。 */\n"
mjs += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
mjs += "window.__QA_COVERAGE__.probe_manifest = %s;\n" % json.dumps(manifest, ensure_ascii=False)
(out_dir / "probe_manifest.js").write_text(mjs, encoding="utf-8", newline="\n")
print("[receipts] 探针登记清单 %d 件 → probe_manifest.js" % len(probe_files))
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())
+226
View File
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>QA 覆盖总览面板——Dungeon Sweeper</title>
<style>
:root { --ok:#3a7d44; --bad:#a33; --off:#9aa; --na:#d5d5d0; --ex:#b8860b; --ink:#2c2a26; --paper:#f4f1ea; }
body { font-family: "Microsoft YaHei", sans-serif; margin: 0; background: var(--paper); color: var(--ink); }
header { padding: 14px 20px 10px; border-bottom: 3px solid var(--ink); background: #fff; }
h1 { font-size: 19px; margin: 0 0 4px; }
h2 { font-size: 16px; margin: 22px 0 6px; border-left: 5px solid var(--ink); padding-left: 8px; }
.note { font-size: 12px; color: #555; margin: 3px 0; }
.warn { color: var(--bad); font-weight: bold; }
main { padding: 12px 20px 40px; }
table { border-collapse: collapse; width: 100%; background: #fff; font-size: 12.5px; }
th, td { border: 1px solid #cfc9bd; padding: 4px 7px; text-align: left; vertical-align: top; }
th { background: #ebe6da; }
.lamp { display: inline-block; width: 13px; height: 13px; border-radius: 50%; margin-right: 3px; vertical-align: -2px; }
.ok { background: var(--ok); } .bad { background: var(--bad); }
.off { background: var(--off); } .na { background: var(--na); } .ex { background: #fff; border: 3px double var(--ex); }
.chip { display: inline-block; padding: 1px 8px; border-radius: 9px; color: #fff; font-size: 12px; }
details { margin: 4px 0 10px; } summary { cursor: pointer; font-size: 13px; }
.gaplist { font-size: 12px; color: #444; line-height: 1.7; }
.seg { margin-bottom: 18px; }
code { background: #eceade; padding: 0 3px; }
</style>
</head>
<body>
<header>
<h1>QA 覆盖总览面板 <span style="font-weight:normal;font-size:13px">(义务 × 执行合并视图——只读)</span></h1>
<div class="note">状态灯:绿=该层最近跑批过 红=跑批挂 灰=该层无承载/未跑 双圈=豁免(<code>tools/coverage_exemptions.js</code> 人工维护)。</div>
<div class="note" id="staleness"></div>
<div class="note"><b>完备性边界</b>:控件级视图只对「被探针/C 打开过的屏」负责;未打开过的屏由动线断言+C 探索兜底。
本体对账为文本 grep 关联——有假阳假阴(断言测了实体但文本没带 id=假缺口,人工核销;文本带 id 但断言弱=假覆盖),缺口须人工复核后再立事项。</div>
<div class="note">数据源:随最近一次闸批落盘于 <code>game/tests/.gates/coverage/</code>;刷新本页即读最新。</div>
</header>
<main>
<h2>一、六层闸况</h2>
<div id="gateSummary"></div>
<h2>二、能力清单 × 六层状态灯(按旅程段)</h2>
<div class="note">灯的判据:用例文件落哪层就钉哪层的回执(单元=逐套件 PASS/FAIL;探针/系统/冒烟/渲染/静态=层判据);该层无对应承载=灰。</div>
<div id="capabilityMatrix"></div>
<h2>三、控件级交互覆盖下钻(探针真输入登记)</h2>
<div class="note">交互源=探针真输入命中登记(push_input 管线命中);「存在但零命中」=控件缺口——hitbox 错位/遮挡/驱动空洞在此暴露。展示源=渲染闸 golden 逐屏登记。</div>
<div id="controlDrill"></div>
<h2>四、本体对账(游戏内容 ↔ 测试验证)</h2>
<div class="note">缺口=本体存在、断言文料零关联;若该 id 出现在探针命中登记则降级为「探针承载」。首见批次=本工具生成时间,逐批对比看新面孔。</div>
<div id="ontologyGaps"></div>
</main>
<script src="./coverage_exemptions.js"></script>
<script src="../game/tests/.gates/coverage/capability_list.js"></script>
<script src="../game/tests/.gates/coverage/gate_smoke.js"></script>
<script src="../game/tests/.gates/coverage/gate_unit.js"></script>
<script src="../game/tests/.gates/coverage/gate_system.js"></script>
<script src="../game/tests/.gates/coverage/gate_probe.js"></script>
<script src="../game/tests/.gates/coverage/gate_render.js"></script>
<script src="../game/tests/.gates/coverage/gate_static.js"></script>
<script src="../game/tests/.gates/coverage/probe_manifest.js"></script>
<script src="../game/tests/.gates/coverage/ontology_gaps.js"></script>
<script>
/* 探针登记文件由 manifest 同步补挂(document.write 在解析期阻塞加载,渲染段前数据已就位) */
(function () {
const probes = ((window.__QA_COVERAGE__ || {}).probe_manifest || {}).probes || [];
for (const p of probes) {
document.write('<script src="../game/tests/.gates/coverage/' + encodeURIComponent(p) + '"><\/script>');
}
})();
</script>
<script>
"use strict";
const C = window.__QA_COVERAGE__ || {};
function esc(s) { return String(s == null ? "" : s).replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c])); }
function lamp(cls, title) { return `<span class="lamp ${cls}" title="${esc(title)}"></span>`; }
function chip(txt, cls) { return `<span class="chip" style="background:${cls}">${esc(txt)}</span>`; }
const VCOLOR = {"绿":"var(--ok)","红":"var(--bad)","灰":"var(--off)","not_run":"var(--off)"};
// ── 数据新鲜度(陈旧可见——防静默旧数据) ──
(function () {
const stamps = [];
const cl = (C.capability_list || {}).meta; if (cl) stamps.push(`义务层 ${cl.generated_at}`);
const rc = C.gate_receipts || {};
for (const k in rc) { const m = (rc[k].receipt || {}).meta; if (m) stamps.push(`${m.label} ${m.generated_at}`); }
const on = (C.ontology || {}).meta; if (on) stamps.push(`本体 ${on.generated_at}`);
const pm = (C.probe_manifest || {}).meta; if (pm) stamps.push(`探针登记 ${pm.generated_at}`);
document.getElementById("staleness").innerHTML = "数据时间:" + stamps.map(esc).join(" · ");
})();
// ── 一、六层闸况 ──
(function () {
const rc = C.gate_receipts || {};
const ORDER = ["smoke", "unit", "system", "probe", "render", "static"];
const LABEL = {smoke:"冒烟测试", unit:"单元与集成测试", system:"系统测试", probe:"场景探针", render:"渲染闸", static:"静态扫描"};
let html = "<table><tr><th>层</th><th>判定</th><th>明细</th></tr>";
for (const k of ORDER) {
const r = (rc[k] || {}).receipt;
if (!r) { html += `<tr><td>${LABEL[k]}</td><td>${chip("缺数据", "var(--off)")}</td><td>—</td></tr>`; continue; }
const v = r.result === "not_run" ? "未跑" : r.result;
const color = VCOLOR[r.result] || "var(--off)";
let detail = `共 ${r.items ? r.items.length : 0} 项`;
if (r.checks != null) detail = `${r.checks} 项检查 / ${r.fails} 失败`;
if (r.suites_total != null) detail = `${r.suites_total} 套件`;
if (r.probes_total != null) detail = `${r.probes_total} 探针`;
html += `<tr><td>${LABEL[k]}</td><td>${chip(v, color)}</td><td>${esc(detail)} <span class="note">源=${esc(r.meta.source_log)} @ ${esc(r.meta.generated_at)}</span></td></tr>`;
}
html += "</table><details><summary>单元与集成逐套件明细</summary><div class='gaplist'>";
const unit = ((rc.unit || {}).receipt || {}).items || [];
for (const it of unit) {
html += `${lamp(it.result === "PASS" ? "ok" : "bad", it.result)} ${esc(it.name.replace("res://tests/", ""))}${it.exit ? "(exit=" + esc(it.exit) + ")" : ""}<br>`;
}
html += "</div></details><details><summary>场景探针逐段明细</summary><div class='gaplist'>";
const pr = ((rc.probe || {}).receipt || {}).items || [];
for (const it of pr) {
html += `${lamp(it.result === "PASS" ? "ok" : (it.result === "FAIL" ? "bad" : "off"), it.result)} ${esc(it.name)}${it.segment ? "(" + esc(it.segment) + ")" : ""}<br>`;
}
html += "</div></details><details><summary>静态扫描逐条明细</summary><div class='gaplist'>";
const st = ((rc.static || {}).receipt || {}).items || [];
for (const it of st) {
html += `${lamp(it.result === "ok" ? "ok" : "bad", it.result)} ${esc(it.name)}<br>`;
}
html += "</div></details>";
document.getElementById("gateSummary").innerHTML = html;
})();
// ── 二、能力清单 × 六层灯 ──
(function () {
const cl = C.capability_list;
if (!cl || !cl.segments) { document.getElementById("capabilityMatrix").innerHTML = "<p class='warn'>义务层缺数据——先跑 tools/extract_capability_list.py</p>"; return; }
const rc = C.gate_receipts || {};
const ex = ((C.exemptions || {}).abilities || []).map(s => String(s));
function layerOf(caseFile) {
if (/system_test/.test(caseFile)) return "system";
if (/probe\//.test(caseFile) || /_probe\.gd/.test(caseFile)) return "probe";
if (/golden|render/.test(caseFile)) return "render";
return "unit";
}
function rowLight(row) {
if (ex.includes(row.ability)) return lamp("ex", "豁免(coverage_exemptions.js 在册)");
if (!row.cases || !row.cases.length) return lamp("off", "无代表用例指针");
let worst = null; const seenLayers = new Set();
for (const c of row.cases) {
const layer = layerOf(c);
seenLayers.add(layer);
const r = (rc[layer] || {}).receipt;
if (!r || r.result === "not_run") { worst = worst || "na"; continue; }
if (r.result === "红") return lamp("bad", `${layer} 层跑批挂`);
if (layer === "unit") {
const items = r.items || [];
const hit = items.filter(it => c.indexOf(it.name.replace("res://tests/", "")) >= 0 || it.name.indexOf(c.split(":")[0].replace(".gd", "")) >= 0);
if (!hit.length) { worst = worst || "off"; continue; }
if (hit.some(it => it.result !== "PASS")) return lamp("bad", "代表套件红:" + c);
worst = worst || "ok";
} else {
worst = worst || (r.result === "绿" ? "ok" : "off");
}
}
if (worst === "ok") return lamp("ok", "承载层绿");
if (worst === "na") return lamp("na", "承载层本批未跑");
return lamp("off", "该层无对应承载(灰)");
}
let html = "";
for (const seg of cl.segments) {
if (!seg.rows.length) continue;
html += `<div class="seg"><h3 style="font-size:14px;margin:8px 0 4px">${esc(seg.segment)}(${seg.rows.length} 行)</h3>`;
html += "<table><tr><th style='width:32%'>能力</th><th>系统</th><th style='width:14%'>灯</th><th style='width:26%'>代表用例</th></tr>";
for (const row of seg.rows) {
html += `<tr><td>${esc(row.ability)}</td><td>${esc(row.system)}</td><td>${rowLight(row)}</td><td>${row.cases.map(esc).join("<br>")}</td></tr>`;
}
html += "</table></div>";
}
document.getElementById("capabilityMatrix").innerHTML = html;
})();
// ── 三、控件级下钻 ──
(function () {
const pm = C.probe_manifest || {probes: []};
const ex = ((C.exemptions || {}).controls || []).map(s => String(s));
const rd = ((C.gate_receipts || {}).render || {}).receipt || {};
const renderScreens = [];
for (const it of (rd.items || [])) if (it.screens instanceof Array) renderScreens.push(...it.screens);
let html = "";
if (renderScreens.length) {
html += `<p class="note"><b>渲染展示登记(golden 逐屏)</b>:${renderScreens.map(esc).join("、")}</p>`;
}
for (const pf of pm.probes) {
const name = pf.replace(/^probe_hits_/, "").replace(/\.js$/, "");
// 同名重复 <script> 无法动态注入——manifest 落盘时同步内联一份数据入口由面板经 window 取
const data = (window.__QA_COVERAGE__.probe_real_input || {})[name];
if (!data) { html += `<p class="warn">${esc(name)}:登记文件未加载(检查 script 标签与 manifest)</p>`; continue; }
const hits = data.hits || {};
const seen = data.seen || [];
const gaps = seen.filter(k => !(k in hits) && !ex.some(e => k.indexOf(e) >= 0));
html += `<details open><summary><b>${esc(name)}</b>:命中 ${Object.keys(hits).length} 控件 / 存在 ${seen.length} 控件 / 零命中缺口 ${gaps.length}(@ ${esc(data.meta.generated_at)})</summary><div class='gaplist'>`;
for (const g of gaps) html += `${lamp("off", "存在但零真输入命中")} ${esc(g)}<br>`;
html += "<b>已命中:</b><br>";
for (const k of Object.keys(hits).sort()) html += `${lamp("ok", "真输入命中 " + hits[k] + " 次")} ${esc(k)}×${hits[k]}<br>`;
html += "</div></details>";
}
document.getElementById("controlDrill").innerHTML = html || "<p class='warn'>探针登记缺数据——先跑探针与 tools/coverage_receipts.py</p>";
})();
// ── 四、本体对账 ──
(function () {
const on = C.ontology;
if (!on || !on.categories) { document.getElementById("ontologyGaps").innerHTML = "<p class='warn'>本体数据缺——先跑 tools/coverage_enum.py</p>"; return; }
const ex = ((C.exemptions || {}).ontology || []).map(s => String(s));
const hitTexts = [];
const pri = (C.probe_real_input || {});
for (const k in pri) for (const hk of Object.keys(pri[k].hits || {})) hitTexts.push(hk);
let html = `<p class="note">实体 ${esc(on.meta.entities_total)} · 断言文料 ${esc(on.meta.corpus_rows)} 行 · 首见批次 ${esc(on.meta.generated_at)}</p>`;
for (const cat of on.categories) {
const shown = cat.gaps.filter(g => !ex.includes(g.id));
const probed = shown.filter(g => hitTexts.some(h => h.indexOf(g.id) >= 0));
const pure = shown.filter(g => !probed.includes(g));
html += `<details><summary><b>${esc(cat.category)}</b>:${cat.total} 实体 / 关联 ${cat.matched} / 缺口 ${shown.length}(其中探针承载 ${probed.length})</summary><div class='gaplist'>`;
for (const g of probed) html += `${lamp("ok", "探针真输入命中承载")} ${esc(g.id)} <span class="note">(断言文料零关联——交互覆盖由探针承载)</span><br>`;
for (const g of pure) html += `${lamp("off", "验证零关联缺口——人工复核后再立事项")} ${esc(g.id)}<br>`;
html += "</div></details>";
}
document.getElementById("ontologyGaps").innerHTML = html;
})();
</script>
</body>
</html>
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""extract_capability_list.py — 能力清单义务层提取器(B 覆盖总览面板·义务层)
把 game/tests/README.md「能力清单」的四列表(能力|系统|真实入口|代表用例)按旅程段
解析成机器可读 JSONP .js,供 tools/coverage_report.html 经 <script src> 直载
(file:// 下 fetch 本地 json 被 CORS 拦——JSONP 是 B 面板全数据源的统一形态)。
用法:
python tools/extract_capability_list.py [--readme <路径>] [--out <路径>]
判据口径:
- 只认表头恰为「能力|系统|真实入口|代表用例」的四列表(覆盖域表等五列表不取);
- 旅程段标题形态=「**旅程段N:名称**」,段名进每行的 segment 字段;
- 代表用例列按「;」切分成用例指针数组(`文件:函数` 形态逐字保真);
- 本工具是义务层的静态投影,产出随 README 变更重跑即得——面板只读本产物,禁在面板里改义务。
产出(JSONP):
window.__QA_COVERAGE__.capability_list = {
"meta": {"generated_at", "source", "rows_total", "segments_total"},
"segments": [{"segment": 名称, "rows": [{"ability", "system", "entry", "cases": []}]}],
}
"""
from __future__ import annotations
import argparse
import datetime
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_README = ROOT / "game" / "tests" / "README.md"
DEFAULT_OUT = ROOT / "game" / "tests" / ".gates" / "coverage" / "capability_list.js"
SEGMENT_RE = re.compile(r"^\*\*旅程段[一二三四五六七八九十]+[::](.+?)\*\*\s*$")
def split_row(line: str) -> list[str]:
body = line.strip()
if body.startswith("|"):
body = body[1:]
if body.endswith("|"):
body = body[:-1]
return [c.strip() for c in body.split("|")]
def parse(readme: Path) -> dict:
lines = readme.read_text(encoding="utf-8").splitlines()
segments: list[dict] = []
cur_seg: str | None = None
cur_rows: list[dict] | None = None
in_capability_table = False
for line in lines:
seg = SEGMENT_RE.match(line.strip())
if seg:
if cur_seg is not None:
segments.append({"segment": cur_seg, "rows": cur_rows or []})
cur_seg = seg.group(1).strip()
cur_rows = []
in_capability_table = False
continue
if cur_seg is None:
continue
if not line.strip().startswith("|"):
in_capability_table = False
continue
cells = split_row(line)
if all(not set(c) - {"-", " ", ":"} for c in cells) and cells:
continue # 表头分隔行 |---|---|
if (len(cells) == 4 and cells[0] == "能力" and cells[1] == "系统"
and cells[2] == "真实入口" and cells[3].startswith("代表用例")):
in_capability_table = True
continue
if in_capability_table and len(cells) == 4:
ability, system, entry, cases = cells
cur_rows.append({
"ability": ability.strip("`"),
"system": system,
"entry": entry,
"cases": [c.strip().strip("`") for c in cases.split(";") if c.strip()],
})
if cur_seg is not None:
segments.append({"segment": cur_seg, "rows": cur_rows or []})
rows_total = sum(len(s["rows"]) for s in segments)
return {
"meta": {
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
"source": str(readme),
"rows_total": rows_total,
"segments_total": len(segments),
},
"segments": segments,
}
def dump_js(payload: dict, out: Path) -> None:
out.parent.mkdir(parents=True, exist_ok=True)
js = "/* 能力清单义务层投影——由 extract_capability_list.py 生成,随 README 变更重跑;人工修订无效(义务源=README 能力清单)。 */\n"
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
js += "window.__QA_COVERAGE__.capability_list = %s;\n" % json.dumps(payload, ensure_ascii=False, indent=2)
out.write_text(js, encoding="utf-8", newline="\n")
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="能力清单义务层提取(B 面板·义务层)")
ap.add_argument("--readme", default=str(DEFAULT_README))
ap.add_argument("--out", default=str(DEFAULT_OUT))
args = ap.parse_args()
readme = Path(args.readme)
if not readme.is_file():
print("[capability] README 不存在:%s" % readme)
return 1
payload = parse(readme)
dump_js(payload, Path(args.out))
print("[capability] 义务层投影已落盘 %s(旅程段 %d / 能力行 %d)" % (
args.out, payload["meta"]["segments_total"], payload["meta"]["rows_total"]))
if payload["meta"]["rows_total"] == 0:
print("[capability] 红:能力行数为 0——表头或旅程段标题形态不匹配,禁当空义务面用")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+465
View File
@@ -0,0 +1,465 @@
# 三关色相 × 五组微光互辨抽测——S1 取铺底值(机检 D)+ S2 出并列样图(机检 A/B/C)。
# 执行依据:reports/提案-三关色相互辨抽测方案-2026-10-29.md(抽测任务=待办清单
# 「三关色相 × 五组微光互辨抽测」条)。产物:并列样图(artwork/,不入 git)+逐对读数表(reports/)。
# 口径出处(冲突时以 owner 为准):
# 微光色值与呈现参数 = game/presentation/views/ui_kit.gd TG_GLOW_*/TYPE_GLOW_*(v2 冻结);
# 合成公式 = game/presentation/views/cell_type_glow.gdshader(blend_mix 距离场柔光);
# 带域/格心取样 = game/probe/type_glow_shot_probe.gd(de<格边×0.125 / 内缩 20%);
# 准入门值(亮度地板/饱和上限/禁入色相带/近色容差)单点在本件 FLOOR_L·CEIL_SAT·
# BANNED_HUE·NEAR_EPS;art-bible §4.1 只述「未开格底与微光存在相对亮度关系」并指路本件。
# 既有色板比对池 = game/tests/golden/palette_baseline/ 两张条带并集(palette_check.py 同源)。
# 方案 §2.3 机检 C 口径订正:模型 0.49/0.51 是反解近似(ui_kit.gd 头注「模型域与实测的差
# = shader 距离场权重非扁平混合」),比较对象改为 shader 精确合成模型(逐像素复刻),判据线
# ±2/255 沿用在役值。
# 方案 §7 表二行数笔误订正:表二实为 90 行(3 关 × 3 底型 × 10 组对),非 30 行。
#
# 用法:
# python tools/glow_discrimination_sheet.py --art <artwork/mockups 目录> [--no-sheet]
# 输出:
# <art>/glow-discrimination-sheet-2026-11-05.png(样图:正图段+灰度副本段+图例)
# reports/读数-三关色相互辨抽测-2026-11-05.md(逐对表 45 行+组间表 90 行+机检汇总)
import argparse
import math
import os
import sys
from PIL import Image
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# ── ui_kit.gd 冻结常量(逐字转写,禁改值)──
GLOWS = [
("BUFF", (247, 223, 67)), # 通用增益·芥末黄
("HEAL", (104, 208, 66)), # 治疗·黄绿
("PIERCE", (48, 166, 166)), # 穿刺·青
("MAGIC", (123, 91, 214)), # 魔法·紫罗兰
("AOE", (166, 29, 55)), # 群伤·绯红
]
GLOW_ALPHA = 170.0 / 255.0
BAND_FRAC = 0.125
SOFT = 0.55
CORNER = 2.0
CELL = 45 # D 区实比格边
BAND = CELL * BAND_FRAC # 5.625
CORE_PAD = int(CELL * 0.20) # 格心内缩(探针 CORE_PAD_FRAC=0.20)
# ── 铺底准入门值单点(文档侧只述「未开格底与微光存在相对亮度关系」,数值归本件)──
FLOOR_L = 0.30 # 未开格底相对亮度地板(线性域)
CEIL_SAT = 0.55 # 饱和度上限
BANNED_HUE = (300.0, 345.0) # 禁入色相带(Boss 紫赤独占)
NEAR_EPS = 8 # 近色容差(三通道差全 <此值 即近色)
B_MIN_CH_DIFF = 8 # 机检 B|格底-微光可辨底线(三通道至少一通道 ≥此值)
C_MODEL_TOL = 2 # 机检 C|样图与合成模型容差(±此值 /255)
BOARD_TYPES = ["dungeon", "underdark", "deep_underdark"]
BASE_VARIANTS = [ # (底型名,瓦片件名后缀)——素砖 + 两种装饰变体(高墙瓦片词表在册件)
("素砖", "terrain_wall_center"),
("裂纹", "terrain_wall_center_crack"),
("碎石斑", "terrain_wall_center_speckle"),
]
TILE_DIR = os.path.join(REPO, "game", "assets", "art", "board")
BASELINE_DIR = os.path.join(REPO, "game", "tests", "golden", "palette_baseline")
CHECKS = []
def check(cid, name, ok, detail=""):
CHECKS.append((cid, name, bool(ok), detail))
print("[%s] %s %s — %s%s" % ("PASS" if ok else "FAIL", cid, name, "✓" if ok else "✗",
(";" + detail) if detail else ""))
# ── 色彩数学 ──
def to_linear(v):
c = v / 255.0
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
def rel_lum(rgb):
return 0.2126 * to_linear(rgb[0]) + 0.7152 * to_linear(rgb[1]) + 0.0722 * to_linear(rgb[2])
def gray709(rgb):
return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]
def hsv(rgb):
r, g, b = [v / 255.0 for v in rgb]
mx, mn = max(r, g, b), min(r, g, b)
d = mx - mn
if d == 0:
h = 0.0
elif mx == r:
h = 60 * ((g - b) / d % 6)
elif mx == g:
h = 60 * ((b - r) / d + 2)
else:
h = 60 * ((r - g) / d + 4)
s = 0.0 if mx == 0 else d / mx
return h % 360, s
def wcag(c1, c2):
l1, l2 = rel_lum(c1), rel_lum(c2)
return (max(l1, l2) + 0.05) / (min(l1, l2) + 0.05)
def smoothstep(e0, e1, x):
t = max(0.0, min(1.0, (x - e0) / (e1 - e0)))
return t * t * (3.0 - 2.0 * t)
def glow_weight(px, py):
"""shader 复刻:像素中心 (px,py) 的微光权重 = glow_alpha × a(d) × mask。"""
h = CELL / 2.0
d = min(px, py, CELL - px, CELL - py)
r = min(CORNER, CELL * 0.25)
qx = abs(px - h) - (h - r)
qy = abs(py - h) - (h - r)
outside = math.hypot(max(qx, 0.0), max(qy, 0.0)) + min(max(qx, qy), 0.0) - r
mask = 1.0 - smoothstep(-1.0, 0.5, outside)
a = 0.0
if d < BAND:
a = 1.0 - smoothstep(SOFT, 1.0, d / BAND)
return GLOW_ALPHA * a * mask
# ── 素材装载 ──
def load_tiles():
tiles = {}
for bt in BOARD_TYPES:
for vn, stem in BASE_VARIANTS:
path = os.path.join(TILE_DIR, "%s_%s.png" % (stem, bt))
if not os.path.exists(path):
print("[阻塞] 素材缺位:%s" % path, file=sys.stderr)
sys.exit(2)
img = Image.open(path).convert("RGB")
if img.size != (CELL, CELL):
img = img.resize((CELL, CELL), Image.NEAREST)
tiles[(bt, vn)] = img.load()
return tiles
def lit_slots(tiles):
"""铺底代表值 = 该关三块底型瓦片像素里线性亮度最大的实测色(顶面受光档)。
零表值:不引用任何色表常量,素材换色后读数自动跟随。"""
slots = {}
for bt in BOARD_TYPES:
best = None
for vn, _ in BASE_VARIANTS:
px = tiles[(bt, vn)]
for y in range(CELL):
for x in range(CELL):
c = px[x, y]
if best is None or rel_lum(c) > rel_lum(best):
best = c
slots[bt] = best
return slots
# ── 带域/格心取样(探针口径)──
def band_pixels():
"""带域像素集(整数格坐标 de < 格边×0.125,type_glow_shot_probe 口径)。"""
return [(x, y) for x in range(CELL) for y in range(CELL)
if min(x, y, CELL - 1 - x, CELL - 1 - y) < BAND]
def core_pixels():
"""格心像素集(内缩 20%)。"""
return [(x, y) for x in range(CELL) for y in range(CELL)
if min(x, y, CELL - 1 - x, CELL - 1 - y) >= CELL * 0.20]
BAND_PX = band_pixels()
CORE_PX = core_pixels()
def mean_rgb(px, pts):
n = len(pts)
return tuple(round(sum(px[x, y][i] for x, y in pts) / n) for i in range(3))
def model_band_mean(base_px, glow_rgb):
"""shader 精确合成模型:逐像素 blend_mix 理论带内均值(独立实现路径)。"""
n = len(BAND_PX)
acc = [0.0, 0.0, 0.0]
for x, y in BAND_PX:
w = glow_weight(x + 0.5, y + 0.5)
b = base_px[x, y]
for i in range(3):
acc[i] += w * glow_rgb[i] + (1.0 - w) * b[i]
return tuple(round(v / n) for v in acc)
# ── 机检 D:铺底代表值五门 ──
def load_baseline_palette():
cols = set()
for fn in ["ui_tokens.png", "art_snapshot.png"]:
p = os.path.join(BASELINE_DIR, fn)
img = Image.open(p).convert("RGB")
for x in range(img.width):
for y in range(img.height):
cols.add(img.getpixel((x, y)))
return sorted(cols)
def gate_d(tiles):
baseline = load_baseline_palette()
lit = lit_slots(tiles)
rows = []
all_pass = True
for bt in BOARD_TYPES:
rgb = lit[bt]
h, s = hsv(rgb)
lin = rel_lum(rgb)
l_ok = lin >= FLOOR_L
s_ok = s <= CEIL_SAT
hue_ok = not (BANNED_HUE[0] <= h <= BANNED_HUE[1])
# 近色门:三值=在役素材色(已在基线池内),palette_check 双门②只判新增色——报读数不判红
near = [b for b in baseline if all(abs(rgb[i] - b[i]) < NEAR_EPS for i in range(3))]
ok = l_ok and s_ok and hue_ok
all_pass = all_pass and ok
rows.append((bt, rgb, lin, s, h, len(near), ok))
check("D-" + bt, "铺底代表值门(底型瓦片实测顶面受光档;亮度/饱和/禁入带,近色只报读数)", ok,
"RGB=%s L=%.3f 饱和=%.3f 色相=%.1f° 近色对=%d(在役素材色,非新增)%s%s%s"
% (rgb, lin, s, h, len(near),
"" if l_ok else " [L红]", "" if s_ok else " [饱和红]",
"" if hue_ok else " [禁入带红]"))
# 语义检查=设计面人工核对(palette_check 同口径),打印读数不判红
print(" [语义] %s=%s 环境域(不占红/金/蓝/紫赤/绿语义段)——人工核对" % (bt, rgb))
# 渲染面带域均值 = 读数不判红(方案机检 D 只判槽值;本读数供观感定性参考)
for bt in BOARD_TYPES:
for vn, _ in BASE_VARIANTS:
m = mean_rgb(tiles[(bt, vn)], BAND_PX)
lin = rel_lum(m)
print(" [读数] 渲染面带域均值 %s/%s RGB=%s L=%.3f(不判红)" % (bt, vn, m, lin))
return all_pass, rows
# ── 样图绘制 ──
PAGE_W, PAGE_H = 640, 1120
MARGIN, HEAD, BLOCK_TITLE, GAP, LEGEND = 10, 60, 20, 30, 80
def draw_sheet(out_path):
import argparse # noqa: F401
from PIL import ImageDraw, ImageFont
img = Image.new("RGB", (PAGE_W, PAGE_H), (255, 255, 255))
d = ImageDraw.Draw(img)
try:
f_zh = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 13)
f_zh_s = ImageFont.truetype("C:/Windows/Fonts/msyh.ttc", 11)
except OSError:
f_zh = ImageFont.load_default()
f_zh_s = f_zh
blocks = []
y = MARGIN + HEAD
for bt in BOARD_TYPES:
d.text((MARGIN + 4, y + 3), "%s —— 列0=基准格,列1–5=五组微光(阶梯序)" % bt, fill=(0, 0, 0), font=f_zh)
y += BLOCK_TITLE
blocks.append((bt, y))
y += 3 * CELL
tiles = load_tiles()
# 画正图段 54 格
cell_pngs = {}
for bt, y0 in blocks:
for ri, (vn, _) in enumerate(BASE_VARIANTS):
for ci, gname in enumerate(["BASE"] + [g[0] for g in GLOWS]):
base = tiles[(bt, vn)]
cell = Image.new("RGB", (CELL, CELL))
cp = cell.load()
if gname == "BASE":
for x in range(CELL):
for yy in range(CELL):
cp[x, yy] = base[x, yy]
else:
glow = dict(GLOWS)[gname]
for x in range(CELL):
for yy in range(CELL):
w = glow_weight(x + 0.5, yy + 0.5)
b = base[x, yy]
cp[x, yy] = tuple(round(w * glow[i] + (1 - w) * b[i]) for i in range(3))
gx = MARGIN + 170 + ci * CELL
gy = y0 + ri * CELL
img.paste(cell, (gx, gy))
cell_pngs[(bt, vn, gname, "main")] = (gx, gy)
# 行标注
for ri, (vn, _) in enumerate(BASE_VARIANTS):
d.text((MARGIN, y0 + ri * CELL + 16), vn, fill=(0, 0, 0), font=f_zh_s)
# 灰度副本段(等幅面,正图段整段 Rec.709 去色)
main_top = MARGIN + HEAD
main_bot = y
gray_top = main_bot + GAP
for xx in range(MARGIN, PAGE_W - MARGIN):
for yy in range(main_top, main_bot):
r, g, b = img.getpixel((xx, yy))
v = int(round(gray709((r, g, b))))
img.putpixel((xx, gray_top + (yy - main_top)), (v, v, v))
d.text((MARGIN + 4, gray_top - 2), "灰度副本段(Rec.709 去色——色弱通道目检面)", fill=(80, 80, 80), font=f_zh_s)
# 图例段
leg_y = gray_top + (main_bot - main_top) + 10
d.text((MARGIN + 4, leg_y), "图例(五组冻结色值 ui_kit.gd TG_GLOW_*;阶梯序=合成空间亮度递减)", fill=(0, 0, 0), font=f_zh_s)
for i, (gn, rgb) in enumerate(GLOWS):
x0 = MARGIN + 170 + i * (CELL + 4)
d.rectangle([x0, leg_y + 18, x0 + CELL, leg_y + 18 + 28], fill=rgb, outline=(0, 0, 0))
d.text((x0, leg_y + 48), gn, fill=(0, 0, 0), font=f_zh_s)
img.save(out_path)
return img, cell_pngs, tiles, blocks, gray_top, main_top, main_bot
# ── 机检 A/B/C + 灰度/净空机检 ──
def run_checks(img, cell_pngs, tiles, blocks, gray_top, main_top, main_bot):
sheet_px = img.load()
table1 = [] # 45 行逐对表
table2 = [] # 90 行组间表
band_means = {}
fail_b, fail_c = [], []
lit = lit_slots(tiles)
for bt, y0 in blocks:
for vn, _ in BASE_VARIANTS:
# 列 0 基准格带域均值 = 该底型的模型底值
gx0, gy0 = cell_pngs[(bt, vn, "BASE", "main")]
band_base = mean_rgb(tiles[(bt, vn)], BAND_PX)
for gi, (gname, glow) in enumerate(GLOWS):
gx, gy = cell_pngs[(bt, vn, gname, "main")]
px = tiles[(bt, vn)]
# 样图带内均值(样图渲染面读数)
n = len(BAND_PX)
acc = [0.0, 0.0, 0.0]
for x, yy in BAND_PX:
p = sheet_px[gx + x, gy + yy]
for i in range(3):
acc[i] += p[i]
band_mean = tuple(round(v / n) for v in acc)
core_mean = mean_rgb(px, CORE_PX)
model = model_band_mean(px, glow)
diff_model = max(abs(band_mean[i] - model[i]) for i in range(3))
ch_diff = [abs(band_mean[i] - core_mean[i]) for i in range(3)]
b_ok = max(ch_diff) >= B_MIN_CH_DIFF
c_ok = diff_model <= C_MODEL_TOL
if not b_ok:
fail_b.append("%s/%s/%s" % (bt, vn, gname))
if not c_ok:
fail_c.append("%s/%s/%s" % (bt, vn, gname))
hue_gap = abs(hsv(lit[bt])[0] - hsv(glow)[0])
hue_gap = min(hue_gap, 360 - hue_gap)
table1.append((bt, vn, gname, band_mean, core_mean, model,
diff_model, ch_diff, hue_gap, b_ok and c_ok))
band_means[(bt, vn, gname)] = band_mean
# 机检 A:组间表 90 行
for bt, y0 in blocks:
for vn, _ in BASE_VARIANTS:
for i in range(len(GLOWS)):
for j in range(i + 1, len(GLOWS)):
a, b = band_means[(bt, vn, GLOWS[i][0])], band_means[(bt, vn, GLOWS[j][0])]
cr = wcag(a, b)
ok = cr >= 1.20
table2.append((bt, vn, "%s/%s" % (GLOWS[i][0], GLOWS[j][0]),
cr, gray709(a), gray709(b), ok))
stair_ok = True
for bt, y0 in blocks:
for vn, _ in BASE_VARIANTS:
seq = [gray709(band_means[(bt, vn, GLOWS[k][0])]) for k in range(len(GLOWS))]
mono = all(seq[k] > seq[k + 1] for k in range(len(seq) - 1))
stair_ok = stair_ok and mono
check("A-stair-%s-%s" % (bt, vn[:4]), "明度阶梯单调递减", mono,
"→".join("%.3f" % v for v in seq))
min_cr = min(t[3] for t in table2)
worst = min(table2, key=lambda t: t[3])
check("A-CR", "组间合成对比最弱对 ≥1.20(90 对)", min_cr >= 1.20,
"最弱 %s/%s CR=%.3f" % (worst[0], worst[2], min_cr))
check("B", "格底-微光可辨底线(45 行三通道差不全<8)", len(fail_b) == 0,
"违例 %d 行:%s" % (len(fail_b), ", ".join(fail_b[:6]) or "无"))
check("C", "绘制参数校准(45 行模型差≤±2/255)", len(fail_c) == 0,
"违例 %d 行:%s" % (len(fail_c), ", ".join(fail_c[:6]) or "无"))
# 灰度段零彩色像素
color_hit = 0
for xx in range(MARGIN, PAGE_W - MARGIN):
for yy in range(gray_top + BLOCK_TITLE, gray_top + (main_bot - main_top)):
r, g, b = sheet_px[xx, yy]
if not (r == g == b):
color_hit += 1
check("GRAY", "灰度副本段零彩色像素", color_hit == 0, "命中 %d" % color_hit)
# 四缘净空(A5 同款)
edge_hit = 0
for xx in range(PAGE_W):
for yy in range(PAGE_H):
if xx < MARGIN or xx >= PAGE_W - MARGIN or yy < MARGIN or yy >= PAGE_H - MARGIN:
if sheet_px[xx, yy] != (255, 255, 255):
edge_hit += 1
check("EDGE", "四缘 10px 带内零内容像素", edge_hit == 0, "命中 %d" % edge_hit)
return table1, table2
def write_report(table1, table2, d_rows):
rep = os.path.join(REPO, "reports", "读数-三关色相互辨抽测-2026-11-05.md")
lines = [
"# 读数:三关色相 × 五组微光互辨抽测(2026-11-05)",
"",
"> 接收方:待办清单「三关色相 × 五组微光互辨抽测——三关图类型色板终值冻结的前置抽测」条。",
"> 执行依据:reports/提案-三关色相互辨抽测方案-2026-10-29.md(S1/S2 步)。",
"> 样图:artwork/mockups/glow-discrimination-sheet-2026-11-05.png(不入 git)。",
"",
"## 机检汇总",
"",
"| 项 | 判定 | 说明 |",
"|---|---|---|",
]
for cid, name, ok, detail in CHECKS:
lines.append("| %s | %s | %s:%s |" % (cid, "PASS" if ok else "FAIL", name, detail))
lines += ["", "## S1 机检 D:铺底代表值(底型瓦片实测顶面受光档)五门", "",
"| 关 | RGB | L(线性域) | 饱和 | 色相 | 近色对 | 判定 |", "|---|---|---|---|---|---|---|"]
for bt, rgb, lin, s, h, near, ok in d_rows:
lines.append("| %s | %s | %.3f | %.3f | %.1f° | %d | %s |" % (bt, rgb, lin, s, h, near, "过" if ok else "红"))
lines += ["", "## 表一 逐对表(45 行)", "",
"| 关 | 底型 | 微光组 | 带内合成均值 | 格心素底均值 | 模型带内均值 | 模型差 | 三通道差 | 色相差° | 判定 |",
"|---|---|---|---|---|---|---|---|---|---|"]
for bt, vn, gn, bm, cm, md, dm, cd, hg, ok in table1:
lines.append("| %s | %s | %s | %s | %s | %s | %d | %s | %.0f | %s |"
% (bt, vn, gn, bm, cm, md, dm, cd, hg, "过" if ok else "红"))
lines += ["", "## 表二 组间表(90 行)", "",
"| 关 | 底型 | 组对 | 合成对比 | 灰度亮度a | 灰度亮度b | 判定 |", "|---|---|---|---|---|---|---|"]
for bt, vn, pair, cr, ga, gb, ok in table2:
lines.append("| %s | %s | %s | %.3f | %.3f | %.3f | %s |" % (bt, vn, pair, cr, ga, gb, "过" if ok else "红"))
n_fail = sum(1 for c in CHECKS if not c[2])
lines += ["", "---", "", "机检 %d 项,FAIL %d 项。" % (len(CHECKS), n_fail),
"", "目检(S3)归用户:四条判据见方案 §2.2(逐对指认 45/45/灰度副本 45/45/相邻并列 36 对可辨/装饰不抢读)。", ""]
with open(rep, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print("[报告] %s" % rep)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--art", default=os.path.join(REPO, "artwork", "mockups"))
ap.add_argument("--no-sheet", action="store_true")
args = ap.parse_args()
tiles = load_tiles()
print("== S1 机检 D:三关铺底代表值 ==")
d_ok, d_rows = gate_d(tiles)
if args.no_sheet:
return 0 if d_ok else 1
os.makedirs(args.art, exist_ok=True)
out = os.path.join(args.art, "glow-discrimination-sheet-2026-11-05.png")
print("== S2 出并列样图 ==")
img, cell_pngs, tiles2, blocks, gray_top, main_top, main_bot = draw_sheet(out)
print("[样图] %s" % out)
t1, t2 = run_checks(img, cell_pngs, tiles2, blocks, gray_top, main_top, main_bot)
write_report(t1, t2, d_rows)
n_fail = sum(1 for c in CHECKS if not c[2])
print("== 抽测机检完成:%d 项,FAIL %d ==" % (len(CHECKS), n_fail))
return 0 if n_fail == 0 else 1
if __name__ == "__main__":
sys.exit(main())
+31
View File
@@ -5,6 +5,8 @@
# 缺位时先拷主区素材+导入边车再跑)。
# 模式:
# --menu:shot_probe 启动态菜单整屏帧 → game/tests/golden/menu_boot.png。
# --lmap:level_map_shot_probe 关卡视口两 Shot(入口停点+Boss 列锚定——2026-11-05 用户裁
# 两 Shot 面)→ game/tests/golden/<plat>/lmap_entry.png + lmap_boss.png。
# 全屏基线可行性前置已验证:菜单连续帧差 0.0000%/max_diff=0(光柱为
# 程序生成静态贴片、零动画着色器,全屏基线不抖动);采集确定性=探针隔离档(无断点+出厂
# 设置 1280×800),与 render_gate 的 shot_menu 对比段同源同帧。
@@ -21,6 +23,7 @@ MODE="board"
for a in "$@"; do
case "$a" in
--menu) MODE="menu" ;;
--lmap) MODE="lmap" ;;
*) if [ -z "$GAME" ]; then GAME="$a"; fi ;;
esac
done
@@ -53,6 +56,34 @@ if [ "$MODE" = "menu" ]; then
exit 0
fi
if [ "$MODE" = "lmap" ]; then
# 关卡视口两 Shot 面(卷轴视口批·2026-11-05 用户裁:只对拍关卡最开始与关卡 Boss 节点,
# 中间过程不对拍)——入口停点 + 末列 Boss 列锚定,两基线各存平台子目录。
for a in assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \
assets/art/ui/button_stone.png \
assets/art/ui/node_battle.png assets/art/ui/node_reward.png \
assets/art/ui/node_rest.png assets/art/ui/node_boss.png \
assets/art/ui/node_shop.png; do
if [ ! -f "$GAME/$a" ]; then
echo "[golden_refresh] 拒跑 — 关卡屏素材缺位: ${a}(S3 停机点口径:基线禁锚字形回退态,先补素材+导入边车)"
exit 1
fi
done
gate_run_timeout 420 "$TMP/run.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/level_map_shot_probe.tscn -- "$TMP/shots"
st=$?
grep -E "\[level_map_shot_probe\]" "$TMP/run.log" | tail -3
# 判定锚工件(同 menu——quit-after 与 quit(0) 竞速的 124 非真红):PASS 签名 + 双图非零。
if ! grep -q "PASS — 两 Shot 落盘" "$TMP/run.log" || [ ! -s "$TMP/shots/lmap_entry.png" ] || [ ! -s "$TMP/shots/lmap_boss.png" ]; then
echo "[golden_refresh] FAIL — 关卡视口截图缺失或探针自检红(st=$st,见上)"
exit 1
fi
mkdir -p "$REPO/game/tests/golden/$PLAT"
cp "$TMP/shots/lmap_entry.png" "$REPO/game/tests/golden/$PLAT/lmap_entry.png" || exit 1
cp "$TMP/shots/lmap_boss.png" "$REPO/game/tests/golden/$PLAT/lmap_boss.png" || exit 1
echo "[golden_refresh] 完成 — 关卡视口两 Shot 基线已代入 game/tests/golden/$PLAT/(提交说明须写明触发原因)"
exit 0
fi
for a in assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \
assets/art/board/seal_mark.png \
assets/art/character/character_hero_idle.png \
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# 合回前置检查钩子(git pre-merge-commit,2026-11-06 用户裁)——主工作区 git merge 落合并提交前
# 跑最小验证面:tools/run_all_gates.sh --premerge(冒烟测试+单元与集成全套件 4 分片,热导入缓存约 2 分钟)。
# 语义(git 2.45.2.windows.1 本机实测):本钩子拦的是「合并提交」不是「合并动作」——exit≠0 时合并结果
# 已进暂存区、仓库停 MERGE_HEAD 待提交态;处置二选一:修红后 git commit 续合,或 git merge --abort 放弃。
# 快进合并不触发本钩子(git 语义)——本仓合回铁规恒 --no-ff,漏写时远端 CI 兜底仍在。
# --no-verify 可整体跳过(实测)——仅属用户,agent 禁用(worktree-guard 在案)。
# 仅主工作区触发(worktree 内合并不触发,与 post-merge 同款 git-dir 判别)。
# 冷缓存注意:本机首次跑批前先刷导入($GODOT --headless --path game --editor --quit,约 20 秒),
# 否则冷导入可致套件内建 120 秒超时假红;主工作区 .godot 缓存常驻,日常免此步。
set -u
_GD="$(cd "$(git rev-parse --git-dir)" && pwd)"
_CD="$(cd "$(git rev-parse --git-common-dir)" && pwd)"
[ "$_GD" = "$_CD" ] || exit 0
REPO="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || pwd)"
bash "$REPO/tools/run_all_gates.sh" --premerge
RC=$?
if [ "$RC" -ne 0 ]; then
echo "" >&2
echo "[pre-merge-commit] ✗ 前置检查红(读数如上)——合并提交未落,仓库停 MERGE_HEAD 待提交态。" >&2
echo " 处置二选一:① 修红后 git commit 续合本合并;② 放弃整次合并=git merge --abort。" >&2
exit 1
fi
echo "[pre-merge-commit] ✓ 前置检查绿——合并提交继续。"
exit 0
+6
View File
@@ -134,6 +134,12 @@ st=$?
grep -E "PASS|FAIL|问题" "$TMP/uip.log" | head -12
verdict "ui_input_probe" "$TMP/uip.log" "$st" || _rc=1
echo "======== level_map_viewport_probe(卷轴视口批 R1-R8) ========"
run_suite 120 "$TMP/lvp.log" res://probe/level_map_viewport_probe.tscn
st=$?
grep -E "PASS|FAIL" "$TMP/lvp.log" | head -12
verdict "level_map_viewport_probe" "$TMP/lvp.log" "$st" || _rc=1
echo "======== ui_struct_probe ========"
run_suite 120 "$TMP/stp.log" res://probe/ui_struct_probe.tscn
st=$?
+32
View File
@@ -165,6 +165,38 @@ else
fi
fi
echo "======== 关卡视口 golden 逐像素(卷轴视口批——两 Shot 面:入口停点+Boss 列锚定)========"
LM="$TMP/lmapshots"
gate_run_timeout 360 "$TMP/lm.log" "$G" --path "$GAME_WIN" --quit-after 3000 res://probe/level_map_shot_probe.tscn -- "$LM"
st=$?; kill_orphans
grep -E "\[level_map_shot_probe\]" "$TMP/lm.log" | tail -3
# 判定锚工件(同 fx/board——quit-after 竞速 124 非真红):PASS 签名 + 双图非零。
if ! grep -q "PASS — 两 Shot 落盘" "$TMP/lm.log"; then
echo "[render_gate] FAIL — level_map_shot_probe 未干净完成(exit=${st})"; _rc=1
fi
for b in lmap_entry.png lmap_boss.png; do
if [ ! -s "$LM/$b" ]; then echo "[render_gate] FAIL — 取证缺失/零字节: $b"; _rc=1; fi
done
scan_log "level_map_shot_probe" "$TMP/lm.log"
if [ "$_rc" -eq 0 ]; then
for b in lmap_entry.png lmap_boss.png; do
if [ ! -s "$GOLDEN/$b" ]; then
# 两 Shot 面(2026-11-05 用户裁)各平台独立冻结——本平台缺基线=未冻结态记 SKIP 不判红
#(windows 先锚;他平台冻结义务归收尾呈报),已锚平台照常逐像素严判
echo "[render_gate] SKIP — $GOLDEN/$b 基线缺失(本平台未冻结——跑 tools/golden_baseline_refresh.sh --lmap)"
else
LMG="$TMP/lmgolden"
mkdir -p "$LMG"
cp "$LM/$b" "$LMG/$b"
python3 tools/golden_compare.py "$REPO/game/tests/golden" "$LMG" "$b" | sed 's/^/[render_gate] /'
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
echo "[render_gate] FAIL — 关卡视口 golden 超容差(S2 停机点口径:视口呈现换代=显式重制基线+呈报;超预期面=L3)"; _rc=1
fi
fi
done
fi
echo "======== 残留检查(批末闸) ========"
left="$(orphans | wc -l | tr -d ' ')"
total="$(gate_godot_count_all)"
+47 -11
View File
@@ -9,7 +9,11 @@
# 只服务开发期按域跑批;--gates 档另为远端纯闸件/管线批的执行形态(domain_route.sh 判 gates → ci.yml 映射 --gates)。
# 跑闸时代码面带着未提交改动,回执会标 dirty_game(验的不是 HEAD 字节)。
#
# 用法:tools/run_all_gates.sh [--code|--docs|--gates|--quick]
# 用法:tools/run_all_gates.sh [--code|--docs|--gates|--premerge|--quick]
# --premerge 合回前置检查档(2026-11-06 用户裁):只跑冒烟测试+单元与集成测试全套件(4 分片,
# 热导入缓存约 2 分钟)——主工作区 git merge --no-ff 落合并提交前由 pre-merge-commit
# 钩子自动调用的拦截面。退出码聚合即判定(三硬闸口径),不落回执、不比基线——
# 验证台账归合回后远端 CI,本档只做当场拦截。快进合并不触发钩子;--no-verify 仅属用户。
# --code 只跑代码域闸(冒烟测试/系统测试/单元与集成测试/场景探针/
# 渲染闸/静态扫描+熵增闸)
# ——本批改动含 game/ 下非 .md 的行为载体(代码/测试脚本/探针/场景/数据表/素材)时用。
@@ -56,10 +60,10 @@ DOMAIN="full"
while [ $# -gt 0 ]; do
case "$1" in
--quick) QUICK=1 ;;
--code|--docs|--gates)
[ "$DOMAIN" = "full" ] || { echo "用法: --code/--docs/--gates 互斥"; exit 2; }
--code|--docs|--gates|--premerge)
[ "$DOMAIN" = "full" ] || { echo "用法: --code/--docs/--gates/--premerge 互斥"; exit 2; }
DOMAIN="${1#--}" ;;
*) echo "用法: tools/run_all_gates.sh [--code|--docs|--gates|--quick]"; exit 2 ;;
*) echo "用法: tools/run_all_gates.sh [--code|--docs|--gates|--premerge|--quick]"; exit 2 ;;
esac
shift
done
@@ -67,7 +71,7 @@ done
# 每个代码面 worktree 用独立 Godot user 目录(game/override.cfg 由 setup 脚本自愈生成;主区 no-op)。
# 并行批共用一套夹具、日志与截图目录会互相踩盘造成假红——隔离建不起来就拒跑(fail-closed)。
# 隔离只服务代码域(Godot 运行面);文档域与闸件自检档不启引擎,跳过省 0.8 秒。
if [ "$DOMAIN" = "code" ] || [ "$DOMAIN" = "full" ]; then
if [ "$DOMAIN" = "code" ] || [ "$DOMAIN" = "full" ] || [ "$DOMAIN" = "premerge" ]; then
"$REPO/tools/setup_worktree_userdir.sh" "$REPO" || {
echo "[gates] FAIL — user 目录隔离未建立,拒绝在共享目录上跑闸。"
exit 1
@@ -82,7 +86,8 @@ export PYTHONIOENCODING=utf-8
# 代码面(game/ 下非 .md 的行为载体)是否带着未提交改动(只算已跟踪文件——闸件自身产物都落在
# gitignore 面)。脏树跑出来的回执记的是 HEAD,验的却是别的字节,回执标 dirty_game 由 CI 落账留痕。
DIRTY_GAME=0
if [ -n "$(git -C "$REPO" -c core.quotepath=false diff HEAD --name-only -- game/ 2>/dev/null | grep -viE '\.md$')" ]; then
# premerge 档不查脏树:钩子跑在合并提交落笔前,合并内容本就未提交(diff HEAD=待合内容),无回执可比。
if [ "$DOMAIN" != "premerge" ] && [ -n "$(git -C "$REPO" -c core.quotepath=false diff HEAD --name-only -- game/ 2>/dev/null | grep -viE '\.md$')" ]; then
DIRTY_GAME=1
echo "[gates] 警告:代码面(game/ 下非 .md)有未提交改动——本次回执将标 dirty_game,不能用于合回(先提交再跑)。"
fi
@@ -130,7 +135,7 @@ in_domain() { # in_domain <code|docs|both>
# 所以 git worktree 检出的工程天生缺全部美术素材:main_theme.tres 解析失败 → 按钮五个样式盒全空
# → 冒烟 V1 成片假红;渲染闸又会因素材缺位把两段 golden 直接跳过(见 render_gate.sh)。
# 缺位即拒跑——禁带着空盘跑完,把「什么都没比」的回执当全绿合回。
if in_domain code; then
if in_domain code || [ "$DOMAIN" = "premerge" ]; then
_miss=""
for a in assets/art/ui/button_stone.png assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \
assets/art/board/seal_mark.png assets/art/character/character_hero_idle.png \
@@ -405,8 +410,8 @@ gate_unit() { # run_all 是 --script 模式,退出码为唯一判据;分
# ══ 闸件自检段(gates 档专属)══
# 闸壳自检:bash -n 过全部 bash 闸件与管线件——闸件批改的就是这些件,语法坏=下一批真跑闸时才炸,
# 闸件批当场拦住。收件面动态收全:tools/ 全部 .sh+game/tests 顶层 .sh(静态扫描)+hooks/post-merge
# (无后缀件);新增 bash 闸件自动入检面,无需维护清单。
# 闸件批当场拦住。收件面动态收全:tools/ 全部 .sh+game/tests 顶层 .sh(静态扫描)+hooks/ 无后缀件
# (post-merge/pre-merge-commit);新增 bash 闸件自动入检面,无需维护清单。
gate_shell_selfcheck() {
local name="闸壳自检"
local rc=0 _n=0 _bad=0 _s
@@ -414,7 +419,7 @@ gate_shell_selfcheck() {
while IFS= read -r _s; do [ -n "$_s" ] && _shells+=("$_s"); done < <(
{ find "$REPO/tools" -name '*.sh' 2>/dev/null
find "$REPO/game/tests" -maxdepth 1 -name '*.sh' 2>/dev/null
echo "$REPO/tools/hooks/post-merge"; } | sort -u
find "$REPO/tools/hooks" -maxdepth 1 -type f ! -name '*.sh' 2>/dev/null; } | sort -u
)
echo ""
echo "════════════════════════════════════════════"
@@ -442,6 +447,15 @@ gate_shell_selfcheck() {
return 0
}
# ══ 合回前置检查档(premerge,2026-11-06 用户裁)══
# 冒烟+单元与集成全套件(4 分片)——合回 merge 点的最小人工拦截面;不是全量、不替代远端 CI。
# 选闸推导整段跳过(本档不按 diff 选闸——合回是最后拦截点,恒全套件);退出码聚合即判定,
# 不落回执不比基线(回执与基线面归远端 CI,本档读数只进合并现场)。
if [ "$DOMAIN" = "premerge" ]; then
gate "冒烟测试" "smoke_boot.log" "$PY" "$REPO/tools/run_smoke.py"
gate_unit
fi
# ══ 代码域 ══
if in_domain code; then
# ── 冒烟测试(极小启动场景:真实开机路径存活检查,零逻辑判定,秒级)──
@@ -493,7 +507,10 @@ fi
# ── 静态扫描(代码域与闸件自检档)+ 熵增闸(各档都跑:它查顶层目录/根文件/图像白名单/退役路径,
# 文档批新增或挪动 design/ 下的文件同样会绊它;纯文本扫描,秒级)──
if [ $QUICK -eq 0 ]; then
# premerge 档不入此面:前置检查只盯冒烟+单元,静态扫描与熵增闸由合回后远端 CI 盯。
if [ "$DOMAIN" = "premerge" ]; then
echo ""; echo "[gates] 静态扫描/熵增闸 → 不入前置检查面(premerge;闸况归合回后远端 CI)"
elif [ $QUICK -eq 0 ]; then
if in_domain code || [ "$DOMAIN" = "gates" ]; then
gate "静态扫描" "static_scan.log" bash "$GAME/tests/static_scan.sh"
fi
@@ -506,6 +523,23 @@ else
echo ""; echo "[gates] 静态扫描(代码域)/ 熵增闸 → SKIP(--quick)"
fi
# ── premerge 档汇总:退出码聚合即判定(三硬闸口径),不落回执不比基线 ──
if [ "$DOMAIN" = "premerge" ]; then
RC=0
for _e in "${EXITS[@]}"; do
[ "$_e" -ne 0 ] && RC=1
done
echo ""
echo "════════════════════════════════════════════"
echo " 前置检查汇总(premerge——退出码即判定,不落回执)"
for _i in "${!NAMES[@]}"; do
_s="绿"; [ "${EXITS[$_i]}" -ne 0 ] && _s="红"
printf ' %s:%s(exit=%s)\n' "${NAMES[$_i]}" "$_s" "${EXITS[$_i]}"
done
echo "════════════════════════════════════════════"
exit $RC
fi
# ── 与存量基线比对 ──
"$PY" - "$BASELINE" "$RECEIPT" "$QUICK" "$DOMAIN" "$REPO" "$DIRTY_GAME" "$SCOPE" "$ONLY_TERMS" "${NAMES[@]}" -- "${EXITS[@]}" -- "${LOGS[@]}" <<'PYEOF'
import hashlib, json, os, re, subprocess, sys
@@ -648,5 +682,7 @@ RC=$?
echo ""
echo "════════════════════════════════════════════"
printf ' 回执:%s\n 基线:%s\n commit:%s\n' "$RECEIPT" "$BASELINE" "$(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || echo '?')"
# B 覆盖总览面板·执行层六源明细化(观测件非判定件——解析失败打红字不翻闸判,coverage_receipts.py 头注)
"$PY" "$REPO/tools/coverage_receipts.py" --stamp-dir "$STAMP_DIR" || true
echo "════════════════════════════════════════════"
exit $RC
+11
View File
@@ -508,3 +508,14 @@
| 1 | 三关封印格观感 | 环+十字+环外压暗光圈呈形正确、与格底可辨;像素判据已在机检承载,本项为观感复核 |
| 2 | 战末金蜡封徽记观感 | 贴图路径未动(零预期变化)——观感确认项 |
| 3 | 封印格与变异纹带同格观感 | 两层共存可辨、不脏面(零代码触面——观感确认项) |
## 52 卷轴视口与结算文案换代目检(迭代 53)
> 关卡画面视口化(卷轴视口)与结算文案换代(减半标签)后,机检已覆盖行为与像素面(探针 26 断言+golden 两 Shot 逐像素+明细行等值断言),观感面按登记不阻塞立规不拦合并。
| # | 目检项 | 判据 |
|---|---|---|
| 1 | 深图卷轴视口实跑观感 | 深度 13+ 图拖拽平滑可达首末列;停点推进跟随方向感自然、当前节点居中锚即时;拖拽不误触选路 |
| 2 | 指引件观感 | 深图两侧渐隐带与顶部平移提示 chip 可辨、不碍选路;浅图两件均不出现 |
| 3 | 结算明细减半标签 | 「挖掘惩罚减半(6→3)」一眼可读,无退役件名突兀感 |
- **结论回流**:观感问题归待办清单登记新条目;数值面不在目检现场改数。
+40 -20
View File
@@ -2,14 +2,6 @@
本清单是流水线的唯一登记处:以后才做的事在此登记一条,随流水线段推进改状态,做完整条删除(条目记进度,不是各段的开工前置);各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。
#### 三关色相 × 五组微光互辨抽测——三关图类型色板终值冻结的前置抽测(2026-10-28 登记,来源=会话提及)
**实况**:三关图类型色板(`tile_closed` 与 `wall_ambient` 两槽)终值冻结前须先跑「三关色相 × 五组类型微光」互辨抽测(格底与类型微光同在未开格面,邻近色相有吞没风险);美术设计段已闭合——取样面 54 格(3 关 × 5 组 × 3 底型+9 基准格)、判据=目检四条+机检四条(阈值全取在役值,色相通道不设机检阈值)、产物=并列样图+逐对读数表两兼。执行步骤与三件定值全载抽测方案过程稿 reports/提案-三关色相互辨抽测方案-2026-10-29.md;抽测挂点=[美术圣经](design/art/art-bible.md) v3.22 §6.3 终值取值程序第一条(终值冻结闸=§1.4 格子样张重跑)。执行前提已核:三关瓦片三套在本机 `game/assets/art/board/` 齐备,可开跑。
**改动面**:文档+美术素材+工具代码
**状态**:美术设计已闭合+待执行
#### 数值标定窗口总挂账——原九条数值类条目合并(2026-10-28 登记,来源=会话提及)
**实况**:九条数值类挂账合并一条(削弱档位终值/迭代 10 奖励重构联动/调参点评登记/三方向解锁清单与定价/GP 经济换算失衡/天赋树欠账余两件/商店刷新两参数/基线通关率越带杠杆扫描/成长系数节奏),逐条明细与统一执行形态(窗口内一次 MC 批量覆盖全冒险期望+奖励期望)全载 reports/汇总-数值标定窗口挂账-2026-10-28.md。共同裁定:未设计不启动,重启条件=用户发令开数值标定窗口。MC 档位硬闸见 sim-data/README.md「MC 采集档位表」。
@@ -20,14 +12,15 @@
**状态**:待文档设计
#### 超预算图策略升级路径——等比缩放与滚动视口两案待裁(2026-10-24 登记,来源=批次移交)
#### 结算明细文案入表——玩家可见明细文案归属改数据表承载(2026-11-06 登记,来源=批次移交)
**实况**:2026-11-05 用户裁滚动视口案落地(等比缩放案否决——像素纪律):关卡画面卷轴视口,跟随+拖拽+滚轮、平滑滚动(减动效瞬切)、当前节点居中锚;「图包络 ≤ 屏预算」约束退役,画布随图生长、规模归参数档。设计闭合凭据:design/gdd/ux/level-map.md v2.0(布局决策 5+视口效果图内嵌,2026-11-05 人审通过);design/gdd/systems/冒险循环/关卡节点图生成.md v1.5(参数契约节+图规模无呈现侧上限句)。需求卡片=reports/需求卡片-卷轴视口批-2026-11-05.md(grilling 复核裁决全落,验收档中)。
- 相关文档:关卡节点图参数取值域已归数据 schema 层;schema 头注「域并单点=调参表」指针反转随实现批顺带(卡片随动项);需求卡片 reports/需求卡片-卷轴视口批-2026-11-05.md(grilling 复核裁决全落,验收档中)。
**实况**:迭代 53 反哺点 4 归属裁决在案=文案入数据表行(2026-11-06 用户裁);规范句已落 [战斗结算](design/gdd/systems/战斗循环/战斗结算.md) v1.8 明细流水节(显示名等玩家可见文案=数据面字段,代码读表拼行、禁代码写死文案字串);现行代码硬编码面=结算明细扩展行标签文案(如减半标签,reward_grant.gd:226 一族)。
**改动面**:文档
**缺口**:还差表结构设计讨论——承载表与字段形态、基础行文案是否同批入表、读表拼行接线面。
**状态**:文档设计已闭合+已写卡+已编迭代 53
**改动面**:文档+游戏代码
**状态**:待文档设计
#### UI 贴图重构批——元进度件仍是程序化占位(2026-10-13 登记,来源=审计发现)
@@ -234,26 +227,53 @@
### 文档与结构
#### damage_controller 按序改写族——内容孤儿机制与退役名注释层(2026-11-04 登记,来源=会话提及)
#### 效果通道与事件词的零现役普查(2026-11-05 登记,来源=审计发现)
**实况**:damage_controller.gd 按序改写族通用机制(trade_hit/trade_atk 交换、armor_rewrite 免掷、spike 满层必中与倍乘、discount_shift 轻甲改善——注释叙事 :285-290、改写执行 :291-325、增伤与倍乘 :350-353 与 :368-372)现役内容面仅淬毒刃纹一行 modify_ticket(mode=clamp_bonus,feats.tres:853-857):其余子分支零表行注册(全仓 grep 证)——「框架先行、内容未接」同族债。五拍注释全以旧 27 专长名叙事(矿工重锤/专注/厚积薄发/猎弱本能,道具退役素材库.md:43-47 退役在案);charge_layers 四处已归 EffectPort 蓄力层删键批(实现提交 ace15701)。2026-11-05 会话裁决=孤儿机制与退役名一律清退(口径「只要有问题的都要改」);同轮核出 spike 的 GDD 承接句站不住(候选条写「零新代码——spike 在册」,实为词面在册、全仓零派发点;长枪条同类——attack_segments 已随架构审查甲案退役),据此订正该节四处陈述。全仓扫=40 退役名命中 53 行/13 文件(注释面 42、字面量 11),逐处清单见卡片。关联文档:设计定稿=design/gdd/systems/跨局循环/锻造工坊.md v0.13(2026-11-05);需求卡片=reports/需求卡片-改写族孤儿机制与退役名清退批-2026-11-05.md
**实况**:普查报告 reports/普查-效果通道与事件词零现役-2026-11-06.md——结论=内容层零孤儿、四词表+秘宝表 schema 旧词层全部属预留埋点(词面全保留零清退项;架构原则=词表先行内容后接)。叙述脱节 5 处已按「预留、未设计入表」口径订正(2026-11-06 用户令直改,冒烟 15 项 0 失败三闸全过)。唯一遗留待裁=秘宝表 schema 旧词面 9 个「迁移完成」收尾(做不做、何时做),待用户裁。
**改动面**:游戏代码
**状态**:文档设计已闭合+已写卡+已编迭代 53
**状态**:待执行
#### 效果通道与事件词的零现役普查(2026-11-05 登记,来源=审计发现)
#### 测试面真输入化分期与存量断言逐条精锚(2026-11-05 登记,来源=批次移交)
**实况**:证据=四条 grep 读数:`cell_removal` 通道在册(`game/data/schemas/treasure_item_table.gd:26`;消费点 `game/core/reward_grant.gd:160-161`)而 `game/data/` 全域零数据行;`mine_hit_halve` 同形态(仅 schema 词表与注记在位);`open_ticket` 的 `channel="reflect"` 行族零表行(21 张 `.tres` 逐张 grep);`armor_roll` 在 `game/core/effect_engine.gd:13` 事件词表在册、发射点 `game/core/damage_controller.gd:296`,而 `game/data/tables/feats.tres` 零订阅行。形态与架构审查甲案已清的 overflow/attack_segments 死通道同族(判据出处=`design/engineering/code-conventions.md:15`、`design/engineering/test-conventions.md:116`)。本批(需求卡片-改写族孤儿机制与退役名清退批)只清其文案与归因串,通道与事件词本体去留待普查批整体裁。
**实况**:证据=test-conventions §8.3「判据出处行锚」「真实入口判定口径」两条文+§2「UI 驱动断言须走真输入管线」条文(批1补,qa-harness 分支 2cbd2ca9);链式锚机检已在闸(capability_audit 链断 0,80 套件全承接)。已承接=两探针 52 处 emit 站点(51 转真输入管线+1 置灰穿透标〔接线断言〕)。剩余面=①视图层测试方法面(press_candidate/press_continue/press_card/press_confirm/press_refresh_for_probe 等探针消费口,非 emit 站点)②存量 996 条断言的判据出处逐条精锚(链式锚已保可核,精锚提升出处粒度)。
**改动面**:游戏代码
**状态**:待执行
#### QA harness 三批缺陷与覆盖遗漏收口(2026-11-06 登记,来源=批次移交,优先级=P0)
**实况**:证据=reports/QA-harness三批-缺陷与覆盖遗漏报告-2026-11-06.md(八类逐条带文件行号与登记处指针:产品缺陷 2=A1 开局被开始层盖住/A2 终局关闭路径引擎报错噪声;测试覆盖遗漏 6=B1 停手钮零真输入/B2 视图方法面/B3 精锚/B4 本体缺口 212 条/B5 控件零命中缺口/B6 判据收紧轮);来源批次=合并 d3c85390(三批读数在案)。处置分流:A1 随营地交互修复批合回即消;A2/B1/B2 走需求性工程写卡通道;B3/B6 归衔接条目与复核轮;B4/B5 人工复核后分流(真缺口立事项/假缺口豁免清单核销)。
**改动面**:游戏代码
**状态**:待写卡
#### 测试面真输入化分期与存量断言逐条精锚(2026-11-05 登记,来源=批次移交)
#### 闸机超时守卫缺陷修复——gate_run_timeout 标志文件恒真(2026-11-06 登记,来源=批次移交)
**实况**:证据=test-conventions §8.3「判据出处行锚」「真实入口判定口径」两条文(本批落);链式锚机检已在闸(capability_audit 链断 0,80 套件全承接)。剩余面=①营地七物件段等 ~40 处 emit 站点仍为接线验证(大门/进档链已于本批真输入化,能力行 :158 残险在案)②存量 996 条断言的判据出处逐条精锚(链式锚已保可核,精锚提升出处粒度)。
**实况**:tools/gate_platform.sh 的 gate_run_timeout 用 mktemp 预建超时标志文件,判据 `[ -f ]` 建时即真、超时态无法与正常完成区分(Windows Git Bash 实测直接调用 4 秒返回 124;远端各闸以工件锚定未受影响)。连带 tools/golden_baseline_refresh.sh 的 guard 子Shell sleep 420 未被 kill 短路(重采集等待 7 分钟症状)。修法=kf 先赋路径+rm -f 后置守卫(probe_gate.sh:97 run_suite 同款正确写法)。证据=tools/gate_platform.sh:37/:48+迭代53实现报告遗留呈报 1(git 历史可考)。
**改动面**:工具代码
**状态**:待执行
#### 关卡视口两 Shot 基线 mac 侧冻结(2026-11-06 登记,来源=批次移交)
**实况**:golden/windows/ 已锚 lmap_entry/lmap_boss(提交 6394abf7);mac 子目录无该两 Shot——渲染闸对 mac 缺基线记 SKIP 不判红,两平台同盯缺一。义务=Mac 侧跑 tools/golden_baseline_refresh.sh --lmap 冻结 mac 基线并呈报确定性读数。证据=game/tests/golden/README.md 现持清单行与时效判定纪律行+迭代53实现报告遗留 2(git 历史可考)。
**改动面**:游戏代码
**状态**:待执行
#### 美术圣经去数值化——定值改由机检件与数据表承载(2026-11-06 登记,来源=会话提及)
**实况**:本条立文档分层规矩——美术圣经与 GDD 同级属 Meta 层,只写概念与方向(可写「两者存在相对亮度关系」),不写具体数值约束;阈值与准入判定归执行件,执行件登记面=[art-pipeline-tools](design/art/art-pipeline-tools.md)(三关色板互辨抽测件本批已入册)。
- 相关文档:纪律出处=[doc-conventions §5 禁实现内容行](design/engineering/doc-conventions.md);同款先例=[08-Alpha版本标准](design/gdd/08-Alpha版本标准.md) 二节「面值一律不在此抄录,以数据表为准」
**缺口**:还差一次清点与归属设计讨论——圣经全部定值逐条定落点(机检件/代码常量/数据表/golden 基线)、定文档侧指针句式、核下游反向引用面(ux 文档、需求卡片、探针注引圣经定值之处)。
**改动面**:文档+工具代码
**状态**:待文档设计