Compare commits

...
Author SHA1 Message Date
DKChan 74b94b4c2e Merge branch 'master' into iter44 2026-10-30 07:11:29 +08:00
DKChan fd95261111 审计尾笔: 独立验收审计通过(可合并)——审计报告落位+记录级缺漏三项补记(卡③串行决定已在案/main_overlay_flow头注词面遗留留反哺点/grant_segment计数偏差补记)+V1d平铺口径例外备案 2026-10-30 07:10:34 +08:00
DKChan 9f1dda4088 产出物尾笔: 实现报告补批末全量域闸实况(三闸归绿·回执b27db010)与闸红修复三笔记录+关联档选测盲区教训 2026-10-30 06:57:19 +08:00
DKChan b27db010e8 闸红修复二(run_time_fatal同型漏网续清): meta_progression_forge_test三处编排器encounter直读改_facts.encounter(:340 current_battle().encounter.get/:749 orch3.encounter[:749/:758 orch4)——分片1复现单进程未现(夹具路径差异)·全域终扫后同类直读清零(仅余消息文本) 2026-10-30 06:49:41 +08:00
DKChan dea383904b 闸红修复(批末全量域闸三处新红必修): ①S9 R1——tune_sim _sim_stats裸Dictionary注解改Dictionary[Variant,Variant](Variant参数化现行口径);②ui_input_probe :482/:488 case⑮装配直构注入 settlement.attunement→_facts.attunement(卡②成员迁移的探针消费漏网——T0映射谱系只扫main./root.前缀,直构注入形态漏;SCRIPT ERROR中断_ready致探针挂死超时);③run_time_fatal_test :75 BoardInstaller束键值 oc.encounter→oc._facts.encounter(同型漏网——负面套件名与触面无字面关联,关联档选测盲区,SCRIPT ERROR中断致退出码2≠期望1)——三件孤跑复绿(S9归零/探针十八断言绿/负面套件exit=1) 2026-10-30 06:39:03 +08:00
DKChan 14054d9dfb 注销(待办): 技术债观察登记条目整条删除(lambda连接防回归)——三层社区标准方案已闭环:①预防层code-conventions §4.1两条明文纪律在册(多行lambda禁嵌match/信号处理体一律命名方法+bind;捕获循环变量先落地再捕获)②体外扫描层error_scan.py三段扫日志命中即红在役③捕获语义类静默错误由测试断言承接;静态层确有缝(gdstyle 23条quality规则无lambda专项)但本事项仅是数数观察不拦任何坑,补缝唯一方式=闸件加规则,而危险形态(多行lambda嵌match)全库117处实测零存量、登记半月零事故,为零命中形态写闸件属过度工程;社区论证凭据=官方提案godot-proposals #4736永久拒绝try/catch、错误码哲学为设计决策非版本bug 2026-10-30 06:38:33 +08:00
DKChan d2f282d9bc 登记(待办): 棋盘封印印记图形换代一条——现件 seal_mark.png 仍是经典扫雷旗形(旗面+旗杆),2026-09-28 只换色未换形;用户欲改法阵或锁链,形未定故只登记待美术设计,缺口列五待裁点(形/封印黑不变槽是否松动/32px 内是否侵入数字字形区/战末金蜡封徽记与悬停印记形光标两处连带件/与施印特效+印记常显两件的合并关系),实况记消费面两处与文档侧仍写「旗」的口径点(art-bible §4.1 旗杆前提+§3.2、art-assets-checklist §3.3/§4、battle.md:128/:270),改动面=文档+美术素材;格式与退役词闸全绿 2026-10-30 06:16:13 +08:00
DKChan 0fb870ac5e iter44-卡3:卡③(测速循环批v1.1): 步进状态机改造(循环内await消除·收尾四语句同帧同相位迁入·_run_one_level零触碰)+双实现对拍套件(帧序列相对偏移/运行态/进度串/stats/报告五面·n=3+n=0)·wd8与探针零改动保持绿·裸键从gdstyle命中消失·含并master与三卡产出物(实现报告/反哺点) 2026-10-30 06:11:49 +08:00
DKChan 3465b932ce iter44-卡2:卡②(战斗编排归约批): RoundCycle+BattleFacts两新件·宿主38→15·treasure_inventory经reward_grant()出口直注·_hero_atk派生读·完美账三mark写口·behavior_roll流随件迁移取号序逐字·对照表V1-V5新增断言内嵌·键账15/15/8·811行 2026-10-30 06:11:49 +08:00
DKChan 5a1ddb65e6 iter44-卡1:卡①(组合根注册面批): ui_registry+三域容器建件·main.gd 49→15(28迁出+7别名消灭+ui新增)·六转发方法+flush退役·全消费面(探针11件/系统测试/流程件/冒烟)改址main.ui.*·V1d全目录重锚〔注册面V1〕·V6改串〔注册面V2〕·行为零变化四闸主证(render golden/probe全段/冒烟/系统测试) 2026-10-30 06:11:49 +08:00
DKChan ab3b4a9d80 同步(工程批收尾): test-conventions §7覆盖率句两处旧口径随a2ec3e8b裁决换代——「是否引入待办清单在册待裁」改直陈不引入(条目已删裁决已落),「现行补洞=零指名单测清单」改能力清单覆盖域表(零指名机制已被能力清单模型取代);规约修订经doc-design入口落笔,零待裁点,用户已发令;断链面已核仅此一处 2026-10-30 06:10:21 +08:00
DKChan fbed817fbe 注销(待办): qa/收尾孤儿遗留余二项整条删除——第5项字面量墨收测前置登记两头失效:主任务(iter19卡②墨色收测)随卡退役无现役承接,前置立常量不成立(实测bcard四处色值仅选中底#8A5A34在art-bible色板在册[木色§7.2],✓章#3A2A08/描述#4A3A28/脚注#6E4534三处游离色板外,真要动第一步是美术线色板准入/换色合并非ui_kit常量化);第7项商店房截图纳屏诉求剩余面只剩像素级(结构面ui_struct_probe _audit(shop)与交互面ui_click_probe已覆盖),像素面shot_probe头注已定谳不纳屏(真实路径不可达+合成投喂违反该探针口径),真缺口(商店屏文字对比度无机检)按未实测不启用不立新条,试玩出观感问题再按当时形态登记 2026-10-30 06:00:04 +08:00
DKChan 0caa2f5b7c 刷新(待办): 技术债观察条目长函数半边核销——点名四件(level_map_screen选路装配原155行/camp_overlay_view原152行/debug_overlay原114行/treasure_item_table schema校验群)实测现各文件最长函数仅25-44行全部消解,S13质量闸扫面覆盖core+data+presentation基线冻结禁恶化已接管防回归;条目缩为lambda连接观察(76处读数归属2026-10-15,测试面其后多轮重组分布口径未重测) 2026-10-30 05:54:45 +08:00
DKChan 30fdec1a62 写卡(迭代 44 卡③测速循环批 v1.1 停卡重裁): 实现段 T0 三重证据定谳卡③核心前提已被迭代 41 测试债批消化——215da315 提交信息明载「tune_sim 运行态收尾归还 run」+wd8 三断言(现位 :145-147)与 ui_tune_probe 全链路实跑两面全绿(停卡呈报=iter44 worktree reports/iter44-实现报告-2026-10-30.md §5),按停卡呈报建议案重写卡片——剥离已申报行为改动 V3(收尾置假还原无对象,还原已由迭代 41 完成并申报;完成定义改「diff 中行为改动零处」,对照表 V3 行改定义为非行为改动行=收尾四语句同帧同相位迁入 _finish_batch 的回归锚面,wd8/probe 判据「零改动转绿」改「零改动保持绿」),T0 义务改现状复核(两面实跑应绿——绿=按无行为改动形态开工,红=停卡呈报),夹具=现行产品码 await 形态逐字副本、与产品码新形态申报差异两处减一处(只余夹具暴露 stats 供对拍),全部 file:line 按 2026-10-30 master 重锚(run :36-54/_run_one_level :58-68 尾部实为败局记账/wd8 :139-147/基线键 :15;S8 九文件漂移订正十文件),architecture v1.25 S13 注销句保持(注销对象是 await 豁免不依赖行为改动申报,句内申报半句随实现批现状化——同步面注记),§16/§17/§13 随动;待办清单条目实况同批订正该失效申报句并登记重裁凭据(状态行维持已写卡+已编迭代 44——v1.1 落定即重立),roadmap 迭代 44 行不动 2026-10-30 05:52:44 +08:00
DKChan a2ec3e8b94 工程(渲染闸脚本错误扫描): 三段探针日志逐段过 tools/error_scan.py 补三硬闸之二——shot(shot_checker 之后)/fx/board 各一次,命中或日志不可读即红,标记与 run_smoke 同源;头注补⑤与「新增日志段必须同步加扫描」纪律
验证=合成日志三分支(clean 0/命中 1/不可读 2)+真跑 render_gate exit=0 全绿(三段零命中、残留 Godot=0、golden changed 0.00%)+文档域闸五件 PASS。
文档同步=architecture 测试域两处「SCRIPT ERROR 扫描缺失」换代、game/tests/README.md 工具行补判定面。
清单=「测试域遗留」整条删除(第 1 项被能力清单模型取代/第 3 项与 test-conventions §6 存量不清扫口径冲突且多进程分片后前提消失/第 4 项行覆盖率用户裁无意义/第 2 项即本批);基线条目扩为 02/05/09 三键(05 主区实测绿,baseline 红键属代码面,随下一代码批在 worktree 内重生成)
2026-10-30 05:39:33 +08:00
DKChan 2a5174d2a9 文档设计(美术出图登记轻量化): 出图登记义务四字段(模型名/分辨率/seed/对话日期)缩为必注两项「用途+生成时间(YYYY-MM-DD HH:MM)」,参数有则记、无则不填,登记不再作正稿遴选前置——旧口径卡住用户在途出图管线(线上对话式生图拿不到模型名与seed,分辨率与时间文件属性自带);立规处remote-gen-prompts头注改写(标题复现性→出处备查),转抄两处同步(art-assets-checklist §3.5/art-prompts §5流程注),手绘件登记项(手绘者/日期/源档尺寸)并入同一轻量口径(作者有则记);art-prompts版本号v2.10→v2.11;待办清单条目同批登记并删除(net-zero) 2026-10-30 04:52:50 +08:00
DKChan 3607271575 注销(待办): 在途草稿整理批(3349 失管草稿)整条删除——审计 P2-1 口径错配:ui-reuse-2026-09-23 系下载的第三方 CC0/OFL 素材包(占 3043/91%),vN 命名率与冗余量法对第三方包不适用,实测重复文件仅浪费 0.1MB;artwork/ 与 archive/ 均 gitignored,不进提交/构建/交付,不构成维护成本;用户裁不处理。留档约束:该包不可整删——quantize_ui_skin/render_sample_sheet/render_sample_sheet_v3/render_level_map_sheet 四个工具脚本按路径引用其中的字体/面板/图标/石纹 2026-10-30 04:30:19 +08:00
DKChan 9154f03b48 收尾(场景探针闸合并批): 待办条目完成整条删除——03/04并「03 场景探针」一行,窄试车十段exit0 2m22s,worktree全量闸all_green=True 3m25s,基线两键随批换代(b1e0f29f);引用面三处同步(README/process-conventions/iter技能) 2026-10-30 04:29:01 +08:00
DKChan b1e0f29fe3 合回(工程批·场景探针闸合并·基线换代): baseline.json两旧键(03探针--click/04探针--full)换「03 场景探针」green键;worktree全量闸复跑all_green=True(15闸全0,03场景探针真跑十件)——批本体55a8f270,验证3m25s 2026-10-30 04:28:39 +08:00
DKChan cfb528b917 工程(场景探针闸合并·基线换代): baseline.json删「03 探针 --click」「04 探针 --full」两旧键、预置「03 场景探针」green键(实测真值=本worktree全量闸03场景探针exit0十件全跑);02/05/09存量红键未动——归「闸基线两键换代」在册条目 2026-10-30 04:20:46 +08:00
DKChan 55a8f27026 工程(场景探针闸合并): 03/04两闸并一行「03 场景探针」——gate调用probe_gate.sh --click --full一次跑齐十件(公共五件只跑一遍,窄试车2m22s十段exit0),04号让空不重排;影响面SKIP块与头注/影响面注释同步;README闸03/04提法与process-conventions影响面句换代;probe_gate.sh零改动(--click/--full保留为开发期单跑开关) 2026-10-30 04:08:48 +08:00
DKChan f9fa8d85fa 登记(工程批): 场景探针闸合并——03/04两闸并一行+闸名换代(probe_gate.sh零改动,公共五件去重复跑) 2026-10-30 04:05:02 +08:00
DKChan c8f5091d0f 收尾(迭代43批后收尾): 反哺点13条处置闭合——落文四处(art-bible v3.23 §1.4样张登记句与§6.1素底牌实装事实/art-prompts v2.10 §5产出状态/战斗表现 v1.2五项过闸状态/test-conventions粒度句撤销措辞按§8.4口径清偿),消解三条(AGENTS红线已换禁变异禁撤销新口径/血条tween缺陷已由迭代41测试债批修复/该条留档备查),移交两条目(批后待裁三点=B区高度270vs252对齐方向·名字行12px档位归属·换色常量退役/批后小件=能力清单补行归写卡段·意图徽标四色墨CHECKS补断言),方向稿目检登记体验验证清单㊶(方向稿气质/改前改后对照/类型微光provisional观感复核并入,㉘节陈旧批指订正)——产出物退役:iter43实现报告/反哺点/验收审计/代拍板追认单与两卡需求卡片(2026-10-28定稿卡+2026-10-16旧v1.4卡)按吸收即死删除;二三关棋盘观感条目陈旧前置句换代(样张目标规格清单已落art-bible v3.22)——账面换代:待办清单样张条目整条删除+roadmap迭代43行已完成(2026-10-30)/卡片报告列退役保留字+当前位换代(迭代位1–43全完成,下一步=迭代44开工前宜裁PARITY查因重锚与待裁三点) 2026-10-30 03:23:05 +08:00
DKChan d3b977480e 合并工程批(parity-rm-20261030): 对拍闸退役批合回——回放确定性对拍闸(原11号条件闸)整闸退役:闸脚本6c段删除/PARITY_TOUCH_FACES改名CROSSCUT_TOUCH_FACES(02横切回落保留)/基线删条目/parity_anchor.py+parity_compare.py+evidence/parity锚三件删除/tests README能力行与工具行删/架构文档闸清单与验收红线清理/待办清单对拍挂账清理(含并行收尾批新登记的查因重锚条目注销)/迭代44在册卡V0验收载体随退役换代七处(用户裁:整局重放对拍代价不可承受,正确性由既有单测/集成/探针/渲染golden承载);两度并master(闸机提速批+迭代42批后收尾)零冲突;全量域闸全绿回执e79c04e8(domain=full,scope=full,changed=[]) 2026-10-30 02:59:54 +08:00
DKChan e79c04e891 工程(对拍闸退役批·连带二): 删除并行收尾批新登记的「PARITY对拍锚漂移查因与重锚」条目——退役批已删锚与工具,条目主体不存在,查因重锚随闸退役一并注销 2026-10-30 02:56:22 +08:00
DKChan 05d1988fcd 并master(迭代42批后收尾)——架构v1.26反哺点落文+批末移交小件登记 2026-10-30 02:55:50 +08:00
DKChan 7ec2af581a 收尾(迭代42批后收尾): 反哺点两处分置闭合(反哺点1落文=architecture v1.26 ShopSession行对外暴露格按委托壳拆除后实测直连面重写——直连方法清单16+白盒2与set_shop_pool调用面改述;反哺点2豁免登记被零豁免裁决消解不落,v1.25豁免注销在先)——产出物退役:iter42实现报告/反哺点清单/验收审计与两卡需求卡片按吸收即死删除(卡片死期=实现批合回;报告遗留义务先迁:S13重锚6键离账折入零豁免条目收账句、PARITY对拍锚漂移查因与重锚新立条目、批末小件四条新立条目)——账面换代:待办清单尾波清偿批与商店委托层拆除两条目整条删除+roadmap迭代42行已完成/卡片报告列退役保留字+iter41注脚152死键refresh已执行订正(fafb4fee)+当前位换代(迭代位1–42全完成、迭代43已合回待收尾、下一步=iter43收尾与迭代44开工) 2026-10-30 02:54:15 +08:00
DKChan 6b17d8f307 并master(闸机提速与防作废批合回)——02分片并行+代码域起手自检+worktree素材同步,与对拍退役批同改闸脚本,冲突就地解 2026-10-30 02:46:54 +08:00
DKChan 42c7d879ed 工程(对拍闸退役批·连带): 迭代44在册卡「战斗编排归约批」七处对拍引用随退役换代——V0验收主载体由PARITY对拍改挂关联套件并集(用户裁:其他测试函数保证正确性)、能力行引用换代、§2待裁点⑤注销、§3触面句与§4契约句、§13零触碰声明、§14 L3条款④⑤重排、§16审计凭据句——pipeline_gate 13卡全过 2026-10-30 02:38:57 +08:00
DKChan 8491505929 工程(对拍闸退役批): 删除回放确定性对拍闸及全部配套件——闸脚本6c段整删、PARITY_TOUCH_FACES改名CROSSCUT_TOUCH_FACES(02横切回落判定保留)、回执指纹NEED-APPROVAL分支清理、基线删11号条目、parity_anchor.py/parity_compare.py两件删除、evidence/parity锚删除、tests README能力行与工具行删行、adventure_monte_carlo台账行换代、架构文档闸清单删条件件与验收红线史句清理、待办清单数值窗口删对拍锚重锚句与触面粗粒度挂账行删、settle_segments_test头注去失指句——用户裁决:整局重放对拍代价不可承受,正确性由既有单测/集成/探针/渲染golden承载 2026-10-30 02:28:28 +08:00
DKChan db5abb8527 收尾(闸机提速与防作废批): 待办条目完成整条删除——实测全量闸3分29秒全绿66套件(对照基线约24分);窄试车6态;素材同步实战补拷247件;iter技能合回段三连成文;C批探针渲染并行未立项 2026-10-30 02:23:57 +08:00
DKChan 4ecdcf444f 合回(工程批): 闸机提速与防作废——02分片并行(run_all.gd加--shard/--shard-count,串行道=RNG日志会话六套件钉第1片防同种子日志互踩,空片放行,逐套件日志_s<k>防覆写)+gate_unit多进程(GATE_SHARDS默认4,=1回落原路径,片序合并日志供错误扫描与指纹取材——指纹绿记green红取FAIL排序集,免记账换代)+code域起手自检(熵增预检红灯即止/master推进黄牌/素材自动同步)+tools/sync_worktree_assets.sh(增量拷assets与.import边车,缺件或缺.godot时编辑器导入刷新,相对路径防杂散目录);窄试车6态+全量闸实测3分29秒全绿66套件(基线24分) 2026-10-30 02:23:18 +08:00
DKChan 1ee65d854b 工程(闸机提速批): 02分片并行+代码域起手自检+worktree素材同步——run_all.gd加--shard/--shard-count分片(串行道=RNG日志会话六套件钉第1片防同种子日志互踩;空片放行;逐套件日志_s<k>段防跨片覆写)+gate_unit多进程并行(GATE_SHARDS默认4,=1回落单进程原路径;日志按片序合并供错误扫描与指纹取材——指纹绿闸记green红闸取FAIL行排序集合,不受片序影响)+run_all_gates.sh code域起手自检(熵增预检红灯即止/master推进黄牌/worktree素材自动同步)+tools/sync_worktree_assets.sh(主区→worktree增量拷assets art与.import边车、缺件或缺.godot时编辑器导入刷新;相对路径拷贝防杂散顶层目录) 2026-10-30 02:17:05 +08:00
DKChan 31c801cd5a 登记(工程批): 闸机提速与防作废批——02分片并行+代码域起手自检+worktree素材同步(来源=迭代42合回六轮重跑复盘会话; C批探针渲染并行缓做不立项) 2026-10-30 02:11:47 +08:00
DKChan 65dff22fcf 合并迭代42: gdstyle尾波清偿批+level_runner商店委托层拆除 2026-10-30 01:42:26 +08:00
DKChan 0fc656c6ac iter42-收尾: 实现报告反哺点验收审计落reports+PARITY归因落档(迭代40/41存量) 2026-10-30 01:37:01 +08:00
DKChan 9c12d0a683 iter42-卡2: level_runner委托壳19枚拆除+shop公开化直连键7=33+残留零真残留+基线记账03/04/05(用户裁) 2026-10-30 01:34:06 +08:00
DKChan 66e6d0452a iter42-卡1: gdstyle尾波清偿批——board_view五段拆分与构建器/装板域拆BoardInstaller/wr5改址,键1-4+9+10归绿 2026-10-30 01:33:47 +08:00
DKChan 3b66d1a689 合回(迭代43): 战斗一屏样张批单卡合回——五项实装定值(血条#A8321F/条底收常量/徽标FS_HINT/护甲三枚PNG/名字行素底不压地台/地台件九宫格接装)+样张脚本扩展(两版出口/整屏四项S1–S4/战斗表现五项P1–P5/V1/V10/A5b,机检30/30全绿)+新单元套件standee_present_values(V5–V9十八断言)+theme_projection V4锚换代(§5.5同生死,双半强替代);全量双域闸全绿回执34eba7a3(domain=full,scope=related,16闸15绿+对拍SKIP);独立验收审计通过(三自查跑批原样绿,V1–V10十行全过,削弱零缺漏);批件三份+审计报告落reports,反哺点13条归文档批 2026-10-30 00:23:22 +08:00
DKChan 0c5eb2133d 审计(迭代43): 独立验收审计通过可合回——审计报告落reports(三自查跑批原样全绿:样张30/30、单测18/18、theme_projection164/164;V1–V10十行全过;削弱检查零缺漏,V4锚换代证实双半强替代);实现报告§7结论落位+V6行号笔误订正;反哺点13补意图徽标四色墨机检断言(审计附注,复算4.97–5.62全过) 2026-10-30 00:22:31 +08:00
DKChan 34eba7a332 iter43-卡1:战斗一屏样张批:五项实装定值落码——血条填充#A8321F经UiKit.HP_BAR_FILL(§4.2语义物件,#689CE8退役值零残留)/条底收HP_BAR_TRACK/顺位徽标提档FS_HINT/护甲三键换24×24 PNG(UiKit.TEX_ARMOR_*单点,缺位回退字形)/名字行垫HP_BAR_BACK素底牌且基线上移地台顶上方(§6.1/§7.2,几何单点_name_plate_rect+name_line_probe扩出口)/地台件TEX_STAGE_PLAZA九宫格横向拉伸接装(缺位回退程序化三段)——样张脚本扩展:draw_base两版画布出口(1280×800基座+1920×1080全局等比1.35×左右黑边96px)/B区按代码252出样/mockup镜像五项定值/S3b色板变体第二套/S5三组关键帧条/S6剪影三语境+描边对照+遮挡对照组/整屏四项断言S1–S4/战斗表现五项P1–P5/A7护甲对比锚/V1两版尺寸/V10基线并排/A5b成稿净空(机检30/30全绿)+--gen-assets四件素材产出入包;新单元套件standee_present_values(V5–V9真实入口断言18条全绿)登记manifest;三枚护甲PNG入包;批件三份落reports(实现报告/反哺点12条/追认单11条);并master合入态订正(迭代41测试债批清偿存量红与tween缺陷) 2026-10-30 00:00:01 +08:00
100 changed files with 3475 additions and 3326 deletions

No files matched your search

+1 -1
View File
@@ -131,7 +131,7 @@
### 3.5 营地场景图
- **要什么**:营地场景图 9 张——`scene_camp_bg`(背景)+ `scene_camp_gate / forge / chest / notebook / dummy / board / campfire`(七个可点物件)+ `scene_camp_tent`(承载件)。钥石顶栏图标沿用现役 `icon_keystone`。
- **来源/规格**:**✦ 插画级**(作者自跑远程生图管线);烛光黄 `#F6C667` + 木色 `#8A5A34`(art-bible §6.2);2× 源档出图、最近邻降采样到 1280×800;后处理=抠底/降采样/色板量化/描边统一。**每张出图须登记模型名+分辨率+seed+对话/日期**。
- **来源/规格**:**✦ 插画级**(作者自跑远程生图管线);烛光黄 `#F6C667` + 木色 `#8A5A34`(art-bible §6.2);2× 源档出图、最近邻降采样到 1280×800;后处理=抠底/降采样/色板量化/描边统一。**出图登记照 [remote-gen-prompts](remote-gen-prompts.md) 头注轻量口径执行:用途+生成时间必注,其余参数有则记。**
- **排期**:营地交互重做**美术自立批**(不占迭代位;UI 贴图重构批已搁置)。终稿落 `game/assets/art/scene/`,消费常量 `UiKit.TEX_CAMP_*`。
- **现状**:全占位。
+4 -2
View File
@@ -1,6 +1,6 @@
# 美术圣经(Art Bible)— Dungeon Sweeper
> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**;现行为 v3.22,历次修订明细见 git 提交史
> 项目:Dungeon Sweeper(扫雷 × 自动战斗 闯关 Roguelike)· 状态:**第 1–9 节定稿**;现行为 v3.23,历次修订明细见 git 提交史
>
> **Art Director Sign-Off(AD-ART-BIBLE)**:历次批均未签核——此为 Lean/Solo 模式(非 PHASE-GATE)下的现行治理状态;full 门禁签核时机待用户表态。
> 风格基调:地牢题材但**非暗黑风**——轻快明亮的幻想冒险风,全年龄;像素风;角色 2~3 头身。
@@ -59,6 +59,8 @@
**整屏样张闸**:格子样张验"格",整屏样张验"屏"——战斗屏/关卡屏/菜单屏/营地屏各出一张整屏样张,验收四项:①全部文字(含菜单/说明/tooltip/按钮)对其直接背景 ≥4.5:1(WCAG AA),大字(≥18px 或加粗档)底线 3:1、目标 4.5:1;②明度分层三档带成立(信息面层 > 世界中层 > 世界底层,层间灰度可分);③整屏焦点预算(§3.4:首焦唯一——数字区/立绘/主按钮,无第二焦点竞争;负空间服务焦点,不属空窗);④文字禁直接压纹理底(垫容器或罩层)。验收顺序 = **方向稿目检先行**(防"机检过闸但气质否决"式返工),目检过后机检冻结。执行时机 = 屏级视觉落码开工前;过闸前一切屏级视觉为占位临时态(§7.1 完成条款)。**样张判定物的产出手段与素材输入级别不进圣经定值**——圣经只定样张要验什么,不定样张以何种手段产出、也不定它取哪一级别素材输入。验收:机检〈整屏样张四项断言全绿,且目检先行过审〉
**战斗一屏样张登记(合并提交 3b66d1a6)**:战斗一屏样张批整屏四项机检全绿——抽样读数:文字对比最弱 5.34:1;舞台底 L0.036/已开格底 L0.089 落两子带、间隔 0.053;首焦=数字区高对比能量占比 59%;名字行素底牌平底零地台色。样张整体过闸待方向稿目检先行(真人检测,登记于体验验证清单 ㊶)
**样张现状**:格子样张已冻结,冻结凭据 = git 提交 7a847c7(机检报告)与 §4.1/§8.2 定值表;样张 v3 产物原件已丢失、未入 git。现行冻结态:色板与帧数档维持冻结;数字形制 = 亮墨三档直画 + 色牌后备(值见 §4.1);新增图类型/变异色板须重跑本节测试。验收:机检〈冻结项定值均以 §4.1/§8.2 对应 owner 表为准〉
---
@@ -374,7 +376,7 @@
### 6.1 战斗舞台背景(B 区环境)
- **双舞台常驻下的 B 区环境**:战斗画面区(站桩立绘所在)的背景 = 明亮地窟,与 §1 风格基调同源——暖倾向、无压抑;背景整体**低对比自然后退**(§3.4 焦点预算延伸):背景禁止构成完整凸形、禁止抢 B 区立绘与 D 区数字的焦点,识别层级恒为 立绘 > 背景。验收:目检〈背景无完整凸形、立绘最抢眼〉
- **舞台正面目标**:B 区 = **地窟看台**——立绘脚下地台/落影承载(世界中层),背景环境层低饱和后退、明度走世界底层档,允许光源氛围(洞顶光柱/暖光晕候选);角色禁止直接贴平色底。规格与终值归整屏样张闸(§1.4)冻结。**地台面上零文字**——暖白 `#F0E2C8` 对地台板身 `#968264` 实测仅 2.89:1,已越 §7.2 的 4.5:1 硬指标,落法按 §7.2「文字禁直接压纹理底」(垫容器、素牌或罩层),非新条款;站桩文字位改垫素底或落进结算明细带。验收:目检〈立绘脚下有地台/落影,非贴平色底〉+机检〈B 区站桩文字位正下方像素非地台板身色 #968264〉
- **舞台正面目标**:B 区 = **地窟看台**——立绘脚下地台/落影承载(世界中层),背景环境层低饱和后退、明度走世界底层档,允许光源氛围(洞顶光柱/暖光晕候选);角色禁止直接贴平色底。规格与终值归整屏样张闸(§1.4)冻结。**地台面上零文字**——暖白 `#F0E2C8` 对地台板身 `#968264` 实测仅 2.89:1,已越 §7.2 的 4.5:1 硬指标,落法按 §7.2「文字禁直接压纹理底」(垫容器、素牌或罩层),非新条款;站桩文字位改垫素底或落进结算明细带(实装=站桩名字行垫 `HP_BAR_BACK` 深档素底牌、文字基线上移至地台顶上方——2026-10-30 落地)。验收:目检〈立绘脚下有地台/落影,非贴平色底〉+机检〈B 区站桩文字位正下方像素非地台板身色 #968264〉
- **明度分层适用**:背景氛围的情绪表达仍禁止以压暗/渐晕实现(§2 禁令);空间纵深与洞穴感走明度分层允许域——世界底层档、光照氛围、色相饱和与形体承载(§3.2 同款逻辑)。验收:方向性
- **图类型联动**:背景 tint 走 §4.3 `wall_ambient` 槽,随三关换挡(方向候选见 §6.3);紫赤仅 Boss 战允许短暂进入背景氛围纹样(§2.6 色相独占的舞台侧延伸,高饱和独占不变),非 Boss 战高饱和紫赤禁入。验收:机检〈背景吸色无非 Boss 高饱和紫赤〉
- **资产渠道**:✦ 插画级(演出类场景贴图,清单 §4 联动登记);战斗一屏样张(§1.4 清单 1.2)含背景出样。**资产契约(尺寸候选,样张冻结终稿)**:基准一张 = `scene_stage_bg.png`(源 1280×270,B 区全高含明细带底;三关换挡走 §4.3 `wall_ambient` tint 槽,不按板出三张——换皮成本复用棋盘同款逻辑),显示 1×;呈现约束按本节各条(低对比后退/明度世界底层档/光柱氛围保留)。程序化平色+光柱现行使用 = 占位;插画工单归 PRD,交付后 art-status-checklist 同步 ✅。**出图只出远景 + 中景两带**:结算期明细带底 84px 会盖住 B 区 78%–100% 段(约 59px),近景带在结算期整条不可见——画进背景等于画一件永远看不见的东西。远景/中景的构图占比归整屏样张(§1.4)。验收:目检〈背景插画内零地台结构件〉
+4 -3
View File
@@ -1,4 +1,4 @@
# 生图提示词卡(角色级资产)v2.9
# 生图提示词卡(角色级资产)v2.11
> **性质**:美术清单生图通道(✦ 用户自跑,现走线上 API)的执行侧文件——每资产一段**自包含完整提示词**:正向 = 通用底座+主体+动作已拼妥、负向 = 通用底座+增补已并妥,整段复制直接贴进工作流即可,**不需要读其他文件、不需要拼装**。人物规格转写自 [character-design.md](character-design.md)(人物设计 v1.0,自 art-bible §5 迁出,圣经 §5 为存根),风格/色彩条款转写自 [art-bible](art-bible.md)(§4.1 主色板 / §4.2 语义色规则);冲突时人物规格以 character-design 为准、色彩以 art-bible 为准。
> **边界**:本文件只管**角色级**资产的生图通道。图标/格面/样张走脚本管线(⚙ 代理出),规格在 art-bible §4.6 与 [art-assets-checklist](art-assets-checklist.md),不需要提示词。**场景级资产(营地场景图的背景层与八个物件层)另立 [camp-scene-prompts](camp-scene-prompts.md),本文件不覆盖**;两卡的角色/场景分工以「谁的资产谁出卡」为界,同一段提示词不两处维护。战斗舞台的场景级两件与护甲类型图标三枚的**出图工单**登记在本文件 §5(camp-scene-prompts 只管营地场景图,战斗屏这两类无处可落);§5 只把各权威节的定值收进逐件工单,不立新值。意图动画三档是动画资产,等一屏样张锁方案后另立规格卡,本文件不覆盖。
@@ -289,7 +289,8 @@ Seamless tileable pixel art texture of dry dirt ground, top-down, for a dungeon
> **范围注**:本文件原定位角色级(见文件头边界句),§4bis 已有「地形类同库登记」的先例,本节循同一先例补登记战斗屏两类——战斗舞台的场景级两件(① ②)与护甲类型图标三枚(③),两组在营地卡与清单里都无处落工单;角色级立绘重产(④)是本文件的本职。四组的规格定值清单 §4 已有登记,本节补的是「逐件照着就能出件」那一层。
> **定值纪律(§8 头注 定值单点纪律)**:本节**指路不立值**——色值 = 圣经 §4.1(含场景环境子板)、尺寸与缩放 = §8.2、明度带 = §2、甲档 = character-design §5.3、命名 = §8.3 + asset-path-map。本节新增零定值;凡圣经未定的值,一律进本节 §5.5 缺口,不在本节填空。
> **验收标注纪律**:逐条按三档标 `←机检: <断言>` / `←目检: <判据>` / `←方向性`,照圣经头注 2026-10-06 用户拍板的写法。
> **流程**:出图前后一律走 §0 五步;出图登记义务(模型名 + 分辨率 + seed + 对话/日期)照 remote-gen-prompts 头注,随候选图存 `artwork/candidates/<资产名>/`,缺登记的候选不入正稿遴选。
> **流程**:出图前后一律走 §0 五步;出图登记义务照 remote-gen-prompts 头注轻量口径(用途+生成时间必注,其余参数有则记),随候选图存 `artwork/candidates/<资产名>/`;登记不作正稿遴选前置。
> **产出状态**:组二地台件与组三护甲三枚已按各节 ⑤「程序化产出即终稿」产出入包(`scene_stage_plaza.png` 48×12、`ui_armor_light/ward/heavy.png` 24×24,路径见各组 ① 字段;登记=战斗一屏样张批,合并提交 3b66d1a6);组一背景插画与组四立绘重产仍待出图。
### 5.1 组一 · B 区舞台背景插画(1 件)
@@ -444,7 +445,7 @@ Do not add any of the following: realistic, photorealistic or 3D rendering; pain
1. 出件顺序 = 先主角、再三 Boss(Alpha 范围 = 主角 + 三 Boss 重产,其余敌种当前可读则保留到 Beta)。
2. **源密度判据 = 三判据②**:显示 96–188px 下脸、铲、旗、物种可认——照这条反推源档即可,逐键定档已完成(§8.2),本批不必再猜档位。
3. 手绘起稿顺序 = 剪影(§5.1 archetype)→ 体块主色 → 暗部与受光 → 描边 → 落影底盘。
4. 逐件 2–3 版候选;登记义务项 = 手绘者 / 日期 / 源档尺寸(手绘件无模型名与 seed,按此三项替换 §0 的登记项),随候选图存 `artwork/candidates/<资产名>/`。
4. 逐件 2–3 版候选;登记照 remote-gen-prompts 头注轻量口径(用途+生成时间必注,手绘作者有则记),随候选图存 `artwork/candidates/<资产名>/`。
5. 候选评审 = 并排横评,**隐去文件名与来源后按三判据逐条判读**(§0 五步第 1 步「便于横评」的手绘侧落法),不靠记忆与印象拍板。
6. 出件 = 透明 PNG(Alpha 直出,禁先出白底图再抠);入包前过 §8.3 色板检查器(报新增色 / 近色告警 / 超 48 上限拦截)。
+1 -1
View File
@@ -2,7 +2,7 @@
> **渠道定位**:生图渠道 = **远程对话式生图为主,本地 ComfyUI 为备选**;本文件是**对话操作单**(怎么开对话/贴什么参考图/怎么续写),提示词正文词库归 [art-prompts](art-prompts.md)——分工:词库改词库、操作单改流程,同一段正文不双处维护。
> **冲突优先级**:画法与色彩以 [art-bible](art-bible.md) 为准,人物规格以 [character-design](character-design.md) 为准;本单为执行侧操作单,不另立规格。
> **出图登记义务(复现性)**:每张产出登记 **模型名 + 分辨率 + seed(可取则取)+ 对话/日期**,随候选原图同存 `artwork/candidates/<资产名>/`;缺登记的候选不入正稿遴选。
> **出图登记义务(出处备查)**:每张产出随图注明 **用途**(备选/参考/其他什么用)与 **生成时间**(YYYY-MM-DD HH:MM 格式),随候选原图同存 `artwork/candidates/<资产名>/`;模型名、seed、分辨率等参数有则随手记、无则不填,分辨率这类文件属性里自带的值不必抄。登记不作正稿遴选前置——缺注明不落选,遴选时问清用途即可。
> 开新对话出图:同一角色 2–3 版候选各开新对话(同对话续写=修图);同角色换姿势在同对话续写。分辨率开大档;候选原图存 `artwork/candidates/<资产名>/`。
## 1. 主角战斗立绘
@@ -2,8 +2,8 @@
## 文档状态
- 版本:1.25
- 最近更新:2026-10-29(零豁免五键清偿设计闭合落文——S13 三条豁免登记注销〔main 组合根/battle_orchestrator 类变量/tune_sim 测量面〕;五案设计闭合、实现批建件:ui_registry 界面注册面、RoundCycle+BattleFacts、CellUnit 两绘制域件、StandeeBody 两状态件、tune_sim 信号步进状态机;gdstyle inner-class 计数截断缺陷登记;沿革查 git)
- 版本:1.26
- 最近更新:2026-10-30(迭代 42 反哺点落文——ShopSession 行对外暴露格按委托壳拆除后实测直连面重写:完整直连方法清单+set_shop_pool 调用面改述;沿革查 git)
- 引擎:Godot 4.7.2-stable(GDScript 单语言;2026-09-07 锁定)
- 评审模式:solo(`review-mode.txt`,2026-09-12 用户拍板——TD/LP 门禁按 solo 跳过,用户授权架构产出自主推进、迭代期回调)
- 覆盖 GDD:现行全集(根索引=`design/gdd/02-系统清单.md`,文档树构成以根索引为准)
@@ -106,7 +106,7 @@
| **reward_economy**(B4) | 格面资源身份映射(install 期预分配、Ready 只读、换图映射消亡)、极性路由字典面((类型, 极性)→ 字典行查询;路由执行归 B1 授予段)、F6 当量供给投影 | 封闭 API:roll_board / query_identity / query_full_snapshot / grant / grant_clear / retire_mapping / polarity_row / phase / weighted_pick;另有 is_mapped 静默布尔投影(批量工具用) | ConfigDB(RewardTable);BattleFxHost 经注入 Callable `luck_bias_query` 取偏运合计(reward_roll 发射与 bias_rewards 求和移至效果层完成);reward_roll 流(scope = 每次 install 批) | 无 |
| **helper_items**(A5,切片已实装秘宝域) | HelperItemTable、探测显影语义 | (契约规范源:`../../gdd/systems/跨局循环/秘宝.md`——桥接位:A2 查询接口 + A4 counter_item_ids) | A2 / A4 / B4 | 无 |
| **CurioDeck**(`core/curio_deck.gd`,A5 秘宝域令牌池件,迭代 37 自 AdventureRunner 拆出) | 三选一/商店共享池的持久多重集、抽签(候选抽取)、cap 库存与专长凭证三计数与判定唯一所有者——件只管池与判定,停点编排与兑付推进留冒险编排器 | `draw_candidates(channel)` 抽签;`consume_deck_token` / `consume_pool_purchase` 池消耗;`cap_ok` 上限判定;`feat_ticket_consider` 凭证判定;`stock_snapshot` / `pool_stock` 只读投影;商店购买消费经注入 Callable 链至 CurioStop 兑付单点(迭代 39 起转纯池——bind_handlers 反向回调已退役) | ConfigDB(秘宝令牌行集);注入 RngServiceCore(reward_pick 封闭流,禁自建随机) | 无(RefCounted 纯逻辑) |
| **ShopSession**(`core/shop_session.gd`,商店会话件,迭代 37 自 LevelRunner 拆出) | 关卡商店双模式(legacy 固定三件加定价/pool 共享令牌池)的货架、定价、购买与刷新唯一宿主——账本/钱包/装配集归宿主 LevelRunner(共享引用经 `bind` 注入,出账直接落同一引用改写宿主账本);legacy 模式零 RNG 零池消费(off 对照承载) | 货架与定价只读面(`shop_products` / `shop_shelf` / `current_price` / `shop_refresh_cost`)+写入口(`buy_product` / `buy_pool_item` / `shop_refresh` / `player_request_refresh` / `open_pool`);外部调用面 2026-10-29 D3 裁改直连(LevelRunner 同签名委托壳删除——并排卡「level_runner 商店委托层拆除」迭代 42 承接,调用方改址) | ConfigDB(商店行集);注入 RngServiceCore;宿主钱包/账本引用;pool 模式令牌消耗与判定经注入 Callable 转 CurioDeck | 无 |
| **ShopSession**(`core/shop_session.gd`,商店会话件,迭代 37 自 LevelRunner 拆出) | 关卡商店双模式(legacy 固定三件加定价/pool 共享令牌池)的货架、定价、购买与刷新唯一宿主——账本/钱包/装配集归宿主 LevelRunner(共享引用经 `bind` 注入,出账直接落同一引用改写宿主账本);legacy 模式零 RNG 零池消费(off 对照承载) | 货架与定价只读面(`shop_products` / `shop_shelf` / `current_price` / `shop_refresh_cost`)+写入口(`buy_product` / `buy_pool_item` / `shop_refresh` / `player_request_refresh` / `open_pool`);外部调用面=调用方经宿主 LevelRunner 公开成员 `shop` 直连(调用直连不留委托壳,2026-10-26 Middle Man 裁定;迭代 42 拆除),直连消费的方法=`shop_products`/`current_price`/`purchased_tier`/`buy_product`/`player_request_refresh`/`set_shop_pool`/`pool_price`/`shop_shelf`/`shop_refresh_free_left`/`shop_refresh_cost`/`shop_refresh`/`spend_gp`/`buy_pool_item`/`is_shop_pool`/`shop_bp_shelf`/`shop_bp_visit_buys`(另白盒两入口仅测试侧:`_draw_shelf`/`_spend_discounted`);set_shop_pool 调用面=pool 注入经 LevelRunner 构造 ctx 供给(构造期内部直调,原委托块注释「AdventureRunner._start_current_level 唯一调用方」系陈旧表述、随壳删除作废),测试面直连 `shop.set_shop_pool` | ConfigDB(商店行集);注入 RngServiceCore;宿主钱包/账本引用;pool 模式令牌消耗与判定经注入 Callable 转 CurioDeck | 无 |
| **HeroLedger**(`core/hero_ledger.gd`,冒险域主角实体账,已实装(2026-10-27 迭代 39 拆件批)) | 冒险生命周期主角数值与获得记录唯一所有者:生命与 GP 钱包(gain/spend/clear 单写原语,钱包事件发射点归件)、锻造强化快照、调试百分比、攻击/甲类型轴快照、特性持有集(活引用注入——关内兑付当关生效)、本局钥石增量与逐杀明细、真视使用记录;获得记录三层结构=获得记录列表(三选一件/商店购买/战末奖励各一条)→属性增益明细(带来源指回)→四轴汇总(由明细推导);生效上限与回血封顶公式单点归件 | 构造注入;单写原语族+只读投影;断点自交片段(账本键+获得日记键) | ConfigDB(表值读取);由 AdventureRunner 冒险开始创建、注入 CurioStop/level_runner/ShopSession/战斗 encounter | 无(RefCounted 纯逻辑) |
| **AttuneLoadout**(`core/attune_loadout.gd`,同调配装件,已实装(2026-10-27 迭代 39 拆件批)) | 装备中的秘宝件对象集唯一所有者(每件内含充能单位与重置周期);同调充能的物化行为唯一事实源——种行/补行改写时同步(RunLedger 充能字典轨与差额对账函数退役);旧档 aux_items 同调行迁移归件 | 构造注入;充能查询/消耗/按周期重置原语;断点自交片段(attunement_charges 键,键名与策略不变) | ConfigDB(秘宝行集);TreasureItems(同调行投放面);注入战斗使用路径 | 无 |
| **CurioStop**(`core/curio_stop.gd`,三选一停点件,已实装(2026-10-27 迭代 39 拆件批)) | 三选一停点编排唯一所有者(架构原则 9 本域承载):停点三态(关卡完成三选一在途/已兑付待推进/奖励节点三选一在途)、刷新三源账(刷新件 X/营地池 Y/刷新券存量/付费刷新计数)、兑付方法族(pick/refresh/redeem/skip)、凭证出箱(RAP/时沙待注入);三选一信号面自带;零自动推进停点语义承载 | 对外=开停点/刷新/兑付/放行谓词/交断点片段五组入口+信号面(curio_choice_presented/curio_resolved);由 AdventureRunner 关卡完成受理时调用 | ConfigDB;注入 CurioDeck(抽签/摘牌)与 HeroLedger(落账);随机只经 CurioDeck 的 reward_pick 封闭流 | 无 |
@@ -144,7 +144,7 @@
- **attune 族处置(2026-10-29 零豁免裁改判,原「原位保留」失效)**:attune 状态两成员归 BattleFacts、行为函数留宿主——迭代 39 副卡(AttuneLoadout 充能单源化)语义领地不破,外部三面消费点零改址;RewardGrant 经宿主 attune 读投影取 _attune_safe_seen 形态不变。
- **呈现零变化**:两快照出口签名与载荷逐字不变,内部改读两件只读投影(快照本体不读揭示态——揭示态投影消费方=宿主 A2 订阅旁路判定);treasure_interact_controller 三触点分家——use_treasure 经组合根现值回调改注 RewardGrant(每战新建编排器,禁一次性直引防悬挂),minesweeper 读与 attune_convert_state 仍走宿主取值回调(双取值面);nest_preview_count 直读面零变化。
- **S13 键账收敛义务**:12 个超标键随函数迁入新文件=基线外 NEW(use_treasure 长度/分支/局部/返回×4、_grant_routed 参数 8、_dispatch_row 参数 6、_apply_percent_row 参数 6、_apply_gp_row 参数 6、_apply_flat_row 参数 7+返回 9、_submit_flat_hero 参数 7、_fx_fact 返回 7)——卡内同步收敛为本波义务(_grant_routed 等签名随引用束注入改形、use_treasure 体拆分),禁波 0 合并 refresh 吸收新文件未还键;新件 _init 引用束注入防 max-parameters 新键。
- **验收红线**:游戏行为零变化(RNG 消费序零漂移——fx_reveal 封闭流随件迁移取号序不变、信号顺序契约冻结、明细行协议词表零触碰、同种子逐位一致);battle_orchestrator 头注发射点指址随拆件改写(词表不动,指址须真);行数恶化债清偿义务(迭代 38 特批带红合并挂账 c4c69701——本波还债归绿);PARITY 触面(battle_orchestrator.gd 被改)实跑对拍须经用户批准、不得借机重锚;S13 改善非清零口径(宿主残留 8–10 键+随迁键卡内收敛后新件零残留为达标线)。
- **验收红线**:游戏行为零变化(RNG 消费序零漂移——fx_reveal 封闭流随件迁移取号序不变、信号顺序契约冻结、明细行协议词表零触碰、同种子逐位一致);battle_orchestrator 头注发射点指址随拆件改写(词表不动,指址须真);行数恶化债清偿义务(迭代 38 特批带红合并挂账 c4c69701——本波还债归绿);S13 改善非清零口径(宿主残留 8–10 键+随迁键卡内收敛后新件零残留为达标线)。
- **S7/S8 随动**:RewardGrant 调 B4 API,两新件一并登记进 S7 B1 域文件集与 S8 结算路径 await 文件集(static_scan.sh 两清单独立、同批更新——防 await 面漏闸);轴丙 BoardInstaller 同规(调 B4 roll_board,实现批随批增行)。
### 冒险域拆分(现状登记——2026-10-26 设计闭合,2026-10-27 迭代 39 拆件批实装,本节各条即现行形态)
@@ -238,14 +238,14 @@
| **run_system_test**(系统测试运行器) | `tools/run_system_test.py` | 系统测试总入口:headless 跑真实玩法路径断言面(宿主=`res://tests/system_test.gd`)+ 三闸判定(口径同 run_smoke,摘要行兼容两种宿主格式) | 进程退出码(0=PASS / 1=FAIL) | 三闸齐备(同 run_smoke:退出码/摘要失败数/脚本错误扫描;wall-clock 超时在位) |
| **run_all**(单元与集成测试) | `game/tests/run_all.gd` | 64 套统一入口(61 单元+3 集成,2026-10-28 实数):套件 manifest 逐条「套件 → 期望退出码」,致命类 expect=1 独立子进程实跑;`negative` 字段标负面套件(仅豁免错误扫描一闸);选跑态 `--only=<词[,词…]>`(直跑形态对套件路径子串匹配只跑命中套件,零命中=用法错 quit(2),仅供开发期最窄确认——不产闸回执、不作验收凭据,批末全量闸不经选跑分支) | 逐套件结果 + 汇总退出码(任一套件失败 quit(1)) | 套件级退出码协议在位;三硬闸齐备(入口内扫子套件输出 + 编排层扫 `unit.log`;`Kit.finish` 零断言返回专用码 2)。子进程输出须经系统命令解释器重定向落盘再读——`OS.execute` 的输出管道在本机恒空 |
| **probe_gate**(场景探针) | `tools/probe_gate.sh` | headless 场景探针段编排:默认段 ui_input_probe / ui_struct_probe / layout_probe / ui_contrast_probe / **screen_flow_probe**(屏壳切换),`--click` 加跑 ui_click_probe,`--full` 加跑 ui_probe / ui_tune_probe / t17_defer_probe / fx_probe;孤儿 Godot 预清 + 批末残留清点(必须 0) | 段级 FAIL 输出 + 汇总退出码 | 每段 timeout 守卫在位(挂死 124/137 = FAIL,不静默);判定统一走 `verdict` 函数——退出码为唯一权威判据,PASS 串降级为附加判据,逐段跑 `tools/error_scan.py` |
| **render_gate**(视觉 golden) | `tools/render_gate.sh` | 窗口化渲染闸:①shot_probe 七屏截图 → shot_checker A-D 语义断言;②fx_shot_probe / board_shot_probe 取证(超时/零字节/文件缺失任一 = FAIL);③棋盘 golden 逐像素容差比对(差 ≤16 / changed ≤2%,provisional)+ 菜单 golden;④批末孤儿残留清零 | 段级 FAIL 输出 + 汇总退出码 | 查退出码在位(每段 timeout 守卫 + 退出码/取证文件双检);**SCRIPT ERROR 扫描缺失**;断言数下限不适用(判定主体 = golden 容差与取证文件存在性,非断言计数) |
| **render_gate**(视觉 golden) | `tools/render_gate.sh` | 窗口化渲染闸:①shot_probe 七屏截图 → shot_checker A-D 语义断言;②fx_shot_probe / board_shot_probe 取证(超时/零字节/文件缺失任一 = FAIL);③棋盘 golden 逐像素容差比对(差 ≤16 / changed ≤2%,provisional)+ 菜单 golden;④批末孤儿残留清零;⑤三段探针日志逐段脚本错误扫描 | 段级 FAIL 输出 + 汇总退出码 | 查退出码在位(每段 timeout 守卫 + 退出码/取证文件双检);SCRIPT ERROR 扫描在位(逐段 `tools/error_scan.py`,标记与 run_smoke 同源);断言数下限不适用(判定主体 = golden 容差与取证文件存在性,非断言计数) |
| **static_scan**(静态扫描域) | `game/tests/static_scan.sh` | 静态扫描域 S1–S10 可执行载体(grep 规则 + `tools/audit_type_hints.py`〔S9〕+ `tools/audit_file_headers.py`〔S10〕) | fail 聚合退出码(任一硬失败规则命中即 exit 1) | 查退出码在位;SCRIPT ERROR 扫描与断言数下限不适用(逐规则命中即红,无运行时错误面与断言计数);S3 现为 warning 级、不参与 fail 判定(见下表) |
### 改动域划分(2026-10-17 用户裁「代码批只跑代码闸、文档批只跑文档闸」)
| 域 | 闸 | 本批改动落在这些路径即属该域 |
|---|---|---|
| **代码域** | run_smoke、run_system_test、run_all、probe_gate `--click`、probe_gate `--full`、render_gate、static_scan、回放确定性对拍(条件件) | **唯一触发=`game/` 下非 `.md` 的行为载体**(代码、测试脚本、探针、场景、数据表、素材)。`game/` 内的 `.md`(README、生成清单)归文档域——2026-10-26 用户裁:判据按文件性质,不按路径前缀。2026-10-18 用户裁「跨领域执法禁绝」(提交 `7bdf53e3`)后,原 `tools/`、`artwork/` 两前缀不再把改动拉进代码域 |
| **代码域** | run_smoke、run_system_test、run_all、probe_gate `--click`、probe_gate `--full`、render_gate、static_scan | **唯一触发=`game/` 下非 `.md` 的行为载体**(代码、测试脚本、探针、场景、数据表、素材)。`game/` 内的 `.md`(README、生成清单)归文档域——2026-10-26 用户裁:判据按文件性质,不按路径前缀。2026-10-18 用户裁「跨领域执法禁绝」(提交 `7bdf53e3`)后,原 `tools/`、`artwork/` 两前缀不再把改动拉进代码域 |
| **文档域** | gdd_lint、doc_link_audit、doc_term_audit、流水线闸 | `game/` 内的 `.md` 与 `game/` 外的一切路径:`design/`、根规则文件(`AGENTS.md`/`roadmap.md`/`待办清单.md`/`体验验证清单.md`)、`tools/` 工具脚本、`.zcode/` 配置、`sim-data/`、`reports/` |
| **两域都含** | repo_structure_audit(熵增闸:顶层目录/根文件/图像白名单/退役路径/design 子目录五查——文档批新增或挪动 `.md` 同样会绊它;纯文本扫描秒级) | — |
@@ -267,7 +267,7 @@
| 硬闸 | run_smoke | run_system_test | run_all | probe_gate | render_gate | static_scan |
|---|---|---|---|---|---|---|
| **退出码优先** | 已落实(闸①) | 已落实(闸①,口径同 run_smoke) | 已落实(套件 manifest expect 协议) | 已落实(`verdict` 函数逐段判退出码,PASS 串不作判据) | 已落实(段级退出码 + 取证双检) | 已落实(fail 聚合退出) |
| **SCRIPT ERROR 扫描** | 已落实(闸③,外部扫描——GDScript 无脚本错误陷阱 API,闸设在外部包装) | 已落实(闸③,标记清单与 run_smoke 同源) | 已落实(入口内扫子套件输出;子进程的 stdout/stderr 须先落盘——`OS.execute` 管道在本机恒空) | 已落实(逐探针段 `tools/error_scan.py`,标记与 run_smoke 同源) | **缺失**(本批未收——判定主体是 golden 容差与取证存在性;登记待办清单) | 不适用(静态域无运行时错误面) |
| **SCRIPT ERROR 扫描** | 已落实(闸③,外部扫描——GDScript 无脚本错误陷阱 API,闸设在外部包装) | 已落实(闸③,标记清单与 run_smoke 同源) | 已落实(入口内扫子套件输出;子进程的 stdout/stderr 须先落盘——`OS.execute` 管道在本机恒空) | 已落实(逐探针段 `tools/error_scan.py`,标记与 run_smoke 同源) | 已落实(三段探针日志逐段 `tools/error_scan.py`,标记与 run_smoke 同源) | 不适用(静态域无运行时错误面) |
| **断言数下限** | 无独立下限(闸②摘要行必须在案,防「跑一半仍 PASS」;脚本错误吞断言路径由闸③兜住) | 同 run_smoke(过渡期宿主摘要行必须在案) | 已落实(`Kit.finish` 零断言返回专用退出码 2——与「断言失败=1」区分,防致命类套件的 expect 协议放过) | 已落实(退出码 0 但无该探针签名标记亦判红——防静默退出) | 不适用(golden 容差 + 取证存在性判定) | 不适用(逐规则命中判定;另有扫描面为空即判红的零命中自检) |
背景事实(run_smoke 头注在案):GDScript 运行时脚本错误不改进程退出码、不计入冒烟失败数、且静默中断所在函数剩余断言——2026-09-19 状态记录 83 实证「309 项 0 失败」实为断言未跑全。这是 SCRIPT ERROR 扫描必须存在于判定层的原因。
+1 -1
View File
@@ -48,7 +48,7 @@
7. **验收改动先行声明**——验收变化(基线换代/探针增改/旧断言改判)预先写进需求卡片,禁事后补
**完成**
8. **批末按域验收才合并(worktree 批;直改车道无批末动作,验收在 commit 点由 git 钩子执行,见 §1 第 3 条)**——在任务 worktree 内跑 `tools/run_all_gates.sh`,**按本批改动域选档**:按文件性质判——代码面=`game/` 下非 `.md` 的行为载体(代码、测试脚本、探针、场景、数据表、素材)→ `--code`;文档件=`game/` 内 `.md`(README、生成清单)与 `game/` 外一切路径(`design/`、根规则文件、`tools/` 工具脚本、`.zcode/` 配置等)→ `--docs`;两类同批都有 → 全量档不带参数。代码域按**关联档选测**(2026-10-29 用户裁):02 单元与集成只跑「需求卡片申报 ∪ diff 推导」并集的套件——申报=在册需求卡片对照表「验收测试」列提到的测试文件名,推导=闸脚本内套件族映射表按改动文件归族;改动命中公共件/数据表/autoload/数值·存档·RNG·结算触面或映射不到 → 横切回落全部套件;00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降;03/04 探针与 05 渲染闸维持影响面口径(本批 game/ 改动全在测量面 game/tests、game/sim 时记 SKIP)。所选域全绿即可合回 master,全量回归由用户自发发起(合入后无自动兜底)。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。该脚本是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。跑完写回执 `game/tests/.gates/receipt.json`(含 `domain`、`scope`(full|related,02 跑子集=related)与 `dirty_game` 字段),合回时由 worktree-guard 按 diff 文件性质推域强制校验(六况拦截:缺回执/quick 模式/回执未覆盖分支尖=回执 commit 之后代码面又有改动、或跑闸时代码面带着未提交改动(`dirty_game`)/域不覆盖本次改动/含新增红/master HEAD 非回执 commit 祖先=合并将产出未测内容;guard 只验域覆盖与全绿,不复核选套件清单)。回执与分支尖的对齐判据是**代码面**不是 commit 号:回执之后只追加文档件(`game/` 内 `.md`、`reports/`、`design/` 等)提交,回执照样有效,不构成复跑理由。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占合回全量——其质量义务=落批时人工确认闸可跑通且判据口径未变,闸真崩溃时合回闸 fail-closed(跑不出回执即拦),不以合回全量为兜底
8. **批末按域验收才合并(worktree 批;直改车道无批末动作,验收在 commit 点由 git 钩子执行,见 §1 第 3 条)**——在任务 worktree 内跑 `tools/run_all_gates.sh`,**按本批改动域选档**:按文件性质判——代码面=`game/` 下非 `.md` 的行为载体(代码、测试脚本、探针、场景、数据表、素材)→ `--code`;文档件=`game/` 内 `.md`(README、生成清单)与 `game/` 外一切路径(`design/`、根规则文件、`tools/` 工具脚本、`.zcode/` 配置等)→ `--docs`;两类同批都有 → 全量档不带参数。代码域按**关联档选测**(2026-10-29 用户裁):02 单元与集成只跑「需求卡片申报 ∪ diff 推导」并集的套件——申报=在册需求卡片对照表「验收测试」列提到的测试文件名,推导=闸脚本内套件族映射表按改动文件归族;改动命中公共件/数据表/autoload/数值·存档·RNG·结算触面或映射不到 → 横切回落全部套件;00 冒烟/01 系统测试/09 静态扫描三件保险恒跑不降;03 场景探针与 05 渲染闸维持影响面口径(本批 game/ 改动全在测量面 game/tests、game/sim 时记 SKIP)。所选域全绿即可合回 master,全量回归由用户自发发起(合入后无自动兜底)。合回前独立验收审计通过(iter 技能第 5 步),未过不合回。该脚本是全部验证闸的**唯一入口、唯一清单与唯一域划分(含选闸算法与套件族映射表)**,闸件增减、分域调整都只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。跑完写回执 `game/tests/.gates/receipt.json`(含 `domain`、`scope`(full|related,02 跑子集=related)与 `dirty_game` 字段),合回时由 worktree-guard 按 diff 文件性质推域强制校验(六况拦截:缺回执/quick 模式/回执未覆盖分支尖=回执 commit 之后代码面又有改动、或跑闸时代码面带着未提交改动(`dirty_game`)/域不覆盖本次改动/含新增红/master HEAD 非回执 commit 祖先=合并将产出未测内容;guard 只验域覆盖与全绿,不复核选套件清单)。回执与分支尖的对齐判据是**代码面**不是 commit 号:回执之后只追加文档件(`game/` 内 `.md`、`reports/`、`design/` 等)提交,回执照样有效,不构成复跑理由。闸件(`tools/` 下机检脚本与闸壳)自身的改动不占合回全量——其质量义务=落批时人工确认闸可跑通且判据口径未变,闸真崩溃时合回闸 fail-closed(跑不出回执即拦),不以合回全量为兜底
9. **合并后主区零复跑**(2026-10-24 用户裁:合回重跑验的是与回执同一份字节,零信息)——合回落地后 code 批只做两件秒级事:`--editor --quit` 重刷类名缓存(在册坑:.godot 缓存不入 git,与合并后代码失配会假红假绿)+冒烟启动确认可开机(合计约 1 分钟);docs 批零动作。全量验证**每批跑一次**=worktree 批末按域闸,顺序是「先并入 master、再跑闸」(§1 第 2 条);只有两种情形允许把这次跑作废重来——① 闸红且修法动了 `game/`,② 闸已开跑后 master 又被推进(合并将产出未测内容,由合回闸祖先判定拦下,§3.9 第六况)。「跑都跑了再跑一遍保险」「闸后顺手改点 `game/` 再补一跑」都不构成复跑理由;master 上永不跑全量
10. **批次完成后的登记义务**——合回 master 后按 doc-conventions §12.1 推进待办清单条目状态(做完整条删除),涉及迭代编排的在 roadmap 迭代总表与顶部「当前位」节换代——执行入口=项目技能 batch-closure(批后收尾与中间文档退役),跨会话教训写记忆库。批次过程与结论由 git 提交信息承载,禁止再写会话状态文件(见 doc-conventions 第 10 节)。`design/` 与根规则文件(`AGENTS.md`/`roadmap.md`/`待办清单.md`/`体验验证清单.md`)实现批禁写,见 §12
11. **文档同步闸(实现批禁写文档)**——实现批**禁写 `design/` 整个目录与根规则文件**,**另禁写 `game/tests/README.md`(能力清单,归写卡段维护,test-conventions §8.9)**,要往外交代的东西一律落 `reports/`:实现报告 `reports/iter<N>-实现报告-<日期>.md`(各卡状态+实机目检总清单+遗留读数义务+实现决定记录——超白名单/超卡片明列范围事项逐条记「决了什么/为什么/代价/撤销方式」,全部按卡执行时只写一行「零拍板」)、GDD 反哺点清单 `reports/iter<N>-反哺点-<日期>.md`(每条写清哪个文件哪一节、改成什么、依据是什么)。设计权威文档(GDD/art-bible)**任何批都不直改**:用户在会话中逐点裁决后,由工程/文档设计改动批(直改类)写入 `design/`(执行入口=doc-design 技能),并把反哺条目从 `reports/` 清掉(吸收即死;退役执行=batch-closure 技能)。机械面文档(roadmap 行/术语表/README 工具注)同样归工程/文档设计改动批在合并前更新。反哺点未落地期间相关 GDD 节视为待反哺,读前先查 `reports/` 与待办清单;需求卡片必列「关联文档同步面」按两层标注
+2 -2
View File
@@ -35,7 +35,7 @@
需求卡片 §8「改动—验收对照表」每一条都要落成可执行的验证;本文定统一写法,卡里定每批的具体内容(测什么/注入哪里)。写卡期查不出执行载体的条目=不可测,当场呈报,禁拖到实现期。
1. **执行载体五选一,逐条声明**:单元测试(`tests/run_all`)/系统测试断言(`tests/system_test.gd`)/场景探针(`probe/` 下 `*_probe`)/MC 同种子逐项比对/人工核对(仅限无法自动化的纪律类需求点,如命名锁定、数值占位)。行为类条目以自动化载体为主。
2. **粒度与对应关系**:每条清单条目至少一个自动断言,且断言必须从该条目的真实入口进入;多条条目可合用一个测试函数,但撤销对应改动时每条都要能单独变红。断言失败消息末尾带编号尾缀〔卡短名 Vn〕(如〔秘宝批 V3〕)——断言与卡片清单可互相 grep 查找。**工具面局限备案(2026-10-27 迭代 39 验收审计)**:`Kit.check_quiet` 静默收集形态(计数不入判定即打印、失败消息进 problems 汇总)的断言消息不进 `tools/extract_checks.py` 扫描面——验收尾缀 grep 对该通道失效;如需覆盖属提取器改版(工程批)。
2. **粒度与对应关系**:每条清单条目至少一个自动断言,且断言必须从该条目的真实入口进入;多条条目可合用一个测试函数,但每条都要有自己单独可判红的判据(取证口径见 §8.4 唯一口径——禁变异/禁撤销)。断言失败消息末尾带编号尾缀〔卡短名 Vn〕(如〔秘宝批 V3〕)——断言与卡片清单可互相 grep 查找。**工具面局限备案(2026-10-27 迭代 39 验收审计)**:`Kit.check_quiet` 静默收集形态(计数不入判定即打印、失败消息进 problems 汇总)的断言消息不进 `tools/extract_checks.py` 扫描面——验收尾缀 grep 对该通道失效;如需覆盖属提取器改版(工程批)。
3. **断言词汇按验证对象五选一**:
| 词汇 | 适用场景 |
@@ -67,7 +67,7 @@
- 「跑一遍没崩=通过」无效:测试绿只证明**当前代码没犯这个错**,不证明**犯了这个错会被抓住**;断言存在不等于断言执行。
- **脚本错误会静默中断所在函数的剩余断言,而套件仍报全绿**(机制事实):GDScript 无脚本错误陷阱 API,错误不改进程退出码、不计失败数。三处实证:①2026-09-19「309 项 0 失败」实为断言未跑全(状态记录 83);②敌人表行集纪律 10 条断言因定型数组赋值错误从未执行(迭代 29 修复后两条正向见红=夹具本缺锚行);③`Callable` 形参类型写错致解析函数返回 null、后续断言整段蒸发。
- 由此产生两条义务:**写闸件**按 §6 配三硬闸;**写验收函数**必须自证断言全数执行——真实入口断言在当轮执行并通过(§8.4)为主证,零命中自检(扫描面为空即判红)为辅证。
- **覆盖率不是目标**:行覆盖率度量缺失(Godot 无内建,第三方属新基建,是否引入待办清单在册待裁);现行补洞=零指名单测清单+人工判定,判定可靠性靠上列自证手段,不靠覆盖率数字。
- **覆盖率不是目标**:不引入行覆盖率度量(Godot 无内建,第三方属新基建);现行补洞=能力清单覆盖域表+人工判定,判定可靠性靠上列自证手段,不靠覆盖率数字。
## 8. 改动级验收闭合(2026-10-21 立)
@@ -1,7 +1,7 @@
# 战斗表现
> 一句话:战斗动画与演出(旧体系未单独成文)
> 状态:范围草案(待设计);现行为 v1.1
> 状态:范围草案(待设计);现行为 v1.2
## 设计目的
@@ -92,6 +92,8 @@
判定与回退:第 1 项不过即演出元素回炉,禁止以牺牲可读性换氛围(改演出、不动数字口径);第 2–4 项不过对应资产或方案回炉重交,已过项结果保留。第 5 项(敌层插入位置)不产生过/不过,产出一条裁决记录:插入位置结论由[敌人系统](敌人系统.md)的呈现契约登记(~~敌人卡形态对照~~半边已于 2026-09-20 提前裁决完成——全站桩定稿,结论已回写 battle.md 起草条款)。样张通过 = 角色剪影、动画方案、战斗空间、敌层插入位置四件锁定物冻结;通过前不启动任何终稿资产与动画设计。
**现行状态(2026-10-30 战斗一屏样张批)**:五项验收的机检断言已随该批全绿(机器代理口径与实测读数查战斗一屏样张批 git 提交史,合并提交 3b66d1a6);样张整体过闸仍差方向稿目检先行(art-bible §1.4 闸形,目检项登记体验验证清单 ㊶)。
### 演出服从信息与可读性
两条现行硬约束,将来任何动画方案都禁止越过。其一,数字可读性高于氛围:挖掘期画面无杂音,动画禁止遮挡数字、禁止在挖掘期抢焦;结算期的色彩与能量走向以 art-bible 第 2 节为权威。其二,结算逐行兑现是信息载体:battle.md 已承诺不可整体跳过、只提供减动效选项(时长减半或直达终态,逐行兑现可加速不可省略),本文将来兑现其动画侧。
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

+48
View File
@@ -0,0 +1,48 @@
## 战斗事实件:战斗事实的唯一记录面——本战受理事实(encounter:打什么、带什么进来)、
## 同调装配事实(attunement)、轮 facts 缓冲(reveals/seals/path)、封印判定账(去重两集+完美三账)、
## 装板溯源(seq/provenance)、条件事实计数(refresh_used、feat_first_dig_used、attune_safe_seen)。
## 零注入引用、零信号、零协作对象调用——纯数据+谓词+写口;缓冲写入点在宿主 A2 处理器,
## 身份快照捕获留宿主(fact 件不持协作引用,防它变成第二编排面)。
## 完美账写口单点=三 mark 方法(契约 2:perfect_hit_false 保持纯净,豁免只累加 perfect_forgiven);
## 其余成员公有直读写(段类/快照/宿主经 oc._facts.X——fx_host._fx_bonus_delta 跨件直读写同款形态)。
## reset_round 只清 reveals/seals/path 三项、不碰 refresh_used(_battle_over 后快照读到的
## refresh_used 必须保持末轮值)。
## 成员冻结:15/15 贴顶——实现期禁增员;扩容阀=先把封印判定账五成员二次拆出(SealLedger)再落新账。
class_name BattleFacts
extends RefCounted
var encounter: Dictionary[String, Variant] = {} # 本战受理事实(begin_battle assign 后全生命期只读)
var reveals: Array[Dictionary] = [] # 轮 facts 缓冲(cell_revealed 批)
var seals: Array[Dictionary] = [] # 封印判定批缓冲(seal_judged 批)
var path := "" # 轮末路径事实(round_end 载荷 path_tag)
var refresh_used := 0 # 本轮刷新计数(复位点=出口分发,语句随迁)
var board_seq := 0 # 板序号(本战斗第几张板——快照归因用)
var board_from_midround := false # 装板 provenance:轮内刷新换来的图=刷新图(清图奖励不发)
var seal_consumed := {} # 封印兑付资格账(危险格实例为键;install 重置)
var seal_miss_seen := {} # 封印未中行去重(每安全格实例一次)
var perfect_hit_true := 0 # 整图完美封印计数(install 重置)
var perfect_hit_false := 0 # 完美误标计数(纯净禁回退改写——契约 2)
var perfect_forgiven := 0 # 完美保险豁免累计(单向累加自有账)
var feat_first_dig_used := false # 条件事实计数(魔矢每战首次)
var attunement: Array[Variant] = [] # 本战装配集快照(enc.attunement 受理携带;受理后只读)
var attune_safe_seen := 0 # 积点制件安全开格计数(装配集自动兑付进度)
# 轮复位(原宿主 _reset_round_facts 逐字——三调用点:begin_battle 首板 install 后/出口分发/终局序列)
func reset_round() -> void:
reveals.clear()
seals.clear()
path = ""
# 完美封印三账写口(授予段直写升级为账本单点——契约 2 执行点)
func mark_perfect_hit() -> void:
perfect_hit_true += 1
func mark_perfect_miss() -> void:
perfect_hit_false += 1
func mark_perfect_forgiven() -> void:
perfect_forgiven += 1
+1
View File
@@ -0,0 +1 @@
uid://bo0eolh0vmvgb
+100 -323
View File
@@ -3,7 +3,7 @@
## 效果引擎施加面归 BattleFxHost、奖励授予管线归 RewardGrant(两拆件,经组件出口 fx_host()/
## reward_grant() 取件,禁链式穿透其内部);本类保留宿主职责面=状态机与生命周期、轮 facts 缓冲、
## A2 三事件订阅、五段驱动、attune 族、行发射基建(_emit_row/_assert_row_source/_fatal)、呈现投影、刷新面。
## 零 await;随机只经 rng 封闭流(encounter/behavior_roll/armor_roll/gp_drop)——同种子同输入逐位复现。
## 零 await;随机只经 rng 封闭流(_facts.encounter/behavior_roll/armor_roll/gp_drop)——同种子同输入逐位复现。
## 呈现层读取唯一出口 = presentation_snapshot()/diagnostic_snapshot(),禁链式穿透内部协作对象。
## 伤害只向 DamageController 开单(何时开单归编排、怎么算归控制器);刷新唯一通道 player_request_refresh,禁 RAP 满自动刷。
class_name BattleOrchestrator
@@ -34,71 +34,56 @@ const SOURCE_EXEMPT_TYPES := ["decay", "verdict"]
# fx_grant_gp=专长·GP 入账(钱包面而非棋盘面,行无格子可指)
const SOURCE_EXEMPT_CLASSES := ["hero_attack", "enemy_action", "clear_reward", "fx_reveal", "fx_grant_gp"]
var _facts: BattleFacts # 战斗事实件(battle_facts.gd——事实族 15 成员单点)
var _rounds: RoundCycle # 轮循环驱动件(settlement/round_cycle.gd——五段持有/段序/重入闸/摇号)
var db: ConfigDb
var rng: RngServiceCore
# E1 封装收口:协作对象 board_generator/enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/diagnostic_snapshot() 成文出口。
# E1 封装收口:协作对象 enemy_system/status_effects/reward_economy 私有化——跨层读取一律经 presentation_snapshot()/diagnostic_snapshot() 成文出口。
# minesweeper/action_economy 保持公有 = 架构文档 API Boundaries 的 HUD框架消费面(意图提交 settle_batch + compute_move_path
# 只读;ap/rap/相位读取)——非本文档登记的契约面,勿增新用途。
var _board_generator: BoardGenerator
# 装板域(BoardGenerator+装板六函数族)已拆 BoardInstaller——生成器实例随件自建,宿主零成员。
var minesweeper: MinesweeperRules
var action_economy: ActionEconomy
var _enemy_system: UnitContainer
var _status_effects: StatusContainer
var _reward_economy: RewardEconomy
var treasure_inventory: TreasureItems = null:
set(v):
treasure_inventory = v
if _reward_grant != null:
_reward_grant.treasure_inventory = v # 库存注入转发(注入时序在宿主构造之后——外部赋值路径零改址)
var _damage: DamageController # 伤害控制器(单据唯一兑付管线)
var _fx_host: BattleFxHost # 效果引擎宿主件(fx 族持有与兑付——拆件定稿轴件一)
var _reward_grant: RewardGrant # 奖励授予件(授予管线与秘宝使用——拆件定稿轴件二)
var encounter: Dictionary[String, Variant] = {}
var round_index := 0
var battle_result := "" # "" / "victory" / "defeat" / "fatal_error"
var _facts_reveals: Array[Dictionary] = []
var _facts_seals: Array[Dictionary] = []
var _facts_path := ""
var _settling := false
# 结算五段 Segment(段边界=审计五段,段序骨架冻结于
# SettlementTable.segment_order 语义:授予→增益/减益→战斗→衰减→判定。本类做段序编排+
# 行发射/开单原语单点——段体逻辑归 game/core/settlement/ 五类)
var _grant_seg = null # GrantSegment(①授予段——含封印判与 counter/grant 助手单点)
var _aura_seg = null # AuraSegment(②增益/减益段)
var _combat_seg = null # CombatSegment(③战斗段)
var _decay_seg = null # DecaySegment(④衰减段)
var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归本类)
var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_cap_per_round)
var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用)
var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励)
var _seal_consumed := {} # 兑付资格以危险格实例为键(整图;换图重置)
# 同调装配运行态(begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动)
var attunement: Array[Variant] = [] # 装备中同调件 item_id 集
var _mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(触发条件事实面——本场累计/上轮转存/当轮累计三读数合一账)
# 转存时点=_roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle
var _attune_safe_seen := 0 # 积点制件 battle 域安全开格计数(装配集 ap_charge_safe 行扫描;满 charge_n 自动兑付当轮 AP+1,无玩家发起入口)
# 同调装配运行态(begin_battle 经 enc 携带 attunement 键注入 facts;off=空集全部效果零扰动)
# 转存时点=roll_round_actions 轮末(轮内部分结算不更新 last);开战清零归 begin_battle
var nest_preview_count := -1 # 巢穴预告读数口(board_installed 命中落值;-1=未装配/未发射;呈现桥消费)
# 特性运行态(relic 持有集经 enc.feats 注入:效果行为=数据行 triggers 经
# 效果引擎分发;魔矢每战首次计数 battle 域。空集=零扰动)
var _feat_first_dig_used := false
# 加算日记读面(begin_battle 经 enc["journal"] 派生——encounter.assign 后全生命期可读;
# 主角攻击基值派生读(enc 携带 hero_atk 键受理后只读——E4 收口语义不变:表值运行期不可变,
# 派生值恒等;「读面即引用、无第二账」同款先例)
func _hero_atk() -> int:
return int(_facts.encounter.get("hero_atk", int(db.combat.hero.atk)))
# 加算日记读面(begin_battle 经 enc["journal"] 派生——encounter 受理 assign 后全生命期可读;
# null=调试/单场直构面,账面消费跳过。领域拆分批成员删除后的引用承接,读面即引用、无第二账)
func _journal() -> GainJournal:
return encounter.get("journal", null) if encounter.get("journal", null) is GainJournal else null
return _facts.encounter.get("journal", null) if _facts.encounter.get("journal", null) is GainJournal else null
# 同调配装件读面(begin_battle 经 enc["loadout"] 派生——充能单源化的 reset_cycle/consume_charge
# 消费面;null=直构零装配,充能面跳过)
func _attune_loadout() -> AttuneLoadout:
return encounter.get("loadout", null)
return _facts.encounter.get("loadout", null)
# 授予件战斗域回调束(bind_battle 注入——键集封闭三键:充能消耗/真视计数/负向踩雷计数;
# 宿主侧各转 _attune_loadout/_journal/_mines_hits 单点,授予件对账本零强引用)
# 宿主侧各转 _attune_loadout/_journal/_rounds.mines_hits 单点,授予件对账本零强引用)
func _battle_hooks() -> Dictionary[Variant, Variant]:
return {
"attune_consume": _hook_attune_consume,
@@ -108,8 +93,8 @@ func _battle_hooks() -> Dictionary[Variant, Variant]:
func _hook_attune_consume(item_id: String) -> void:
if _attune_loadout() != null and treasure_inventory != null:
_attune_loadout().consume_charge(item_id, treasure_inventory)
if _attune_loadout() != null and _reward_grant.treasure_inventory != null:
_attune_loadout().consume_charge(item_id, _reward_grant.treasure_inventory)
func _hook_vision_add() -> void:
@@ -120,8 +105,8 @@ func _hook_vision_add() -> void:
# 负向 blast(雷)踩雷计数入账(触发条件事实面——正向 spark_aoe/清图奖励不计入;
# 极性判定在授予件 _flat_blast 传点,本口只记账)
func _book_negative_blast() -> void:
_mines_hits["round"] += 1
_mines_hits["total"] += 1
_rounds.mines_hits["round"] += 1
_rounds.mines_hits["total"] += 1
# 效果引擎宿主侧事实单点(fx_host facts_cb 注入——round_index/battle_index/seals_count/nest_count/hp_ratio 五事实;charge_layers 归 fx_host 自答;未知事实回落 null)
@@ -136,10 +121,10 @@ func _fx_host_fact(fact: String) -> Variant:
"round_index":
return round_index
"battle_index":
return int(encounter.get("battle_index", 1)) # 关卡域战斗序(1 基——level_runner enc 携带;开局姿态窗条件面)
return int(_facts.encounter.get("battle_index", 1)) # 关卡域战斗序(1 基——level_runner enc 携带;开局姿态窗条件面)
"seals_count":
var hits_n := 0
for fevt in _facts_seals:
for fevt in _facts.seals:
if bool(fevt.get("hit", false)):
hits_n += 1
return hits_n
@@ -151,7 +136,7 @@ func _fx_host_fact(fact: String) -> Variant:
# attune 积点读投影(RewardGrant attune_safe_seen_cb 注入面——use_treasure 拒绝文案读数;经读投影出户防组件回调宿主私有族)
func _attune_safe_seen_read() -> int:
return _attune_safe_seen
return _facts.attune_safe_seen
# 组件出口(拆件后取件面——呈现与测试经此取件,禁链式穿透协作对象内部)
@@ -164,7 +149,7 @@ func reward_grant() -> RewardGrant:
func _attune_has(item_id: String) -> bool:
return attunement.has(item_id)
return _facts.attunement.has(item_id)
# 装配集只读出口(_attune_has 契约化出口,save v6 只读访问器先例。只读零副作用——
@@ -179,7 +164,7 @@ func attune_has(item_id: String) -> bool:
# kind 分发查询先例=_attune_refresh_x_total bonus_refresh。零装配=0)
func attune_param_sum(kind: String) -> int:
var total := 0
for iid_v in attunement:
for iid_v in _facts.attunement:
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
if (arow.get("effect_kinds", []) as Array).has(kind):
total += int(arow.get("magnitude", 0))
@@ -189,7 +174,7 @@ func attune_param_sum(kind: String) -> int:
# 装配集内 ap_charge_safe 行读取(取数源=装配集行扫描;
# 首行语义=装配集内至多一件积点制〔试炼镐〕)
func _attune_charge_safe_row() -> Dictionary:
for iid_v in attunement:
for iid_v in _facts.attunement:
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v))
if (arow.get("effect_kinds", []) as Array).has("ap_charge_safe"):
return arow
@@ -200,9 +185,9 @@ func _attune_charge_safe_row() -> Dictionary:
# (used handle 不回收=本轮已耗充能作废,补新 handle=「回满」;差额注入防双份=level_runner
# set_attune_charges 同构)。reset ∈ {battle, level}——adventure 周期归冒险开始种子面)
func _reseed_attune_charges(reset: String) -> void:
if _attune_loadout() == null or treasure_inventory == null:
if _attune_loadout() == null or _reward_grant.treasure_inventory == null:
return # 直构零装配/库存未就位——充能回满跳过(空装配同语义)
_attune_loadout().reset_cycle(reset, treasure_inventory) # 写时种行/补行(按周期筛——语义逐字承原循环体)
_attune_loadout().reset_cycle(reset, _reward_grant.treasure_inventory) # 写时种行/补行(按周期筛——语义逐字承原循环体)
# 积点制件进度读数(自动兑付无玩家发起入口,读数口=C 区秘宝槽悬停浮窗进度行:
@@ -213,30 +198,25 @@ func attune_convert_state() -> Dictionary:
if row.is_empty() or String(row.get("form", "")) != "active_charged":
return {"assembled": false, "points": 0, "need": 0, "display_name": "", "item_id": ""}
var cn := int(row.get("charge_n", 0))
return {"assembled": true, "points": _attune_safe_seen % cn if cn > 0 else 0, "need": cn,
return {"assembled": true, "points": _facts.attune_safe_seen % cn if cn > 0 else 0, "need": cn,
"display_name": String(row.get("display_name", "")), "item_id": String(row.get("item_id", ""))}
# 特性求值(效果行为=数据行 triggers 订阅,效果引擎 dispatch 分发;
# 流派档位质变=FeatsEngine 纯函数求值
# (档位机制本身系引擎基建非内容效果),两运行态注伤害控制器:档一 rider+1 / 档二 rider 可暴击)
var _seal_miss_seen := {} # 封印未中行去重(每安全格实例一次)
var _perfect_hit_true := 0 # 整图完美封印计数(install 重置)
var _perfect_hit_false := 0
var _perfect_forgiven := 0 # 完美保险豁免累计(单向累加自有账;契约 2 禁改写 _perfect_hit_false)
var _hero_atk := 0 # 主角攻击基值快照(encounter.hero_atk;E4 收口——表值运行期不可变)
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
db = db_ref
rng = rng_ref
_board_generator = BoardGenerator.new()
action_economy = ActionEconomy.new(db)
minesweeper = MinesweeperRules.new(db, action_economy)
_enemy_system = UnitContainer.new(db)
_status_effects = StatusContainer.new(db)
_facts = BattleFacts.new() # 战斗事实件(受理事实/轮缓冲/封印账/溯源/条件计数——成员迁移见设计 §4)
_reward_economy = RewardEconomy.new(db, rng)
_board_generator.fatal_handler = _fatal; _reward_economy.fatal_handler = _fatal # 协作件违约→本类 _fatal 封印(单点接线)
_reward_economy.fatal_handler = _fatal # 协作件违约→本类 _fatal 封印(单点接线)
_damage = DamageController.new(db, rng, _enemy_system, _status_effects)
_damage.emit_row_cb = _emit_row # 行协议归属本类(9 类行协议 + 来源断言不外泄)
_fx_host = BattleFxHost.new({"db": db, "rng": rng, "minesweeper": minesweeper, "action_economy": action_economy,
@@ -253,11 +233,8 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
minesweeper.round_end_board.connect(_on_round_end)
action_economy.round_end_stopped.connect(_on_round_end)
# 结算五段接线(授予/战斗两段持三引用=宿主+两协作件;信号连接纪律不涉段〔段零信号连接〕)
_grant_seg = GrantSegment.new(self, _fx_host, _reward_grant)
_aura_seg = AuraSegment.new(self)
_combat_seg = CombatSegment.new(self, _fx_host, _reward_grant)
_decay_seg = DecaySegment.new(self)
_verdict_seg = VerdictSegment.new(self)
# 结算五段接线随轮循环驱动件迁驻(round_cycle.gd——段件持有/段序/重入闸/摇号单点;构造序禁环不变)
_rounds = RoundCycle.new(self, _fx_host, _reward_grant)
# ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → install+roll → 行动经济)----
@@ -268,9 +245,9 @@ func begin_battle(enc: Dictionary) -> bool:
return false
if not _begin_battle_accept(enc):
return false
if not _install_new_board():
if not _install_and_reset():
return false
_reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险)
_facts.reset_round() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险)
_begin_battle_fx_state(enc)
_begin_battle_attune_params(enc)
_begin_battle_preview_economy(enc)
@@ -282,14 +259,14 @@ func begin_battle(enc: Dictionary) -> bool:
if not action_economy.notify_round_start():
_fatal("行动经济 首轮 notify_round_start 被拒")
return false
_roll_round_actions() # 开轮摇号(首轮同样摇;先于 round_started,挖掘期动画位可读)
_rounds.roll_round_actions() # 开轮摇号(首轮同样摇;先于 round_started,挖掘期动画位可读)
round_started.emit(round_index)
return true
# 受理段:encounter 装载+缓冲清零+护甲合流+重载闸+开域门+遭遇骰与敌人系统 spawn 受理门。
# 受理段:_facts.encounter 装载+缓冲清零+护甲合流+重载闸+开域门+遭遇骰与敌人系统 spawn 受理门。
func _begin_battle_accept(enc: Dictionary) -> bool:
encounter.assign(enc)
_facts.encounter.assign(enc)
_reward_grant.reset_battle() # 战斗域零 GP 持久(每场战斗缓冲从零起算;案C 闸复位)
_damage.set_hero_ac_override(int(enc.get("hero_armor_class", -1))) # 主角护甲合流入参(伤害控制器 defender_ac 消费)
db.set_reload_gate(true)
@@ -300,9 +277,9 @@ func _begin_battle_accept(enc: Dictionary) -> bool:
if dice.is_empty():
_fatal("敌人表 行集解析为空(%d,%d,%s)" % [int(enc.level_index), int(enc.node_depth), String(enc.get("node_type", "battle"))])
return false
# E4 收口:加成经 encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径)
_hero_atk = int(enc.get("hero_atk", int(db.combat.hero.atk)))
if not _enemy_system.spawn({"level_index": int(enc.level_index), "node_depth": int(enc.node_depth), "hero_hp": int(enc.hero_hp), "unit_count_overrides": dice[0], "hero_atk_override": _hero_atk, "hero_max_hp_override": int(enc.get("hero_max_hp", -1)), "enemy_atk_overrides": dice[1], "hero_attack_type": String(enc.get("attack_type", "")), "hero_armor_type": String(enc.get("armor_type", "")), "row_kind": String(enc.get("node_type", "battle"))}): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss)
# E4 收口:加成经 _facts.encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径;
# _hero_atk 派生读=受理后不变量,无快照成员)
if not _enemy_system.spawn({"level_index": int(enc.level_index), "node_depth": int(enc.node_depth), "hero_hp": int(enc.hero_hp), "unit_count_overrides": dice[0], "hero_atk_override": _hero_atk(), "hero_max_hp_override": int(enc.get("hero_max_hp", -1)), "enemy_atk_overrides": dice[1], "hero_attack_type": String(enc.get("attack_type", "")), "hero_armor_type": String(enc.get("armor_type", "")), "row_kind": String(enc.get("node_type", "battle"))}): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss)
_fatal("敌人系统 spawn 被拒")
return false
return true
@@ -310,7 +287,7 @@ func _begin_battle_accept(enc: Dictionary) -> bool:
# 遭遇骰(难度曲线与Boss GDD Core Rule 8):行有 unit_count_max 时按 [unit_count, max] 均匀骰;
# 行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值
# encounter 封闭流恰一 scope(数量骰先、atk 骰后——区间宽度为零的行不掷=「定值不骰」既有口径);
# _facts.encounter 封闭流恰一 scope(数量骰先、atk 骰后——区间宽度为零的行不掷=「定值不骰」既有口径);
# 敌人系统零随机纪律保持(收 override 即确定值)。
# 返回 [count_overrides, atk_overrides];行集为空 = 返回 [](fail 判据交受理段 _fatal)。
func _begin_battle_dice(enc: Dictionary) -> Array:
@@ -353,12 +330,12 @@ func _begin_battle_dice_roll(rows: Array, estream: RandomNumberGenerator, roll_a
# 引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零)
# 扩签运行态就位(归 fx_host)+账本挂接+桶挂引用+揭示/AC 两回调+rider 档位恒零+订阅重建。
func _begin_battle_fx_state(enc: Dictionary) -> void:
# 同调装配运行态注入(enc.attunement 携带;off=空集零扰动。淬毒 N 注入
# 同调装配运行态注入(enc._facts.attunement 携带;off=空集零扰动。淬毒 N 注入
# 伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install)
attunement = (enc.get("attunement", []) as Array).duplicate()
_attune_safe_seen = 0
_facts.attunement = (enc.get("attunement", []) as Array).duplicate()
_facts.attune_safe_seen = 0
_fx_host.reset_battle_state(int(enc.get("hero_armor_class", -1)))
_mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款)
_rounds.mines_hits = {"total": 0, "last": 0, "round": 0} # 踩雷计数开战清零(事实面生命周期同款)
var adv_bucket: Variant = enc.get("adv_bucket", null)
_fx_host.mount_buckets(adv_bucket if adv_bucket is Dictionary else {}, enc.get("level_counters")) # 冒险桶宿主字典(直构兜底自有空字典);关卡桶 LevelRunner 每关 new 下传、非 Dictionary 即不挂
_reward_grant.bind_battle(_battle_hooks()) # 授予件战斗域回调束(充能消耗/真视计数/踩雷计数)
@@ -369,13 +346,13 @@ func _begin_battle_fx_state(enc: Dictionary) -> void:
# 不设档位机制」):rider_tier 两注入恒零,散件单体效果=唯一形态
_damage.inject["rider_tier_bonus"] = 0
_damage.inject["rider_tier_crit"] = false
_feat_first_dig_used = false
_facts.feat_first_dig_used = false
# 树 rule 键消费(Fork 激活侧效果=效果引擎合成
# 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。其余 rule 键
# =stat 面投影不入战斗(T8 表内容 provisional 口径披露))
var tree_rules: Array[Variant] = enc.get("tree_rules", []) as Array
# 效果引擎订阅重建(装载序=feats 表序→treasure 表序→树规则,写死保种子复放——细则见 battle_fx_host.rebuild_subscriptions)
_fx_host.rebuild_subscriptions(feat_ids, attunement, tree_rules)
_fx_host.rebuild_subscriptions(feat_ids, _facts.attunement, tree_rules)
# 秘宝常时参数注入(装配集 kind 分发:远程镐挖距放宽/信标网络封印可达豁免;
@@ -411,144 +388,27 @@ func _begin_battle_preview_economy(enc: Dictionary) -> void:
action_economy.grant_reserve_ap(voucher_rap)
func _install_new_board(during_round := false) -> bool:
var stream: RandomNumberGenerator = rng.open_stream("board_gen")
var board: BoardData = _board_generator.generate(int(encounter.level_index), int(encounter.node_depth), stream, db)
if board == null:
# ---- 装板受理薄壳(BoardInstaller 拆件后的宿主承接面——begin_battle/中途刷新/轮内刷新三调用点唯一入口)----
# 件内装板(生成→全局变异决议→装板受理→资源摇号→变异布置)成功后执行宿主态复位:五处计数清零+
# 板序推进+board_changed 发射+秘宝叠加层退役——时序逐字;复位职责留宿主(成员单点),不随件。
func _install_and_reset(during_round := false) -> bool:
var installer := BoardInstaller.new({"db": db, "rng": rng, "minesweeper": minesweeper,
"reward_economy": _reward_economy, "encounter": _facts.encounter, "fatal_cb": _fatal})
if not installer.install(during_round):
return false
# 全局作用域变异决议(装板序内前置于 roll_board:图级偏斜旋钮须在资源摇号前定型——详见 _resolve_global_variant)
_resolve_global_variant(board)
var res: Dictionary[Variant, Variant] = minesweeper.install_board(board, during_round)
if not res.accepted:
_fatal("排雷玩法 install 拒绝(不变式违例): %s" % str(res))
return false
# 地图战斗资源 义务⑥:roll_board 恰一次(install 后、挖掘前);资格格 = 全部非封锁格(安全格 + 危险格,雷安同池)
var r0 := {}
for c in board.compute_r0():
r0[c] = true
var eligible: Array[Dictionary] = []
for c in board.safe_cells():
if not r0.has(c):
eligible.append({"cell": c})
for c in board.mines.keys():
if not r0.has(c):
eligible.append({"cell": c})
if not _reward_economy.roll_board(eligible):
_fatal("地图战斗资源 roll_board 被拒")
return false
# 地形变异 布置段(棋盘设置顺序第 4 步:生成棋盘 → 打开状态初始化 →
# 战斗资源格设置 → 变异格设置)——布置在初始展开与资源格之后,变异天然不落已开区
_place_variants(board)
_seal_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8)
_seal_miss_seen.clear()
_perfect_hit_true = 0 # 整图完美计数随 install 重置
_perfect_hit_false = 0
_perfect_forgiven = 0 # 豁免累计随 install 重置(domain=battle 每战清零)
_board_from_midround_refresh = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false
_board_seq += 1
_facts.seal_consumed.clear() # 换图新雷 = 新实例,资格自然重置(行动经济 Rule 8)
_facts.seal_miss_seen.clear()
_facts.perfect_hit_true = 0 # 整图完美计数随 install 重置
_facts.perfect_hit_false = 0
_facts.perfect_forgiven = 0 # 豁免累计随 install 重置(domain=battle 每战清零)
_facts.board_from_midround = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false
_facts.board_seq += 1
board_changed.emit()
if treasure_inventory != null:
treasure_inventory.retire_overlays() # 叠加层随图消亡(换图/刷新图装板点;已花道具不返还)
if _reward_grant.treasure_inventory != null:
_reward_grant.treasure_inventory.retire_overlays() # 叠加层随图消亡(换图/刷新图装板点;已花道具不返还)
return true
# ---- 全局作用域变异 决议(装板序内 roll_board 之前)----
# boss/elite 图命中全局型:写整图 board_variants(恰一行)+ 注入资源面偏斜旋钮(图级投影在
# RewardEconomy.roll_board 消费);不消费 variant_place 流(全局取用确定性——零取号合法态)。
# 全局池空(含 L1 Boss 免疫/unlock 门控未达):字段留空、旋钮归零——_place_variants 原路走
# region 布置 = 兜底回落合法态(防配置缺行静默裸奔——provisional)。
func _resolve_global_variant(board: BoardData) -> void:
board.board_variants.clear()
_reward_economy.board_skew = 0
var row: Dictionary[Variant, Variant] = db.combat.variation.global_variant_row(int(encounter.level_index), String(board.board_type),
String(encounter.get("node_type", "")))
if row.is_empty():
return
board.board_variants = [String(row.variant_id)]
_reward_economy.board_skew = int(row.get("effect_value", 0))
# ---- 地形变异 布置段(棋盘设置顺序第 4 步)----
# 候选池门控(unlock_level ≤ 关卡序号 + board_types 与本地形交集,∅ 通配)经 config_db.variant_pool;
# 池空 = 合法态,静默零布置(框架 P0 口径不变)。零特判:本段只写格子变异数据快照,不改扫雷规则本体。
# 候选格域 = 安全格 − 初始展开区(伤害型 placement=closed_only 的结构保障——布置发生在初始展开之后,
# 已开格天然排除);危险格不吃变异(挖雷一层惩罚 + 变异伤害双层叠罚污染逐行归因,首版排除——provisional)。
# 每图至多一种类型(候选池内均匀选一);数量 = round(候选池格数×percent) +
# (关卡序号−1)×per_level_bonus——基数为候选池而非总格数(percent 语义 = 占可挖格比例,
# 关卡递增由 per_level_bonus 绝对格数承载);连通片生长(8 邻),单片 ≤ pack_max。
# 替换制:整图 board_variants 非空 = 本图已带全局型,跳过 region 布置(详见 _resolve_global_variant)。
func _place_variants(board: BoardData) -> void:
if not board.board_variants.is_empty():
return
var pool: Array[Variant] = db.combat.variation.variant_pool(int(encounter.level_index), String(board.board_type))
if pool.is_empty():
return
var r0 := {}
for c in board.compute_r0():
r0[c] = true
var candidates: Array[Vector2i] = []
for c in board.safe_cells():
if not r0.has(c):
candidates.append(c)
if candidates.is_empty():
return
var stream: RandomNumberGenerator = rng.open_stream("variant_place") # 一板恰一 scope(换图/刷新图各取号一次)
var vrow: Dictionary[Variant, Variant] = pool[stream.randi_range(0, pool.size() - 1)]
var total: int = int(round(float(candidates.size()) * float(db.combat.variation.percent_of_cells) / 100.0)) \
+ (int(encounter.level_index) - 1) * int(db.combat.variation.per_level_bonus)
total = mini(total, candidates.size())
_place_variants_apply(board, candidates, stream, vrow, total)
# 变异片生长施加面(布置域=候选格,数量=total,vrow 单型贯穿;取号序=stream 顺序消费)
func _place_variants_apply(board: BoardData, candidates: Array[Vector2i], stream: RandomNumberGenerator, vrow: Dictionary, total: int) -> void:
var placed := {}
while placed.size() < total:
var pack_size: int = mini(total - placed.size(), stream.randi_range(2, int(db.combat.variation.pack_max)))
var free: Array[Vector2i] = []
for c in candidates:
# 新片种子/生长格禁与其他已布片 8 邻贴接——否则两片粘连成一个超 pack_max 的连通分量
# (R3 随机流下实测暴露:独立生长的相邻片合流击穿单片上限断言)
if placed.has(c) or _adjacent_to_packs(board, placed, c):
continue
free.append(c)
if free.is_empty():
break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发)
var seed_c: Vector2i = free[stream.randi_range(0, free.size() - 1)]
placed[seed_c] = true
var pack: Array[Variant] = [seed_c]
var pack_set := {seed_c: true}
while pack.size() < pack_size:
var fringe := _variants_fringe(board, pack, candidates, placed, pack_set)
if fringe.is_empty():
break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发)
var nxt: Vector2i = fringe[stream.randi_range(0, fringe.size() - 1)]
placed[nxt] = true
pack_set[nxt] = true
pack.append(nxt)
for c in placed.keys():
board.variants[c] = [String(vrow.variant_id)]
# 片生长待选格收集(本片内各生长格的 8 邻候选并集——未布置、未在途、不与他片贴接)
func _variants_fringe(board: BoardData, pack: Array, candidates: Array[Vector2i], placed: Dictionary, pack_set: Dictionary) -> Array[Vector2i]:
var fringe: Array[Vector2i] = []
for c in pack:
for n in board.neighbors8(c):
if candidates.has(n) and not placed.has(n) and not fringe.has(n) \
and not _adjacent_to_packs(board, placed, n, pack_set):
fringe.append(n)
return fringe
# 候选格是否与其他已布片(own_pack 之外的 placed 格)8 邻贴接——片间隔离保证连通分量 = 单片 ≤ pack_max
func _adjacent_to_packs(board: BoardData, placed: Dictionary, c: Vector2i, own_pack: Dictionary = {}) -> bool:
for m in board.neighbors8(c):
if placed.has(m) and not own_pack.has(m):
return true
return false
# ---- 事件缓冲(轮 facts 收集;结算触发 = Waiting 相位编排点,不依赖到达序)----
func _on_cell_revealed(evt: Dictionary) -> void:
@@ -566,9 +426,9 @@ func _on_cell_revealed(evt: Dictionary) -> void:
if String(evt.get("kind", "")) == "safe":
var cs_row: Dictionary[Variant, Variant] = _attune_charge_safe_row()
if not cs_row.is_empty():
_attune_safe_seen += 1
_facts.attune_safe_seen += 1
var cn_b: int = int(cs_row.get("charge_n", 999))
if cn_b > 0 and _attune_safe_seen % cn_b == 0:
if cn_b > 0 and _facts.attune_safe_seen % cn_b == 0:
action_economy.grant_ap(int(cs_row.get("magnitude", 1)))
if bool(evt.get("resource", false)):
# 身份快照随缓冲捕获(批次隔离——轮内刷新后 mapping 已换批,授予必须用缓冲期身份,
@@ -577,47 +437,20 @@ func _on_cell_revealed(evt: Dictionary) -> void:
if snap != null:
evt = evt.duplicate()
evt["reward_snapshot"] = snap
_facts_reveals.append(evt)
_facts.reveals.append(evt)
func _on_seal_judged(evt: Dictionary) -> void:
_facts_seals.append(evt)
_facts.seals.append(evt)
func _on_round_end(_payload: Dictionary) -> void:
_facts_path = String(_payload.get("path_tag", ""))
_maybe_settle()
_facts.path = String(_payload.get("path_tag", ""))
_rounds.settle_if_due()
func _maybe_settle() -> void:
if _settling or battle_result != "":
return
if action_economy.phase != action_economy.PHASE_WAITING:
return
_settle()
# ---- 每轮结算序列(五段——段序骨架冻结于 SettlementTable.segment_order)----
func _settle() -> void:
_settling = true
var ri := round_index
# 段序编排(五段——段体归 game/core/settlement/,本类只做段序+行发射/开单原语单点)
_grant_seg.run(ri) # ① 授予段(案C 闸随段装载)
_aura_seg.run(ri) # ② 增益/减益段(续存实例组合行)
_combat_seg.run(ri) # ③ 战斗段(主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕)
_decay_seg.run(ri) # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减)
var verdict: String = _verdict_seg.run(ri) # ⑤ 判定段(防御性优先级 hero 先判)
if verdict != "continue":
return # 终局:_settling 不回置——_battle_over 已接管
_settling = false
# ---- 授予段 helpers(极性路由)----
# 开格产出乘区(贪婪矿灯/矿脉直觉/赌徒的血签事件收集器注入):
# positive=power ×(1+静态 mult%);negative=power ×(1+cost%)。
# 全零收集器=原快照原样返回(零扰动)。
func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int) -> Dictionary:
if positive and pos_m <= 0:
return snap
@@ -674,57 +507,7 @@ func set_treasure_unlocked(ids: Dictionary) -> void:
# 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。
# 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。
# 摇定后、兑现前死亡 → 本拍技能作废。
func _roll_round_actions() -> void:
var pity_m: int = int(db.combat.skill.pity_threshold)
# facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts,
# 容器内直读主角行)
var facts := {
"round_index": round_index,
"last_round_end": "none" if action_economy.last_round_end().is_empty() else String(action_economy.last_round_end().get("path_tag", "none")),
"mines_hit_last": int(_mines_hits["last"]),
"mines_hit_total": int(_mines_hits["total"]),
}
for u in _enemy_system.query_alive("enemy"):
# Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行
# 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性
# 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机
# 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏)
for sid in _enemy_system.check_trigger_unlocks(u, facts):
var srow_u: Dictionary[Variant, Variant] = db.combat.skill.row_by_id(sid)
_emit_row({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"},
round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))],
0, {"skill_id": sid})
# enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射,
# 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值)
var pct_v := 0
for pe in (u.get("pool", []) as Array):
var pe_d: Dictionary[Variant, Variant] = pe
if String(pe_d.get("skill_id", "")) == sid and (pe_d.get("trigger", {}) as Dictionary).has("value"):
pct_v = int((pe_d.get("trigger", {}) as Dictionary).get("value", 0))
break
# target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记,
# 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value)
var eht_hits := _fx_host.fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v,
"is_boss": String(encounter.get("node_type", "")) == "boss"})
for h in eht_hits:
if String(h.get("kind", "")) == "target_attack_bonus":
u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0))
var entries: Array[Variant] = _enemy_system.rollable_skills(u, facts)
if _enemy_system.basic_streak(u) >= pity_m:
var specials: Array[Variant] = _enemy_system.special_skills(u, facts)
if not specials.is_empty():
entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合
var stream: RandomNumberGenerator = rng.open_stream("behavior_roll")
_enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream))
_enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记)
_mines_hits["last"] = _mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新)
_mines_hits["round"] = 0
# ---- 战斗段/算术读数(段体与掷点/折扣/暴击/克制/池乘区/下限/交付管线归 damage_controller.gd
# 与 settlement/combat_segment.gd;本类只保留薄委托——_defender_ac 供结算/呈现快照消费)----
# 防守方护甲等级读数——实现归 伤害控制器 defender_ac(本委托供结算快照/呈现快照)
func _defender_ac(unit: Dictionary) -> int:
return _damage.defender_ac(unit)
@@ -761,12 +544,12 @@ func _emit_settlement_snapshot(verdict: String) -> void:
buffs.append({"id": eff.effect_id, "side": "enemy", "layers": eff.layers, "left": eff.remaining_settlements})
var mines_hit := 0
var safe_n := 0
for evt in _facts_reveals:
for evt in _facts.reveals:
if evt.kind == "mine":
mines_hit += 1
elif evt.kind == "safe":
safe_n += 1
var board_stats := {"seq": _board_seq}
var board_stats := {"seq": _facts.board_seq}
if minesweeper.current_board() != null:
board_stats["w"] = minesweeper.current_board().width
board_stats["h"] = minesweeper.current_board().height
@@ -774,18 +557,18 @@ func _emit_settlement_snapshot(verdict: String) -> void:
board_stats["seals"] = minesweeper.seal_count()
settlement_snapshot.emit({
"kind": "settlement",
"level": int(encounter.get("level_index", 0)),
"depth": int(encounter.get("node_depth", 0)),
"level": int(_facts.encounter.get("level_index", 0)),
"depth": int(_facts.encounter.get("node_depth", 0)),
"round": round_index,
"verdict": verdict,
"unit_count": enemies.size(),
"spawn_count": _enemy_system.all_enemies().size(), # 本战斗实例化总数(含死亡残位)——unit_count=0 时区分「首轮全灭」vs「空场」
"hero": {"hp": int(_enemy_system.hero_unit().get("hp", 0)), "max_hp": int(_enemy_system.hero_unit().get("max_hp", 0)), "atk": int(_enemy_system.hero_unit().get("atk", 0)), "armor_class": _defender_ac(_enemy_system.hero_unit())},
"economy": {"phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "refresh_used": _refresh_used_this_round},
"economy": {"phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "refresh_used": _facts.refresh_used},
"board": board_stats,
"enemies": enemies,
"buffs": buffs,
"round_facts": {"path": _facts_path, "safe": safe_n, "mines": mines_hit, "seal_events": _facts_seals.size(), },
"round_facts": {"path": _facts.path, "safe": safe_n, "mines": mines_hit, "seal_events": _facts.seals.size(), },
})
@@ -793,12 +576,12 @@ func _emit_settlement_snapshot(verdict: String) -> void:
func _exit_distribution() -> void:
var mode := "same_board"
if _facts_path == "cleared":
if _facts.path == "cleared":
mode = "new_board" # cleared 轮恒走 new_board(契约义务);非清图轮恒同图(等待期刷新通道已移除)
if mode == "new_board":
_reward_economy.retire_mapping()
minesweeper.retire_board()
if not _install_new_board():
if not _install_and_reset():
_fatal("换图编排失败")
return
# 工兵晨令续队直调拆除(内容数据驱动扩展批 T4——round_start 订阅行承载,见 begin_battle 注)
@@ -807,16 +590,16 @@ func _exit_distribution() -> void:
round_index += 1
_fx_host.emit_round_start()
action_economy.notify_round_start()
_reset_round_facts()
_refresh_used_this_round = 0
_roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道)
_facts.reset_round()
_facts.refresh_used = 0
_rounds.roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道)
round_started.emit(round_index)
# 呈现层查询面(HUD 刷新按钮可用性)——挖掘期唯一通道
func can_refresh() -> bool:
return battle_result == "" \
and _refresh_used_this_round < int(db.run.action_economy.refresh_cap_per_round) \
and _facts.refresh_used < int(db.run.action_economy.refresh_cap_per_round) \
and action_economy.phase == action_economy.PHASE_ACTIVE \
and action_economy.rap >= int(db.run.action_economy.rap_refresh_cost)
@@ -829,10 +612,10 @@ func player_request_refresh() -> bool:
return false
if not action_economy.request_refresh():
return false
_refresh_used_this_round += 1
_facts.refresh_used += 1
_reward_economy.retire_mapping() # 旧图批次退役(地图战斗资源 Ready→retired;E6 新批重置前提)
minesweeper.retire_board(true) # 旧板退役(轮内通道)
return _install_new_board(true) # 当场换图——失败即编排违例 fail-fast
return _install_and_reset(true) # 当场换图——失败即编排违例 fail-fast
func _battle_over(result: String) -> void:
@@ -855,11 +638,11 @@ func _battle_over(result: String) -> void:
_reward_economy.retire_mapping()
_enemy_system.despawn_all()
_status_effects.notify_battle_end()
if treasure_inventory != null:
treasure_inventory.retire_overlays() # 叠加层随战斗结束清空(库存不动——关卡域跨战斗)
if _reward_grant.treasure_inventory != null:
_reward_grant.treasure_inventory.retire_overlays() # 叠加层随战斗结束清空(库存不动——关卡域跨战斗)
action_economy.notify_combat_end() # → Idle,RAP 清零
db.set_reload_gate(false)
_reset_round_facts()
_facts.reset_round()
battle_finished.emit({"result": result, "hero_hp": hero_hp_left, "gp_dropped": gp_dropped})
@@ -867,7 +650,7 @@ func _battle_over(result: String) -> void:
# gp_dropped = gp_drop 封闭流掷定(tier 行的 roll_min..roll_max 均匀整数——每战斗末恰一 scope)
# + level_gp_bonus(level_index) + escrow。掷定行缺位 = 数据面缺行(fail 报错按 0 兑付——不哑掉)。
func _roll_gp_drop() -> int:
var tier := "boss" if String(encounter.get("node_type", "")) == "boss" else "normal"
var tier := "boss" if String(_facts.encounter.get("node_type", "")) == "boss" else "normal"
var drops: Dictionary[Variant, Variant] = db.run.shop.gp_drop_row(tier)
var roll := 0
if drops.is_empty():
@@ -875,11 +658,11 @@ func _roll_gp_drop() -> int:
else:
var stream: RandomNumberGenerator = rng.open_stream("gp_drop")
roll = stream.randi_range(int(drops.get("roll_min", 0)), int(drops.get("roll_max", 0)))
var drop := roll + db.run.shop.level_gp_bonus_at(int(encounter.get("level_index", 0)))
var drop := roll + db.run.shop.level_gp_bonus_at(int(_facts.encounter.get("level_index", 0)))
# 财富之道·聚(战斗掉落 GP 乘算·入账前:只乘战斗掉落两成分〔掷定+关卡加成〕,
# 店内 escrow 不乘〔范围=仅战斗掉落渠道〕;本函数返回值=结算框「GP 增量」行
# 显示源,落在入账前=显示即实得;乘数 provisional 150%)
if (encounter.get("tree_rules", []) as Array).has("economy_hoard"):
if (_facts.encounter.get("tree_rules", []) as Array).has("economy_hoard"):
drop = int(round(float(drop) * 1.5))
return drop + _reward_grant.escrow
@@ -930,7 +713,7 @@ func presentation_snapshot() -> Dictionary:
"phase": String(action_economy.phase),
"ap": int(action_economy.ap), "rap": int(action_economy.rap),
"can_refresh": can_refresh(),
"refresh_used": _refresh_used_this_round, # 受理计数入呈现成文面(HUD 余量单源读此处)
"refresh_used": _facts.refresh_used, # 受理计数入呈现成文面(HUD 余量单源读此处)
"hero": hero,
"enemies": enemies,
"hero_effects": _status_effects.query_active("hero").duplicate(true), # 值拷贝出户(快照消费方直改不渗入状态容器)
@@ -957,12 +740,12 @@ func diagnostic_snapshot() -> Dictionary:
return {
"battle_result": battle_result,
"round": round_index,
"settling": _settling,
"refresh_used_round": _refresh_used_this_round,
"board_seq": _board_seq,
"facts_path": _facts_path,
"facts_reveals": _facts_reveals.size(),
"facts_seals": _facts_seals.size(),
"settling": _rounds.is_settling(),
"refresh_used_round": _facts.refresh_used,
"board_seq": _facts.board_seq,
"facts_path": _facts.path,
"facts_reveals": _facts.reveals.size(),
"facts_seals": _facts.seals.size(),
"action_economy": {"phase": String(action_economy.phase), "ap": action_economy.ap, "rap": action_economy.rap},
"minesweeper_board_installed": minesweeper.current_board() != null,
"minesweeper_revealed": minesweeper.revealed_cells().size() if minesweeper.current_board() != null else 0,
@@ -973,12 +756,6 @@ func diagnostic_snapshot() -> Dictionary:
}
func _reset_round_facts() -> void:
_facts_reveals.clear()
_facts_seals.clear()
_facts_path = ""
# ---- 行协议(Core Rule:公共字段 + 行类型特有字段)----
func _emit_row(head: Dictionary, ri: int, display: String, value: int, extra: Dictionary) -> void:
@@ -1029,6 +806,6 @@ func _fatal(msg: String) -> void:
## 完美封印谓词(两账相减:有效误标=误标总数−豁免数)
## 契约 2:_perfect_hit_false 保持纯净,豁免只累加 _perfect_forgiven,禁回退式改写与拦截式吞事件。
## 契约 2:_facts.perfect_hit_false 保持纯净,豁免只累加 _facts.perfect_forgiven,禁回退式改写与拦截式吞事件。
func perfect_sweep_earned() -> bool:
return _perfect_hit_false - _perfect_forgiven <= 0 and _perfect_hit_true >= 1
return _facts.perfect_hit_false - _facts.perfect_forgiven <= 0 and _facts.perfect_hit_true >= 1
+167
View File
@@ -0,0 +1,167 @@
## 棋盘安装与变异布置域件(B1 域·BoardInstaller):生成受理、全局作用域变异决议、装板受理、
## 资源摇号资格域组装(roll_board 恰一次——install 后、挖掘前)、region 变异决议与连通片布置。
## 棋盘设置顺序 = 生成棋盘 → 打开状态初始化 → 战斗资源格设置 → 变异格设置(布置在初始展开与
## 资源格之后,变异天然不落已开区)。宿主侧装板后状态复位(封印/完美计数清零、板序推进、
## board_changed 发射、秘宝叠加层退役)留宿主薄壳——本件零复位职责。零 UI 零 await;
## 随机只经注入 RngServiceCore 的 board_gen/variant_place 两封闭流(流名与开流时点不变,
## 生成器只消费传入流——同种子同输入逐位复现)。
class_name BoardInstaller
extends RefCounted
var db: ConfigDb
var rng: RngServiceCore
var minesweeper: MinesweeperRules
var reward_economy: RewardEconomy
var encounter: Dictionary[String, Variant] # 宿主 encounter 同对象引用(assign 原地变异——跨战活引用)
var fatal_cb: Callable # 协作件违约→宿主 _fatal 封印(单点接线——BoardGenerator 同款)
var generator: BoardGenerator # 件内自建自接线(原宿主 _board_generator 成员随件迁移)
func _init(bundle: Dictionary) -> void:
for k in ["db", "rng", "minesweeper", "reward_economy", "encounter", "fatal_cb"]:
assert(bundle.has(k), "[装板域件] 引用束缺键: %s" % k)
db = bundle.db
rng = bundle.rng
minesweeper = bundle.minesweeper
reward_economy = bundle.reward_economy
encounter = bundle.encounter
fatal_cb = bundle.fatal_cb
generator = BoardGenerator.new()
generator.fatal_handler = fatal_cb # 协作件违约→宿主 _fatal 封印(单点接线)
func _fatal(msg: String) -> void:
if fatal_cb.is_valid():
fatal_cb.call(msg) # 组装层注入 orchestrator._fatal(封印传导,不绕第二中止通道)
else:
push_error(msg)
assert(false, msg)
# 装板单入口(生成 → 全局变异决议 → 装板受理 → 资源摇号 → 变异布置);
# 宿主态复位归宿主薄壳(_install_and_reset)——本件返回即装板完成、复位未行。
func install(during_round := false) -> bool:
var stream: RandomNumberGenerator = rng.open_stream("board_gen")
var board: BoardData = generator.generate(int(encounter.level_index), int(encounter.node_depth), stream, db)
if board == null:
return false
# 全局作用域变异决议(装板序内前置于 roll_board:图级偏斜旋钮须在资源摇号前定型——详见 _resolve_global_variant)
_resolve_global_variant(board)
var res: Dictionary[Variant, Variant] = minesweeper.install_board(board, during_round)
if not res.accepted:
_fatal("排雷玩法 install 拒绝(不变式违例): %s" % str(res))
return false
# 地图战斗资源 义务⑥:roll_board 恰一次(install 后、挖掘前);资格格 = 全部非封锁格(安全格 + 危险格,雷安同池)
var r0 := {}
for c in board.compute_r0():
r0[c] = true
var eligible: Array[Dictionary] = []
for c in board.safe_cells():
if not r0.has(c):
eligible.append({"cell": c})
for c in board.mines.keys():
if not r0.has(c):
eligible.append({"cell": c})
if not reward_economy.roll_board(eligible):
_fatal("地图战斗资源 roll_board 被拒")
return false
# 地形变异 布置段(棋盘设置顺序第 4 步)——布置在初始展开与资源格之后,变异天然不落已开区
_place_variants(board)
return true
# ---- 全局作用域变异 决议(装板序内 roll_board 之前)----
# boss/elite 图命中全局型:写整图 board_variants(恰一行)+ 注入资源面偏斜旋钮(图级投影在
# RewardEconomy.roll_board 消费);不消费 variant_place 流(全局取用确定性——零取号合法态)。
# 全局池空(含 L1 Boss 免疫/unlock 门控未达):字段留空、旋钮归零——_place_variants 原路走
# region 布置 = 兜底回落合法态(防配置缺行静默裸奔——provisional)。
func _resolve_global_variant(board: BoardData) -> void:
board.board_variants.clear()
reward_economy.board_skew = 0
var row: Dictionary[Variant, Variant] = db.combat.variation.global_variant_row(int(encounter.level_index), String(board.board_type),
String(encounter.get("node_type", "")))
if row.is_empty():
return
board.board_variants = [String(row.variant_id)]
reward_economy.board_skew = int(row.get("effect_value", 0))
# ---- 地形变异 布置段(棋盘设置顺序第 4 步)----
# 候选池门控(unlock_level ≤ 关卡序号 + board_types 与本地形交集,∅ 通配)经 config_db.variant_pool;
# 池空 = 合法态,静默零布置(框架 P0 口径不变)。零特判:本段只写格子变异数据快照,不改扫雷规则本体。
# 候选格域 = 安全格 − 初始展开区(伤害型 placement=closed_only 的结构保障——布置发生在初始展开之后,
# 已开格天然排除);危险格不吃变异(挖雷一层惩罚 + 变异伤害双层叠罚污染逐行归因,首版排除——provisional)。
# 每图至多一种类型(候选池内均匀选一);数量 = round(候选池格数×percent) +
# (关卡序号−1)×per_level_bonus——基数为候选池而非总格数(percent 语义 = 占可挖格比例,
# 关卡递增由 per_level_bonus 绝对格数承载);连通片生长(8 邻),单片 ≤ pack_max。
# 替换制:整图 board_variants 非空 = 本图已带全局型,跳过 region 布置(详见 _resolve_global_variant)。
func _place_variants(board: BoardData) -> void:
if not board.board_variants.is_empty():
return
var pool: Array[Variant] = db.combat.variation.variant_pool(int(encounter.level_index), String(board.board_type))
if pool.is_empty():
return
var r0 := {}
for c in board.compute_r0():
r0[c] = true
var candidates: Array[Vector2i] = []
for c in board.safe_cells():
if not r0.has(c):
candidates.append(c)
if candidates.is_empty():
return
var stream: RandomNumberGenerator = rng.open_stream("variant_place") # 一板恰一 scope(换图/刷新图各取号一次)
var vrow: Dictionary[Variant, Variant] = pool[stream.randi_range(0, pool.size() - 1)]
var total: int = int(round(float(candidates.size()) * float(db.combat.variation.percent_of_cells) / 100.0)) \
+ (int(encounter.level_index) - 1) * int(db.combat.variation.per_level_bonus)
total = mini(total, candidates.size())
_place_variants_apply(board, candidates, stream, vrow, total)
# 变异片生长施加面(布置域=候选格,数量=total,vrow 单型贯穿;取号序=stream 顺序消费)
func _place_variants_apply(board: BoardData, candidates: Array[Vector2i], stream: RandomNumberGenerator, vrow: Dictionary, total: int) -> void:
var placed := {}
while placed.size() < total:
var pack_size: int = mini(total - placed.size(), stream.randi_range(2, int(db.combat.variation.pack_max)))
var free: Array[Vector2i] = []
for c in candidates:
# 新片种子/生长格禁与其他已布片 8 邻贴接——否则两片粘连成一个超 pack_max 的连通分量
# (R3 随机流下实测暴露:独立生长的相邻片合流击穿单片上限断言)
if placed.has(c) or _adjacent_to_packs(board, placed, c):
continue
free.append(c)
if free.is_empty():
break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发)
var seed_c: Vector2i = free[stream.randi_range(0, free.size() - 1)]
placed[seed_c] = true
var pack: Array[Variant] = [seed_c]
var pack_set := {seed_c: true}
while pack.size() < pack_size:
var fringe := _variants_fringe(board, pack, candidates, placed, pack_set)
if fringe.is_empty():
break # 生长域耗尽——该片就地收束(数量不足总目标为合法态,候选域碎片化时触发)
var nxt: Vector2i = fringe[stream.randi_range(0, fringe.size() - 1)]
placed[nxt] = true
pack_set[nxt] = true
pack.append(nxt)
for c in placed.keys():
board.variants[c] = [String(vrow.variant_id)]
# 片生长待选格收集(本片内各生长格的 8 邻候选并集——未布置、未在途、不与他片贴接)
func _variants_fringe(board: BoardData, pack: Array, candidates: Array[Vector2i], placed: Dictionary, pack_set: Dictionary) -> Array[Vector2i]:
var fringe: Array[Vector2i] = []
for c in pack:
for n in board.neighbors8(c):
if candidates.has(n) and not placed.has(n) and not fringe.has(n) \
and not _adjacent_to_packs(board, placed, n, pack_set):
fringe.append(n)
return fringe
# 候选格是否与其他已布片(own_pack 之外的 placed 格)8 邻贴接——片间隔离保证连通分量 = 单片 ≤ pack_max
func _adjacent_to_packs(board: BoardData, placed: Dictionary, c: Vector2i, own_pack: Dictionary = {}) -> bool:
for m in board.neighbors8(c):
if placed.has(m) and not own_pack.has(m):
return true
return false
+1
View File
@@ -0,0 +1 @@
uid://di6u170vqf8ae
+15 -95
View File
@@ -41,7 +41,7 @@ var hero_hp: int:
var gp: int: # GP 冒险钱包(局内货币——冒险开始 = 0、收束清零;不入存档不入摘要)
get: return _wallet.gp
set(v): _wallet.gp = v
var _shop: ShopSession = null # 商店件(_init 建;双模式货架/定价/购买/刷新唯一宿主)
var shop: ShopSession = null # 商店件(_init 建;双模式货架/定价/购买/刷新唯一宿主)
# 协作对象私有(E1/E3 封装收口):settlement 读取经 current_battle() 成文出口
var _settlement: BattleOrchestrator = null # 当前战斗编排器(关卡结束置 null)
var treasure_inventory: TreasureItems = null # 秘宝 关卡域库存(TreasureItems;跨战斗延续,
@@ -96,10 +96,10 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore, ctx: Dictionary = {}) -> v
_hero_armor_type = String(ctx.get("hero_armor_type", ""))
_voucher_rap = maxi(0, int(ctx.get("voucher_rap", 0)))
_voucher_time_sand = maxi(0, int(ctx.get("voucher_time_sand", 0)))
_shop = ShopSession.new(db, rng)
shop = ShopSession.new(db, rng)
var pool: Dictionary[Variant, Variant] = ctx.get("pool_wiring", {}) as Dictionary
if String(ctx.get("shop_mode", "")) == "pool" and not pool.is_empty():
_shop.set_shop_pool(pool.get("deck", []) as Array, pool.get("consume", Callable()) as Callable,
shop.set_shop_pool(pool.get("deck", []) as Array, pool.get("consume", Callable()) as Callable,
pool.get("ticket", Callable()) as Callable, pool.get("cap", Callable()) as Callable)
_bind_shop()
@@ -302,8 +302,8 @@ func breakpoint_fragment() -> Dictionary[Variant, Variant]:
"node_id": current_node_id(),
"visited_ids": visited_ids(),
"aux_items": treasure_snapshot,
"shop_shelf": _shop.shop_bp_shelf(),
"shop_visit_buys": _shop.shop_bp_visit_buys(),
"shop_shelf": shop.shop_bp_shelf(),
"shop_visit_buys": shop.shop_bp_visit_buys(),
}
@@ -503,11 +503,11 @@ func _enter_node(node: Dictionary) -> bool:
"shop":
# 商店房兑付:到达即开店(覆盖层归呈现层),购买出账/
# 离开归玩家意图——零自动推进;离开确认经 continue_after_node 回选路口
if _shop.pool_mode:
_shop.open_pool() # 进店抽 3(恰一 shop_draw scope;买后全重抽同签)
node_entered.emit(node, {"gp": gp, "shelf": true})
return true
node_entered.emit(node, {"gp": gp}) # legacy 固定三件(off 对照承载——零 RNG 零池消费)
var payload := {"gp": gp} # legacy 固定三件(off 对照承载——零 RNG 零池消费)
if shop.pool_mode:
shop.open_pool() # 进店抽 3(恰一 shop_draw scope;买后全重抽同签)
payload = {"gp": gp, "shelf": true}
node_entered.emit(node, payload)
return true
push_error("[关卡节点地图] 未知节点类型: %s" % str(node))
return false
@@ -517,7 +517,7 @@ func _start_battle(node: Dictionary) -> bool:
_settlement = BattleOrchestrator.new(db, rng)
if treasure_inventory == null:
treasure_inventory = TreasureItems.new(db) # 惰性建——关卡域单实例(跨战斗延续)
_settlement.treasure_inventory = treasure_inventory # 库存注入(授予路由+use_treasure 编排消费面)
_settlement.reward_grant().treasure_inventory = self.treasure_inventory # 库存注入(经授予件出口直注——注入时序在宿主构造之后不变)
if _settlement != null:
_settlement.set_treasure_unlocked(_treasure_unlocked) # 解锁门控下传(装板摇号前)
# core↔core 接线只许方法 Callable(静态规则 S6——lambda 捕获 self 会延长
@@ -701,7 +701,7 @@ func continue_after_node() -> bool:
if t != "reward" and t != "rest" and t != "shop":
return false
if t == "shop":
_shop.leave() # 离开店面货架态清零(断点快照 shop_shelf 键随离开落空架合法态)
shop.leave() # 离开店面货架态清零(断点快照 shop_shelf 键随离开落空架合法态)
return _present_choices()
@@ -721,12 +721,12 @@ func close_wallet() -> void:
# ---- 商店房兑付(GP+商店房实施批:呈现层只读投影 + 购买出账)----
# 实装归 ShopSession 件(双模式货架/定价/购买/刷新唯一宿主);本类保留同签名委托与件注入/
# 白盒入口——外部调用面与断点取数面零变化。
# 实装归 ShopSession 件(双模式货架/定价/购买/刷新唯一宿主);外部调用面=调用方经宿主公开
# 成员 shop 直连(调用直连不留委托壳——2026-10-26 Middle Man 裁定)。
# 件注入(建件与宿主引用换代时调用——钱包/主角账三件/图行/装配集为共享引用,出账直接落同一引用)
func _bind_shop() -> void:
_shop.bind({
shop.bind({
"wallet": _wallet,
"journal": _journal,
"level_row": level_row,
@@ -746,83 +746,3 @@ func _emit_wallet_event(event: String, gp_now: int, extra: Dictionary) -> void:
# 购血(商店 max_hp 商品——当前生命 +Δ 并封顶新上限,与账本置满/关卡完成回血同式)
func _gain_max_hp_flat(flat: int) -> void:
_stats.hero_hp = mini(_stats.hero_hp + flat, _stats.effective_max_hp())
func shop_products() -> Array:
return _shop.shop_products()
func current_price(prow: Dictionary) -> int:
return _shop.current_price(prow)
func purchased_tier(product_id: String) -> int:
return _shop.purchased_tier(product_id)
func buy_product(product_id: String) -> bool:
return _shop.buy_product(product_id)
func player_request_refresh() -> bool:
return _shop.player_request_refresh()
# 散折价单点(测试取数面沿用本类调用名)
func _spend_discounted(price: int) -> int:
return _shop._spend_discounted(price)
# 注入(AdventureRunner._start_current_level 唯一调用方——pool 模式专用):令牌库共享引用 +
# 池消费 handler + 专长凭证判定 + cap 检查回调(cap 满 kind 剔出货架候选视图)。
func set_shop_pool(deck_ref: Array, consume_handler: Callable, ticket_consider: Callable = Callable(), cap_check: Callable = Callable()) -> void:
_shop.set_shop_pool(deck_ref, consume_handler, ticket_consider, cap_check)
# 货架抽签白盒入口(测试取数面沿用本类调用名)
func _shop_draw_shelf() -> void:
_shop._draw_shelf()
func pool_stock(kind: String) -> int:
return _shop.pool_stock(kind)
func pool_price(kind: String) -> int:
return _shop.pool_price(kind)
func shop_shelf() -> Array:
return _shop.shop_shelf()
func shop_refresh_free_left() -> int:
return _shop.shop_refresh_free_left()
func shop_refresh_cost() -> int:
return _shop.shop_refresh_cost()
func shop_refresh() -> bool:
return _shop.shop_refresh()
func spend_gp(amount: int) -> bool:
return _shop.spend_gp(amount)
func buy_pool_item(kind: String) -> bool:
return _shop.buy_pool_item(kind)
func is_shop_pool() -> bool:
return _shop.is_shop_pool()
func shop_bp_shelf() -> Array:
return _shop.shop_bp_shelf()
func shop_bp_visit_buys() -> int:
return _shop.shop_bp_visit_buys()
+2 -2
View File
@@ -26,7 +26,7 @@ func run(ri: int) -> void:
# 同一次 _settle 内,facts 已定。
var round_safe := 0
var round_mine := false
for evt in oc._facts_reveals:
for evt in oc._facts.reveals:
if evt.kind == "safe":
round_safe += 1
elif evt.kind == "mine":
@@ -53,7 +53,7 @@ func _hero_strike(ri: int) -> void:
var hero_target = oc._enemy_system.default_target()
if hero_alive and hero_target != null:
var ht := oc._damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击",
{"channel": "combat", "base_value": oc._hero_atk, "target_rule": DamageController.TARGET_FRONT_ALIVE,
{"channel": "combat", "base_value": oc._hero_atk(), "target_rule": DamageController.TARGET_FRONT_ALIVE,
"ri": ri, "origin_cell": Vector2i(-1, -1),
"attack_type": String(oc._enemy_system.hero_unit().get("attack_type", "normal"))})
oc._damage.submit(ht)
+21 -21
View File
@@ -34,7 +34,7 @@ func run(ri: int) -> void:
# 开格/踩雷兑付批(轮 facts_reveals 资源事件序:safe 正向乘区兑付 / mine_hit 减半负向兑付)
func _reveal_batch(ri: int) -> void:
for evt in oc._facts_reveals:
for evt in oc._facts.reveals:
if not bool(evt.get("resource", false)):
continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②)
var snap = evt.get("reward_snapshot")
@@ -61,7 +61,7 @@ func _reveal_safe(evt: Dictionary, snap: Dictionary, ri: int) -> void:
if is_new_type:
fx_host._fx_new_resource_types[ctype] = true
var dr_hits := fx_host.fx_emit("dig_reveal", {"cell": evt.cell, "round": ri,
"is_first_dig_of_battle": not oc._feat_first_dig_used,
"is_first_dig_of_battle": not oc._facts.feat_first_dig_used,
"terrain_kind": oc._harmful_variants(evt.get("variant_ids", []) as Array),
"is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0
or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1,
@@ -91,8 +91,8 @@ func _reveal_safe_apply(dr_hits: Array, evt: Dictionary, ri: int) -> void:
if String(h.get("kind", "")) == "grant_material":
_apply_material_hit(h, evt.cell, ri)
var spell_hits: Array[Variant] = oc._hits_of(dr_hits, "spawn_spell")
if not spell_hits.is_empty() and not oc._feat_first_dig_used:
oc._feat_first_dig_used = true
if not spell_hits.is_empty() and not oc._facts.feat_first_dig_used:
oc._facts.feat_first_dig_used = true
var spell_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell", "奥术矢",
{"channel": "board", "base_value": int((spell_hits[0] as Dictionary).get("value", 0)),
"target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": ri, "origin_cell": evt.cell, "attack_type": "magic"})
@@ -160,7 +160,7 @@ func _reveal_mine_apply(mh_hits: Array, snap: Dictionary, cell: Vector2i, ri: in
# 危险格恒无变异——布置段排除,双层叠罚不存在;封印格直挖照吃变异伤害——地形后果同 mine_hit 口径,
# 非奖励不作废;吞噬流资源兑付〔荆棘编织/地脉补偿〕中伤害形态行同样经案C闸暂存,非伤害形态照常当场)
func _terrain_batch(ri: int) -> void:
for evt in oc._facts_reveals:
for evt in oc._facts.reveals:
var vids: Array[Variant] = evt.get("variant_ids", [])
for vid in vids:
var vrow: Dictionary[Variant, Variant] = oc.db.combat.variation.row_by_id(String(vid))
@@ -203,7 +203,7 @@ func _terrain_damage_reduce(dt_hits: Array, dt_amount: int) -> int:
# 台账披露待追认〕;完美布防=本轮封印批全中(hit>0 且 miss==0)时封印伤 ×mult——全对推理奖励)。
# 伤害单据案C 暂存战斗段末兑付。
func _seal_batch(ri: int) -> void:
for evt in oc._facts_seals:
for evt in oc._facts.seals:
_judge_seal(evt, ri)
_seal_strike_batch(ri)
@@ -213,7 +213,7 @@ func _seal_batch(ri: int) -> void:
func _seal_strike_batch(ri: int) -> void:
var fs_hits := 0
var fs_misses := 0
for fevt in oc._facts_seals:
for fevt in oc._facts.seals:
if bool(fevt.get("hit", false)):
fs_hits += 1
else:
@@ -223,7 +223,7 @@ func _seal_strike_batch(ri: int) -> void:
var strike := _seal_strike_value(fs_fx, fs_hits, fs_misses)
if strike.is_empty():
return
for fevt in oc._facts_seals:
for fevt in oc._facts.seals:
if not bool(fevt.get("hit", false)):
continue
var st_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_seal_strike", "战封印军规",
@@ -272,8 +272,8 @@ func _round_end_apply(ri: int) -> void:
var rap_pct: int = oc.attune_param_sum("rap_transfer_mult")
oc.action_economy.finish_round_stop(fx_host.engine.has_effect_on("round_end", "grant_ap", "transfer"),
rap_pct if rap_pct > 0 else 100)
if oc._facts_path == "cleared":
reward_grant.grant_clear_reward(ri, oc._board_from_midround_refresh, oc.perfect_sweep_earned())
if oc._facts.path == "cleared":
reward_grant.grant_clear_reward(ri, oc._facts.board_from_midround, oc.perfect_sweep_earned())
fx_host.flush_pending_reveals(ri) # 揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入)
oc._damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界)
@@ -305,10 +305,10 @@ func _judge_seal(evt: Dictionary, ri: int) -> void:
# 殉爆票据 → 施加面 → 雷区图谱额外兑付 → 基线正向兑付)
func _judge_seal_hit(evt: Dictionary, ri: int) -> void:
var cell: Vector2i = evt.cell
if oc._seal_consumed.has(cell):
if oc._facts.seal_consumed.has(cell):
return # 去重跳过;零明细行
oc._seal_consumed[cell] = true
oc._perfect_hit_true += 1
oc._facts.seal_consumed[cell] = true
oc._facts.mark_perfect_hit()
var fh_hits := fx_host.fx_emit("seal_hit", {"cell": cell, "round": ri,
"terrain_kind": oc._harmful_adjacent(cell)})
_seal_burst_tickets(fh_hits, cell, ri)
@@ -367,8 +367,8 @@ func _seal_preview_area(cell: Vector2i, ri: int) -> void:
var n_snap = oc._reward_economy.query_full_snapshot(n)
if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))):
types.append(String((n_snap as Dictionary).get("cell_type", "")))
if oc.treasure_inventory != null and not types.is_empty():
oc.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell))
if reward_grant.treasure_inventory != null and not types.is_empty():
reward_grant.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell))
oc._emit_row({"type": "extended", "source_class": "fx_warning_beacon", "origin_cell": cell}, ri, "示警烟火·周围资源类型", 0, {"effect_id": "fx_preview_area", "types": types})
@@ -376,10 +376,10 @@ func _seal_preview_area(cell: Vector2i, ri: int) -> void:
# grant_ap mode:queue 下一轮兑现 / forgive_seal_miss 完美保险豁免 → 归因行 → 负向兑付)
func _judge_seal_miss(evt: Dictionary, ri: int) -> void:
var cell: Vector2i = evt.cell
oc._perfect_hit_false += 1
if oc._seal_miss_seen.has(cell):
oc._facts.mark_perfect_miss()
if oc._facts.seal_miss_seen.has(cell):
return # 每格实例一次去重
oc._seal_miss_seen[cell] = true
oc._facts.seal_miss_seen[cell] = true
# 危险直觉·守挂点(seal_missed 发射:契约 12 触发主语=「当回合发生的标错
# 动作」——去重闸=新旧标错判别器〔后续轮重判旧封印被闸挡回零触发〕;双条件②「结算时封印仍在
# 场」由发射面既有过滤承载〔被挖开的封印已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮
@@ -391,11 +391,11 @@ func _judge_seal_miss(evt: Dictionary, ri: int) -> void:
elif String(h.get("kind", "")) == "forgive_seal_miss":
# 完美保险(订阅式豁免:本战首次标错计一次豁免,只累加自有账
# _perfect_forgiven,绝不回写 _perfect_hit_false=契约 2 计数纯净;发射时机与消费点零改动=契约 3)
oc._perfect_forgiven += 1
oc._facts.mark_perfect_forgiven()
_apply_counter_hit(h) # 豁免同时记账(insurance_used battle 域——下次标错 when lt 1 不再命中)
oc._emit_row({"type": "extended", "source_class": "seal_miss", "origin_cell": cell}, ri, "完美保险·标错豁免", 0,
{"effect_id": "insurance_used", "forgiven": oc._perfect_forgiven})
var hl := "perfect_broken" if oc._perfect_hit_false - oc._perfect_forgiven == 1 else ""
{"effect_id": "insurance_used", "forgiven": oc._facts.perfect_forgiven})
var hl := "perfect_broken" if oc._facts.perfect_hit_false - oc._facts.perfect_forgiven == 1 else ""
oc._emit_row({"type": "seal_miss", "source_class": "seal_miss", "origin_cell": cell, "highlight": hl}, ri, "封印未中", 0, {})
var snap = oc._reward_economy.grant(cell)
if not (snap is Dictionary):
+106
View File
@@ -0,0 +1,106 @@
## 轮循环驱动件:每轮结算周期的唯一驱动——结算重入门闸(settling)、五段段序执行
## (段序骨架冻结于 SettlementTable.segment_order)、开轮摇号(共识点 B:技能选择前置到轮次开始,
## 摇定即本拍结算的选技)、踩雷计数账(转存时点=摇号轮末)。
## 单一变化原因:轮结算周期的推进结构(段序、重入闸、摇号时序)。
## 耦合边界:oc=宿主引用(五段段类既定同款直调形态)——经 oc 取 facts/协作件/行发射/出口分发;
## 五段构造参数与持有形态不变(grant/combat 三引用、aura/decay/verdict 单引用);判定段零改动
## (_exit_distribution 留宿主);随机只经 oc.rng 的 behavior_roll 封闭流(流名、开流时点、取号序逐字);
## 零信号连接、零 await;随编排器每战新建同灭。
class_name RoundCycle
extends RefCounted
var oc: BattleOrchestrator # 宿主引用(段类同款直调——facts/协作件/行发射/出口分发取数单点)
var mines_hits: Dictionary[Variant, Variant] = {"total": 0, "last": 0, "round": 0} # 踩雷计数(本场累计/上轮转存/当轮累计三读数合一账)
var _grant_seg = null # GrantSegment(①授予段——含封印判与 counter/grant 助手单点)
var _aura_seg = null # AuraSegment(②增益/减益段)
var _combat_seg = null # CombatSegment(③战斗段)
var _decay_seg = null # DecaySegment(④衰减段)
var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归宿主)
var _settling := false
func _init(host: BattleOrchestrator, fx_host_ref: BattleFxHost, reward_grant_ref: RewardGrant) -> void:
oc = host
# 结算五段接线(授予/战斗两段持三引用=宿主+两协作件;信号连接纪律不涉段〔段零信号连接〕——五行自宿主 _init 逐字迁入)
_grant_seg = GrantSegment.new(oc, fx_host_ref, reward_grant_ref)
_aura_seg = AuraSegment.new(oc)
_combat_seg = CombatSegment.new(oc, fx_host_ref, reward_grant_ref)
_decay_seg = DecaySegment.new(oc)
_verdict_seg = VerdictSegment.new(oc)
# 结算重入门闸+段序体(原宿主 _maybe_settle 三闸与 _settle 合体——闸序 settling→battle_result→phase
# 逐字;终局早退不回置 _settling,_battle_over 接管)
func settle_if_due() -> void:
if _settling or oc.battle_result != "":
return
if oc.action_economy.phase != oc.action_economy.PHASE_WAITING:
return
_settling = true
var ri := oc.round_index
# 段序编排(五段——段体归 game/core/settlement/,宿主只做行发射/开单原语单点)
_grant_seg.run(ri) # ① 授予段(案C 闸随段装载)
_aura_seg.run(ri) # ② 增益/减益段(续存实例组合行)
_combat_seg.run(ri) # ③ 战斗段(主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕)
_decay_seg.run(ri) # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减)
var verdict: String = _verdict_seg.run(ri) # ⑤ 判定段(防御性优先级 hero 先判)
if verdict != "continue":
return # 终局:_settling 不回置——_battle_over 已接管
_settling = false
func is_settling() -> bool:
return _settling
# ---- 开轮摇号(共识点 B:技能选择前置到轮次开始,数值计算留结算段——原宿主函数逐字迁入)----
# 每实例:保底判定(连续执行普攻 ≥ M → 必出特殊技、特殊技内按权重、不受冷却限制)→ 排除冷却中技能
# (中签率 = 权重 ÷ 可摇权重合计——份额归一)→ 按行序逐敌从统一流顺序取号摇定本拍动作。
# 随机消费只在换算层(本方法 + SkillLottery);单位容器收确定值(零随机纪律——armor_roll 先例同构)。
# 统一流:behavior_roll 无子流派生向量(位置交错,单点定位由事件日志承载)。
# 摇定后、兑现前死亡 → 本拍技能作废。
func roll_round_actions() -> void:
var pity_m: int = int(oc.db.combat.skill.pity_threshold)
# facts 组包(开轮摇号前一次成型——last_round_end 空字典首轮映射 none 唯一落点;hero 事实不进 facts,
# 容器内直读主角行)
var facts := {
"round_index": oc.round_index,
"last_round_end": "none" if oc.action_economy.last_round_end().is_empty() else String(oc.action_economy.last_round_end().get("path_tag", "none")),
"mines_hit_last": int(mines_hits["last"]),
"mines_hit_total": int(mines_hits["total"]),
}
for u in oc._enemy_system.query_alive("enemy"):
# Boss 半血解锁闩(迭代14④ T4——开轮摇号前逐敌检查;一次过门单向闩;解锁出一次归因明细行
# 「XX 解锁了 YY」带 skill_unlock 高光键〔type=highlight 既有行型零新增〕,呈现层弹一次性
# 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机
# 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏)
for sid in oc._enemy_system.check_trigger_unlocks(u, facts):
var srow_u: Dictionary[Variant, Variant] = oc.db.combat.skill.row_by_id(sid)
oc._emit_row({"type": "highlight", "source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "highlight": "skill_unlock"},
oc.round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))],
0, {"skill_id": sid})
# enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射,
# 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 trigger 行值)
var pct_v := 0
for pe in (u.get("pool", []) as Array):
var pe_d: Dictionary[Variant, Variant] = pe
if String(pe_d.get("skill_id", "")) == sid and (pe_d.get("trigger", {}) as Dictionary).has("value"):
pct_v = int((pe_d.get("trigger", {}) as Dictionary).get("value", 0))
break
# target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记,
# 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value)
var eht_hits := oc._fx_host.fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v,
"is_boss": String(oc._facts.encounter.get("node_type", "")) == "boss"})
for h in eht_hits:
if String(h.get("kind", "")) == "target_attack_bonus":
u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0))
var entries: Array[Variant] = oc._enemy_system.rollable_skills(u, facts)
if oc._enemy_system.basic_streak(u) >= pity_m:
var specials: Array[Variant] = oc._enemy_system.special_skills(u, facts)
if not specials.is_empty():
entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合
var stream: RandomNumberGenerator = oc.rng.open_stream("behavior_roll")
oc._enemy_system.set_pending_action(u, SkillLottery.draw(entries, stream))
oc._enemy_system.tick_cooldowns() # 递减在摇号读值之后——"施放后连续 N 拍不可摇中"的等价实现(单位容器 注记)
mines_hits["last"] = mines_hits["round"] # 轮次真正结束转存(轮内部分结算不更新)
mines_hits["round"] = 0
+1
View File
@@ -0,0 +1 @@
uid://ce07smbypdcyj
+1 -1
View File
@@ -1,7 +1,7 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 商店会话件:关卡商店双模式(legacy 固定三件加定价/pool 共享令牌池)的货架、定价、购买与刷新唯一宿主。
## 钱包/主角账/装配集归宿主(GpWallet/GainJournal 共享引用经 bind 注入——GP 读写走钱包、购买加算走日记单点);
## Core 层零 UI 依赖;公开面由 LevelRunner 同签名委托承载(外部调用面零变化);
## Core 层零 UI 依赖;外部调用面=调用方经宿主 LevelRunner 公开成员 shop 直连(调用直连不留委托壳);
## legacy 模式零 RNG 零池消费(off 对照承载)。
class_name ShopSession
extends RefCounted
+12 -12
View File
@@ -170,37 +170,37 @@ func on_shop_buy(product_id: String) -> void:
var runner = _runner_get.call()
if runner == null:
return
if runner.is_shop_pool():
if runner.shop.is_shop_pool():
# pool 面:买 1 → 核心全重抽——货架整架重投喂(重抽反馈;余额不足核心拒绝零副作用)
if not runner.buy_pool_item(product_id):
if not runner.shop.buy_pool_item(product_id):
logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"})
toast.show_toast("GP不足——买不起这件商品", "warn")
return
shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp))
shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行
shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost())
shop_overlay.open_shelf(runner.shop.shop_shelf(), int(runner.gp))
shop_overlay.set_visit_info(int(runner.shop.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行
shop_overlay.set_refresh_display(runner.shop.shop_refresh_free_left(), runner.shop.shop_refresh_cost(), int(runner.gp) >= runner.shop.shop_refresh_cost())
_refresh_presentations.call()
return
if not runner.buy_product(product_id):
if not runner.shop.buy_product(product_id):
# 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1)
logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"})
toast.show_toast("GP不足——买不起这件商品", "warn")
return
shop_overlay.refresh_state(runner.shop_products(), int(runner.gp))
shop_overlay.refresh_state(runner.shop.shop_products(), int(runner.gp))
_refresh_presentations.call()
# MC-1:刷新意图(shop_overlay.refresh_requested——核心 shop_refresh 受理后整架重投喂=重抽反馈)
func on_shop_refresh() -> void:
var runner = _runner_get.call()
if runner == null or not runner.is_shop_pool():
if runner == null or not runner.shop.is_shop_pool():
return
if not runner.shop_refresh():
if not runner.shop.shop_refresh():
toast.show_toast("GP不足——刷新失败", "warn")
return
shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp))
shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行
shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost())
shop_overlay.open_shelf(runner.shop.shop_shelf(), int(runner.gp))
shop_overlay.set_visit_info(int(runner.shop.shop_bp_visit_buys()), float(db.run.shop.price_a)) # T6 爬价规则行
shop_overlay.set_refresh_display(runner.shop.shop_refresh_free_left(), runner.shop.shop_refresh_cost(), int(runner.gp) >= runner.shop.shop_refresh_cost())
_refresh_presentations.call()
@@ -160,7 +160,7 @@ func on_refresh() -> void:
if not refreshed:
# 免费位(X/Y)已穷——GP 付费档(先扣钱包后重抽,余额不足拒绝零副作用)
var cost: int = adventure.refresh_bank().gp_cost()
if runner == null or not runner.spend_gp(cost):
if runner == null or not runner.shop.spend_gp(cost):
ctx.views.toast.show_toast("GP不足——刷新失败", "warn")
return
refreshed = adventure.stop().refresh_curio_paid()
+77 -265
View File
@@ -1,7 +1,8 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 组合根:唯一的模式流转与 core↔view 接线处(垂直切片验证:线性第一关承载元进度循环)。
## db.hero 运行期只读,加成经 runner.set_hero_override / debug_session 生效值入战斗。
## 禁链式穿透 settlement.*(相位读取只走 presentation_snapshot);模态开合只走 _open_modal/_close_modal。
## 禁链式穿透 settlement.*(相位读取只走 presentation_snapshot);模态开合只走 ui.modals.flow(注册面编排口)。
## 界面句柄与层清单在 ui 注册面(ui_registry.gd)——屏壳/战区/模态三域句柄与 LAYER_CONTRACT 单点宿主。
## 局态全在 run_scope.gd 节点,禁在屏/视图侧持有局态引用;autoload 节点路径常量单点在 AUTOLOAD_PATH_*。
extends Control
@@ -15,10 +16,8 @@ const AUTOLOAD_PATH_META := "/root/MetaProgression"
const UiAssembler := preload("res://presentation/ui_assembler.gd")
const Narrator := preload("res://presentation/battle_narrator.gd")
const ModalStack := preload("res://presentation/modal_stack.gd")
const TuneSimController := preload("res://presentation/tune_sim_controller.gd")
const DebugSessionScript := preload("res://presentation/debug_session.gd")
const RowPacer := preload("res://presentation/row_pacer.gd") # 结算行排播泵
const TreasureInteractController := preload("res://presentation/treasure_interact_controller.gd") # 秘宝两步交互
const CurioInteractController := preload("res://presentation/curio_interact_controller.gd") # 三选一与替换子交互
const CampShopPresenter := preload("res://presentation/camp_shop_presenter.gd") # 营地/商店投影
@@ -31,33 +30,6 @@ const MenuScreenScene := preload("res://presentation/scenes/menu_screen.tscn")
const CampScreenScene := preload("res://presentation/scenes/camp_screen.tscn")
const AdventureScreenScene := preload("res://presentation/scenes/adventure_screen.tscn")
# ==================== 视觉层合同(唯一事实源——树序=登记序=z 序;改序只改此表) ====================
# kind: shell=屏壳容器(成员随壳不占独立树位;level_screen 例外另占登记位)/modal=模态层(入登记册,
# tags 随登记)/state=全屏态(占树位不入登记册)/node=非视觉节点(占树位不入登记册)。
# host=宿住屏壳(树序随宿主、登记序按本合同);shell=模态成员的屏壳(成员与屏壳同槽:屏壳占该树位、
# 成员占该登记位)。落位=_apply_layer_contract(全仓唯一层序落位点,add_child 之后统一排序+登记)。
const LAYER_CONTRACT: Array[Dictionary] = [
{"key": "adventure_screen", "kind": "shell"},
{"key": "menu_screen", "kind": "shell"},
{"key": "row_pacer", "kind": "node"},
{"key": "legend_overlay", "kind": "modal", "tags": []},
{"key": "node_overlay", "kind": "modal", "tags": []},
{"key": "level_screen", "kind": "modal", "tags": [ModalStack.TAG_SCREEN_STATE], "host": "adventure_screen"},
{"key": "curio_overlay", "kind": "modal", "tags": []},
{"key": "end_overlay", "kind": "modal", "tags": []},
{"key": "shop_overlay", "kind": "modal", "tags": []},
{"key": "camp_overlay", "kind": "modal", "tags": [], "shell": "camp_screen"},
{"key": "settings_overlay", "kind": "modal", "tags": []},
{"key": "status_panel", "kind": "modal", "tags": []},
{"key": "career_overlay", "kind": "modal", "tags": []},
{"key": "save_select_overlay", "kind": "modal", "tags": []}, # 选档界面(营地交互重做卡③——登记生涯同档)
{"key": "inter_overlay", "kind": "state", "tags": []},
{"key": "confirm_dialog", "kind": "modal", "tags": []},
{"key": "debug_overlay", "kind": "modal", "tags": [ModalStack.TAG_NON_LOCKING]},
{"key": "debug_badge", "kind": "float"}, # 调试入口全局浮层——末位=最顶绘制序;
# float 类不注册 ModalStack(open() 遮挡记账只隐藏登记层)→ 任何 UI 打开都不遮它
]
var db
var run_scope = null # 会话状态承载(run_scope.gd;_ready 即建闲时态,永非 null)
# SaveManager/MetaProgression 升格 autoload——成员名保留为「autoload 读视图」
@@ -71,99 +43,24 @@ var meta: MetaProgressionCore:
return get_node(AUTOLOAD_PATH_META)
var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件)
var _debug_entry := false # 调试会话门控(缺省 false=发布态:角标恒隐、浮窗无 UI 开启口;沿革查 git log)
var _seed_edit: LineEdit
var confirm_dialog: ConfirmDialog # 通用确认弹窗(消费方编排——现行消费方=选档删档确认;新冒险互斥确认已随卡②主菜单撤出,归卡④大门)
var ui: UiRegistry # 界面注册面(ui_registry.gd——界面句柄/层序合同/模态编排句柄的单点宿主)
var _confirm_consumer := "" # 确认弹窗消费方状态量(契约 3 编排——open 前设、分派后清)
var _confirm_consumer_slot := 0 # 消费方附随槽号(delete_slot 消费方专用)
var curio_overlay: CurioPickOverlay # 提升三选一覆盖层(level-clear.md v2 选择面承载;奖励节点共用)
var _start_overlay: Control
var _end_overlay: BattleEndOverlay
var _inter_overlay: Control
var _inter_label: Label
var level_screen: LevelMapScreen # 关卡画面句柄(指向 adventure_screen.level_screen)
var _battle_root: Control # 战斗屏宿主句柄(指向 adventure_screen.battle_root)
# 调试模式编排(单场/连续两层——状态机与战斗生命周期归 debug_session.gd,本类只接线与出 UI)
var _debug = null
# 视图(views/)——battle.md 三区(节点链进度
# 由关卡画面本体选路覆盖层承载)
var stage: BattleStageView # B 区(含 log_view 明细带)——句柄指向 adventure_screen
var log_view: BattleLogView
var info_panel: HeroInfoPanelView # C 区(攻击/AC 数据框+两预留区)
var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx)
var board_view: BoardView # 探针兼容别名(= board_zone.board_view)
var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar)
var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下)
# 三屏壳实例(屏成员经本类句柄暴露;探针挂载假设 main.X 一级路径)
var _menu_screen = null # MenuScreen(开始菜单屏壳)
var _camp_screen = null # CampScreen(营地屏壳)
var _adventure_screen = null # AdventureScreen(冒险屏壳——内含关卡地图/战斗两容器)
# 拆件后的组合根持有面(职责见各类头注;装配见 _build_ui)
var _row_pacer = null # 结算行排播泵(row_pacer.gd——本类只入队/排空/读队列)
var _treasure_ui = null # 秘宝两步交互(treasure_interact_controller.gd——本类只接线与转发意图)
var _bonus = null # 三选一与替换子交互(curio_interact_controller.gd——本类只接线与回调注入)
var _camp_pres = null # 营地/商店投影(camp_shop_presenter.gd——本类只接线、探针缝保缝转发)
# 排播队列只读委托面(探针/测试读 main._row_queue——与 RowPacer 队列共享同一 Array 引用)
var _row_queue: Array[Variant]:
get:
return _row_pacer.pending_rows()
# 模态 overlay(z 序 = 登记序;pattern 6 层级纪律)
var legend_overlay: LegendOverlay
var node_overlay: NodeOverlay
var shop_overlay: ShopOverlay # 商店覆盖层(商店房节点兑付呈现)
var camp_overlay: CampOverlay # 营地覆盖层句柄(指向 camp_screen.camp_overlay)
var settings_store # 系统级设置独立存储(音量/全屏/减动效)
var settings_overlay: SettingsOverlay # 设置覆盖层(最小设置面)
var status_panel: StatusPanel # 局内状态面板·只读(钥石余额+营地三柜台进度查看;登记设置层同档)
var career_overlay: CareerOverlay # 生涯页·只读全屏页(跨局汇总六类九项;登记营地同档〔设置层旁、确认弹窗之前〕)
var save_select_overlay: SaveSelectOverlay # 选档界面·3 存档位(新档/进档/删档——营地交互重做卡③;登记生涯同档)
var debug_overlay: DebugOverlay # 调试浮窗(种子/数值调整/批量模拟/两层调试入口收一)
var debug_badge: Button # 调试浮层入口角标(UI 最顶层浮层——LAYER_CONTRACT 末位 float 类,任何 UI 不遮挡)
var _modal_flow = null # 模态编排纯逻辑类(modal_flow.gd——依赖仅 ModalStack+查询 Callable)
var _modals = null # 模态层级状态机(=_modal_flow.stack——探针 _list 登记序断言缝保留)
var _tune_sim = null # 调试浮窗批量模拟控制器(tune_sim_controller.gd)
# 结算框挂起态属性化(探针/冒烟直读写接缝语义零变化——读=armed,写=arm/disarm;计时器随会话住
# run_scope.settlement_stop_timer)
var _settlement_stop_pending: bool:
get:
return run_scope.settlement_stop_timer.is_armed() if run_scope != null else false
set(v):
if run_scope == null:
return
if v:
run_scope.settlement_stop_timer.arm()
else:
run_scope.settlement_stop_timer.disarm()
# 锁期点击重放(v3.1):延迟一帧重入唯一动作通道——此刻队列空锁已解,按普通动作受理
#(局面在演播期无变化:逻辑先行、演播只是回放——点击语义保真;_row_queue=RowPacer 委托面,
# 排播泵本体在 row_pacer.gd)。
var _deferred_modal: Control: # 演播后开窗挂起只读视图(t17_defer_probe 挂起态断言缝;本体归 _modal_flow)
get:
return _modal_flow._deferred if _modal_flow != null else null
# 演播后排窗(伤害过程演完才切结算窗口):排播队列在播=挂起,演播完边沿
# (_on_pump_ended 派发)再开——旧路径 _open_modal 前置 flush 会把击杀轮的伤害/死亡演出
# 同帧倒掉(「直接跳战斗胜利」缺陷本体)。队列空=立即开(行为同旧路径)。
func _open_modal_after_playback(m: Control) -> void:
_modal_flow.open_after_playback(m) # 演播后排窗挂起编排归 ModalFlow
# 演播完边沿派发(row_pacer.pump_ended 注入口——先开挂起 modal,后锁期点击重放):
func _on_pump_ended() -> void:
_modal_flow.pump_ended() # 派发编排归 ModalFlow(重放经注入 Callable 回本类)
func _replay_locked_click() -> void:
if run_scope.locked_click.is_empty() or not _row_queue.is_empty():
if run_scope.locked_click.is_empty() or not ui.screens.row_pacer.pending_rows().is_empty():
return
var click: Dictionary[Variant, Variant] = run_scope.locked_click
run_scope.locked_click = {}
@@ -173,7 +70,7 @@ func _replay_locked_click() -> void:
# 会话生命周期:_ready 建闲时态(永非 null——全组合根零空判责),
# 开局/续玩/调试两层各建新会话,旧会话整节点 queue_free()(帧末回收;新节点先挂树无真空窗)。
func _begin_session(adventure_session: bool) -> void:
_row_pacer.flush() # 会话更替即清队(待播行不住局态——机制保证,不靠时序约定)
ui.screens.row_pacer.flush() # 会话更替即清队(待播行不住局态——机制保证,不靠时序约定)
if run_scope != null:
run_scope.queue_free()
run_scope = RunScope.new()
@@ -194,10 +91,11 @@ func _ready() -> void:
settings_store = SettingsStore.new()
settings_store.load_settings()
settings_store.apply()
ui = UiRegistry.new() # 注册面先于装配建(_build_ui 八步向容器登记——装配时序零变化)
_build_ui()
_begin_session(false) # 闲时会话(gp_ledger 等探针注入面自 _ready 起恒在——ui_probe 合成段时序)
_start_overlay.visible = true
debug_badge.visible = _debug_entry # 浮层角标可见性须门控落定后赋值(构建/连接在 _build_ui);_debug_entry 缺省 false=发布态恒隐
ui.screens.start_overlay.visible = true
ui.screens.debug_badge.visible = _debug_entry # 浮层角标可见性须门控落定后赋值(构建/连接在 _build_ui);_debug_entry 缺省 false=发布态恒隐
# boot 菜单优先——启动不自动开浮窗(开始屏不得被调试面板盖住);
# 调试入口=UI 最顶层全局浮层角标(LAYER_CONTRACT 末位 float 类,任何 UI 不遮挡)
@@ -206,94 +104,13 @@ func _ready() -> void:
func _build_ui() -> void:
MainUiBuild.build(self)
debug_overlay.diagnose_requested.connect(_on_diagnose_pressed) # 诊断转储入口
ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed) # 诊断转储入口
func _fatal_screen(msg: String) -> void:
MainUiBuild.fatal_screen(self, msg)
# ==================== 模态 overlay 层级管理(pattern 6 / 缺陷⑤——状态机归 modal_stack.gd) ====================
func _register_modal(m: Control, tags: Array = []) -> void:
# 撑满宿主(Control 默认锚 = 0×0——全屏变暗层与居中布局整体失效;route 选路层预览截图实证后
# 统一收口:六个模态同病,探针经信号直发绕过真实点击从未暴露。anchors_and_offsets 而非 anchors
# ——后者会把控件重置回最小内容尺寸,ui_assembler 头注同款纪律)
m.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_modal_flow.register(m, tags) # 排除组随层登记(non_locking=调试浮窗/screen_state=关卡地图屏)
# 合同层寻址(key→节点;纯查表不含层序语义——层序唯一事实源=LAYER_CONTRACT)
func _layer_node(key: String) -> Node:
match key:
"adventure_screen": return _adventure_screen
"menu_screen": return _menu_screen
"row_pacer": return _row_pacer
"legend_overlay": return legend_overlay
"node_overlay": return node_overlay
"level_screen": return level_screen
"curio_overlay": return curio_overlay
"end_overlay": return _end_overlay
"shop_overlay": return shop_overlay
"camp_screen": return _camp_screen
"camp_overlay": return camp_overlay
"settings_overlay": return settings_overlay
"status_panel": return status_panel
"career_overlay": return career_overlay
"save_select_overlay": return save_select_overlay
"inter_overlay": return _inter_overlay
"confirm_dialog": return confirm_dialog
"debug_overlay": return debug_overlay
"debug_badge": return debug_badge
return null
# 合同装配(_build_ui 尾调用):按 LAYER_CONTRACT 序统一落位——直接子层 move_child 到合同槽、
# 模态层按合同序登记;宿住层(host)树序随宿主只登记;成员层(shell)与屏壳同槽(屏壳占树位、成员占登记位)。
# 本函数是全仓唯一层序落位点(move_child 单口)——改层序只改 LAYER_CONTRACT。
func _apply_layer_contract() -> void:
var slot := 0
for entry: Dictionary in LAYER_CONTRACT:
var tags: Array[Variant] = entry.get("tags", [])
if entry.has("shell"):
var shell: Node = _layer_node(String(entry["shell"]))
var member: Node = _layer_node(String(entry["key"]))
if shell != null:
move_child(shell, slot)
slot += 1
if member != null:
_register_modal(member as Control, tags)
else:
push_error("[合同装配] 层缺失: %s" % String(entry["key"]))
continue
var node: Node = _layer_node(String(entry["key"]))
if node == null:
push_error("[合同装配] 层缺失: %s" % String(entry["key"]))
continue
if entry.has("host"):
_register_modal(node as Control, tags)
continue
move_child(node, slot)
slot += 1
if String(entry["kind"]) == "modal":
_register_modal(node as Control, tags)
func _open_modal(m: Control) -> void:
_modal_flow.open(m) # 挂起兜底/排播排空/遮挡记账/开侧同步编排归 ModalFlow
func _close_modal(m: Control) -> void:
_modal_flow.close(m) # 关闭恢复+关侧同步归 ModalFlow(同上)
func _on_modal_closed(m: Control) -> void:
_close_modal(m)
func _input_locked() -> bool:
# 模态/非战斗期棋盘输入锁(判据编排归 ModalFlow.locks_input:互斥域随层登记
# [non_locking=调试浮窗]+ 三例外经查询 Callable 注入;本函数留探针缝)
return _modal_flow.locks_input()
# ==================== 冒险模式入口(唯一试玩入口)====================
func _start_adventure_from_menu() -> void:
@@ -301,7 +118,7 @@ func _start_adventure_from_menu() -> void:
# 弹窗本体与两分支逻辑归卡④大门承接)。本函数为探针直调中间态保留(mn10——冒险段探针
# 直调位点逐个登记实现报告「已知中间态」;卡④到位后由大门两分支换代)。无激活态写盘拒绝
# 由卡① sv3 承接(结算不落盘)。
_start_overlay.visible = false
ui.screens.start_overlay.visible = false
_start_adventure()
@@ -309,16 +126,16 @@ func _start_adventure_from_menu() -> void:
#(确认才删、取消只关窗——裁决 3)。"new_adventure" 消费方已随卡② 主菜单确认链摘除退役
#(mn6/V5——主菜单零确认残留),分派机制保留(卡④ 到位接入第三消费方不返工)。
func _on_confirm_accepted() -> void:
_close_modal(confirm_dialog) # 模态栈单口
ui.modals.flow.close(ui.modals.confirm_dialog) # 模态栈单口
match _confirm_consumer:
"delete_slot":
save_manager.slots.delete_slot(_confirm_consumer_slot)
save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态)
ui.modals.save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态)
_confirm_consumer = ""
func _on_confirm_declined() -> void:
_close_modal(confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删);关侧恢复归 ModalFlow
ui.modals.flow.close(ui.modals.confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删);关侧恢复归 ModalFlow
_confirm_consumer = ""
@@ -334,16 +151,16 @@ func _resume_from_menu() -> void:
if not save_manager.has_breakpoint():
return
var bp: Dictionary[Variant, Variant] = save_manager.breakpoint_snapshot()
_close_modal(debug_overlay) # 开冒险路径显式关浮窗(续玩同口径)
_start_overlay.visible = false
ui.modals.flow.close(ui.modals.debug_overlay) # 开冒险路径显式关浮窗(续玩同口径)
ui.screens.start_overlay.visible = false
_begin_session(true)
_wire_adventure()
run_scope.level_active = true
_sync_status_entry() # 续玩恢复点亮状态面板入口角标
level_screen.set_abandon_available(true)
log_view.clear()
ui.modals.level_screen.set_abandon_available(true)
ui.battle.log_view.clear()
logger.start_session(int(bp.get("master_seed", 0)))
run_scope.adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct())
run_scope.adventure.set_hero_override(ui.modals.debug_overlay.atk_pct(), ui.modals.debug_overlay.hp_pct())
# 同调三键随续玩下传(build_adventure_params 统一构造,与开新局同源同口径)
if not run_scope.adventure.resume_adventure(bp, build_adventure_params(save_manager, meta,
{"treasure_unlocked": _treasure_ui.unlocked_set()})):
@@ -379,16 +196,16 @@ static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref:
# 冒险模式(三关连探——AdventureRunner 编排;转发信号签名与 runner 一致,接线照原样对接;
# 信号连接只在组合根)
func _start_adventure() -> void:
_close_modal(debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开)
ui.modals.flow.close(ui.modals.debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开)
_begin_session(true)
_wire_adventure()
run_scope.level_active = true
_sync_status_entry() # 进对局点亮状态面板入口角标
level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期)
log_view.clear()
ui.modals.level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期)
ui.battle.log_view.clear()
var seed_val := _seed_value()
logger.start_session(seed_val)
run_scope.adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct())
run_scope.adventure.set_hero_override(ui.modals.debug_overlay.atk_pct(), ui.modals.debug_overlay.hp_pct())
# 永久强化注入:冒险开始参数面——meta 现算 override 显式传参(全零 = 白板)
# 开局参数面(同调三键同源构造——确定性承载=固定种子+参数档,首局教学图由种子域三分判定给 42)
if not run_scope.adventure.start_adventure(seed_val, build_adventure_params(save_manager, meta,
@@ -435,33 +252,33 @@ func _on_inter_next_pressed() -> void:
func _show_board_fx_for_node(node_type: String = "") -> void:
# Boss 遭遇变体:紫赤影子爬棋盘一次性占位演出(§2.6)
if node_type == "boss":
board_zone.fx.play_boss_shadow()
ui.battle.board_zone.fx.play_boss_shadow()
func _on_battle_finished(payload: Dictionary) -> void:
# 仅调试模式接线(冒险的关卡结束归 AdventureRunner._on_level_finished——大奖励/冒险终局分派)
var result: String = String(payload.get("result", ""))
_sync_settlement_visuals() # → 全关(battle_over 反馈面)
HudPresenter.battle_end_feedback(stage, ap_bar, true) # 灰化/idle 回正/RAP 清零/状态全清——HUD 单通道
HudPresenter.battle_end_feedback(ui.battle.stage, ui.battle.board_zone.ap_bar, true) # 灰化/idle 回正/RAP 清零/状态全清——HUD 单通道
var decision: Dictionary[Variant, Variant] = _debug.on_battle_finished(payload)
match String(decision.outcome):
"next_layer":
_inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [
ui.screens.inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [
int(decision.layer), int(decision.carry_hp), int(db.combat.hero.max_hp)]
_inter_overlay.visible = true
ui.screens.inter_overlay.visible = true
_refresh_presentations()
return
"level_end":
_end_overlay.open_victory(Narrator.battle_end_text("victory", true),
"返回", board_view.get_global_rect(), _end_victory_opts(payload))
ui.modals.end_overlay.open_victory(Narrator.battle_end_text("victory", true),
"返回", ui.battle.board_zone.board_view.get_global_rect(), _end_victory_opts(payload))
"standalone_end":
if result == "victory":
_end_overlay.open_victory(Narrator.battle_end_text("victory", false),
"回到遭遇选择", board_view.get_global_rect(), _end_victory_opts(payload))
ui.modals.end_overlay.open_victory(Narrator.battle_end_text("victory", false),
"回到遭遇选择", ui.battle.board_zone.board_view.get_global_rect(), _end_victory_opts(payload))
else:
_end_overlay.open_defeat(Narrator.battle_end_text("defeat", false),
"返回", board_view.get_global_rect())
_open_modal_after_playback(_end_overlay)
ui.modals.end_overlay.open_defeat(Narrator.battle_end_text("defeat", false),
"返回", ui.battle.board_zone.board_view.get_global_rect())
ui.modals.flow.open_after_playback(ui.modals.end_overlay)
_refresh_presentations()
@@ -499,22 +316,17 @@ func _on_step_row(row: Dictionary) -> void:
# 清图奖励可感知反馈(挖完全图须有奖励感——clear_blast 兑付对敌伤害
# 零可见,toast 桥接归组合根呈现层;零兑付(未生效)不弹安静留明细带)
if String(row.get("source_class", "")) == "clear_reward" and String(row.get("type", "")) != "reward_ineffective":
board_zone.toast.show_toast("清图奖励:%s" % String(row.get("display", "")).replace("(清图奖励)", ""), "info")
ui.battle.board_zone.toast.show_toast("清图奖励:%s" % String(row.get("display", "")).replace("(清图奖励)", ""), "info")
# 排播入口:减动效=延迟归零同帧直落(行呈现不跳过);常态入队按段阶梯播放
if UiKit.motion_scale < 1.0:
_row_pacer.consume_now(row)
ui.screens.row_pacer.consume_now(row)
return
_row_pacer.enqueue(row)
ui.screens.row_pacer.enqueue(row)
# 段间延迟取数面(减动效双态断言用——pacing_queue_test 直读;委托 row_pacer 同名面)
func pace_delay_for(prev_seg: String, next_seg: String) -> float:
return _row_pacer.pace_delay_for(prev_seg, next_seg)
# 强制排空(模态开启前调用——明细回看/停点覆盖层/终局面所见明细带恒完整;pacing_queue_test 直读)
func _flush_row_queue() -> void:
_row_pacer.flush()
return ui.screens.row_pacer.pace_delay_for(prev_seg, next_seg)
func _on_round_started(ri: int) -> void:
@@ -522,10 +334,10 @@ func _on_round_started(ri: int) -> void:
#(view 内判减动效=静态化变体)。减动效快路=分隔行同帧直落(不跳过——consume_now 复用
# _consume_row 轮首分隔行分支)。
if UiKit.motion_scale < 1.0:
_row_pacer.consume_now({"__round_line": ri})
ui.screens.row_pacer.consume_now({"__round_line": ri})
else:
_row_pacer.enqueue({"__round_line": ri})
settlement_fx.play_round_banner(ri)
ui.screens.row_pacer.enqueue({"__round_line": ri})
ui.battle.settlement_fx.play_round_banner(ri)
# 秘宝呈现同步:每战首轮装配链
# 显式重算〔🔴 契约复原语义:下一战装配链重算即含「未装配→写 0」复原〕。
# 挂点时序依据:round_started 首轮发射位在 begin_battle 尾(attunement 注入之后)。
@@ -534,7 +346,7 @@ func _on_round_started(ri: int) -> void:
_sync_treasure_glow()
# 巢穴预告呈现(装板报数量读数口消费:≥0=本战装配命中,toast 一次性提示)
if run_scope.settlement != null and int(run_scope.settlement.nest_preview_count) >= 0:
board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(run_scope.settlement.nest_preview_count), "info")
ui.battle.board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(run_scope.settlement.nest_preview_count), "info")
# 开战横幅——整图地形变异预告:首轮快照 board_variants
# 非空才发射(Boss 图恒带一型=替换制;普通图空快照零发射);轮横幅下方叠行,view 内判减动效。
if ri == 1 and run_scope.settlement != null:
@@ -542,7 +354,7 @@ func _on_round_started(ri: int) -> void:
if not bvs.is_empty():
var vlines: Array[String] = HudPresenter.board_variant_lines(db, bvs)
if not vlines.is_empty():
settlement_fx.play_board_variant_banner("地形变异 · " + " · ".join(vlines))
ui.battle.settlement_fx.play_board_variant_banner("地形变异 · " + " · ".join(vlines))
_refresh_presentations()
@@ -552,19 +364,19 @@ func _on_board_changed() -> void:
run_scope.settlement = run_scope.runner.current_battle()
if run_scope.settlement == null:
return
board_view.rebuild(run_scope.settlement.minesweeper.current_board(), run_scope.settlement.minesweeper)
ui.battle.board_zone.board_view.rebuild(run_scope.settlement.minesweeper.current_board(), run_scope.settlement.minesweeper)
_check_board_hints()
_refresh_presentations()
# 秘宝视野呈现桥:阔视野带 light_radius_bonus
# 装配参数合计显式写给 board_view.set_fog_glow_bonus(装配=半径 8+4 / 未装配=复原 0),写入即 setter
# 装配参数合计显式写给 ui.battle.board_zone.board_view.set_fog_glow_bonus(装配=半径 8+4 / 未装配=复原 0),写入即 setter
# 内 refresh 全板重算雾态。调用点=每战首轮装配链(_on_round_started ri==1)——纯呈现边界 🔴 契约 2:
# 只写呈现态参数,minesweeper 结算零触碰;off 态(未装配)写 0=缺省基线。
func _sync_treasure_glow() -> void:
if run_scope.settlement == null or board_view == null:
if run_scope.settlement == null or ui.battle.board_zone.board_view == null:
return
board_view.set_fog_glow_bonus(run_scope.settlement.attune_param_sum("light_radius_bonus"))
ui.battle.board_zone.board_view.set_fog_glow_bonus(run_scope.settlement.attune_param_sum("light_radius_bonus"))
# ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ----
@@ -575,10 +387,10 @@ func _check_board_hints() -> void:
return
# 零合法板(棋盘生成 EC5 降级)诚实提示——非静默(toast,非模态)
if board.illegal_product:
board_zone.toast.show_toast("本板为零合法板降级产物——出图器已尽力,数据面诚实告知", "warn")
ui.battle.board_zone.toast.show_toast("本板为零合法板降级产物——出图器已尽力,数据面诚实告知", "warn")
# 接受最优降级(有合法板但无一达标)——降级标记独立于非法产物标记,同走诚实告知
elif board.degraded_product:
board_zone.toast.show_toast("本板为接受最优降级产物(未达可推理率及格线)——数据面诚实告知", "warn")
ui.battle.board_zone.toast.show_toast("本板为接受最优降级产物(未达可推理率及格线)——数据面诚实告知", "warn")
func _on_hover_forbidden(_cell: Vector2i) -> void:
@@ -594,7 +406,7 @@ func _on_chord_attempted(_cell: Vector2i) -> void:
func _show_hint(text: String, hint_id: String) -> void:
run_scope.hints_shown[hint_id] = true
logger.log_ux("ux_hint_shown", {"hint_id": hint_id})
board_zone.toast.show_toast(text, "info")
ui.battle.board_zone.toast.show_toast(text, "info")
# ---- 节点覆盖层"继续"(奖励/恢复点兑付确认)与节点图选路覆盖层 ----
@@ -706,15 +518,15 @@ func _on_wallet_changed(event: String, gp: int, extra: Dictionary)-> void:
func _on_board_action(cell: Vector2i, kind: String) -> void:
# 玩家动作 → 核心写调用的唯一通道(模态/无战斗拒绝在此收口)
if _input_locked():
if ui.modals.flow.locks_input():
return
# 结算演播期棋盘冻结(v3 口径——不可点/不可移/不可挖):
# 锁面=排播队列活跃(纯队列判据;排播泵尾迹窗口不可作演播中标志——见 row_pacer._process 注)。
# 队列排空即解锁、无状态残留;HUD 按钮不在锁面(停手/明细回看各自通路,开启前
# _flush_row_queue 自行排空)。
# 开层前排空钩子(flow.flush_rows——模态开启前明细带恒完整)。
# **点击不丢弃**(v3.1——「卡死」体感根因):锁期点击缓存最后一次,演播完边沿
# (_process 泵 _was_pumping 分支)自动重放——玩家操作零丢失、体感=「演播一停动作就生效」。
if not _row_queue.is_empty():
if not ui.screens.row_pacer.pending_rows().is_empty():
run_scope.locked_click = {"cell": cell, "kind": kind}
return
# 秘宝选中态劫持(两步交互第二步):点格=使用目标拾取,不落挖格;
@@ -734,22 +546,22 @@ func _on_board_action(cell: Vector2i, kind: String) -> void:
# settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 settlement 已被
# runner 置空,清图演出判定须先判 settlement(重入隐患守卫)
if accepted and kind == "dig" and run_scope.settlement != null and run_scope.settlement.minesweeper.current_board() != null and run_scope.settlement.minesweeper.clear_map_cleared():
board_zone.fx.play_clear_burst()
ui.battle.board_zone.fx.play_clear_burst()
if accepted and kind == "dig" and run_scope.settlement != null and run_scope.settlement.minesweeper != null:
# 挖掘演出:挥铲三帧 + 角色弹跳——触发=格子点开那一刻
#(settle_batch 同步受理即状态已变,动画纯表现与 refresh 同拍零竞态;
# 挖掘不移动英雄,出手方向 = 目标格−英雄格,圆心=英雄持握手位)
var hero_cell: Vector2i = run_scope.settlement.minesweeper.hero_pos()
board_zone.fx.play_shovel_swing(board_view.hero_hand_point(), cell - hero_cell, board_view.hero_sprite_scale())
board_view.play_hero_dig_bounce()
ui.battle.board_zone.fx.play_shovel_swing(ui.battle.board_zone.board_view.hero_hand_point(), cell - hero_cell, ui.battle.board_zone.board_view.hero_sprite_scale())
ui.battle.board_zone.board_view.play_hero_dig_bounce()
elif accepted and kind == "move":
board_view.notify_hero_move() # 原地移动(目标=当前格幂等受理)帧闪回执——refresh diff 前置告知
board_view.refresh()
ui.battle.board_zone.board_view.notify_hero_move() # 原地移动(目标=当前格幂等受理)帧闪回执——refresh diff 前置告知
ui.battle.board_zone.board_view.refresh()
_refresh_presentations()
func _on_stop_pressed() -> void:
if _input_locked():
if ui.modals.flow.locks_input():
return
# stop 经批次提交(排雷玩法 D1×6①——不直调 行动经济)
if run_scope.settlement == null:
@@ -758,25 +570,25 @@ func _on_stop_pressed() -> void:
logger.log_action("stop", Vector2i(-1, -1), not outcomes.is_empty() and bool(outcomes[0].accepted))
if not outcomes.is_empty() and String(outcomes[0].get("outcome", "")) == "voided":
# voided ≠ 拒绝——独立文案(pattern 1:防静默改判读感欺骗)
board_zone.toast.show_toast("本轮已清图,停手未执行 · 剩余 AP 已作废", "warn")
ui.battle.board_zone.toast.show_toast("本轮已清图,停手未执行 · 剩余 AP 已作废", "warn")
logger.log_ux("ux_reject", {"source": "voided", "sub_reason": "stop_voided"})
board_view.refresh()
ui.battle.board_zone.board_view.refresh()
_refresh_presentations()
func _on_refresh_confirmed() -> void:
# 弃图刷新(挖掘期 Active 当场换图继续挖——D4;RAP 按门槛扣费 · 旧板封印作废,每轮上限读表)
if _input_locked():
if ui.modals.flow.locks_input():
return
var ok: bool = false
if run_scope.level_active and run_scope.runner != null:
ok = run_scope.runner.player_request_refresh()
ok = run_scope.runner.shop.player_request_refresh()
elif run_scope.settlement != null:
ok = run_scope.settlement.player_request_refresh()
logger.log_action("refresh", Vector2i(-1, -1), ok)
if ok:
logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_snapshot()})
board_zone.toast.show_toast("RAP−%d · 旧板封印作废——新图已就位,AP 保留继续挖" % int(db.run.action_economy.rap_refresh_cost), "gold")
ui.battle.board_zone.toast.show_toast("RAP−%d · 旧板封印作废——新图已就位,AP 保留继续挖" % int(db.run.action_economy.rap_refresh_cost), "gold")
else:
# 资源面/上限拒绝(EC13 合法拒绝可见;行动经济 无载荷——客户端按上下文推断)
var rap: int = int(run_scope.settlement.presentation_snapshot().get("rap", 0)) if run_scope.settlement != null else 0
@@ -784,11 +596,11 @@ func _on_refresh_confirmed() -> void:
var result := "rejected_rap" if rap < cost else "rejected_cap"
logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_snapshot() + 1})
if result == "rejected_rap":
board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn")
ap_bar.pulse_rap()
ui.battle.board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn")
ui.battle.board_zone.ap_bar.pulse_rap()
else:
board_zone.toast.show_toast("本轮刷新次数已用完(每轮 2 次)", "warn")
board_view.refresh()
ui.battle.board_zone.toast.show_toast("本轮刷新次数已用完(每轮 2 次)", "warn")
ui.battle.board_zone.board_view.refresh()
_refresh_presentations()
@@ -798,9 +610,9 @@ func _handle_outcome_feedback(outcome: Dictionary) -> void:
return
logger.log_ux("ux_reject", {"source": String(outcome.get("reject_reason", "")),
"sub_reason": String(outcome.get("sub_reason", ""))})
board_zone.toast.show_toast(HudPresenter.outcome_text(outcome), "warn")
ui.battle.board_zone.toast.show_toast(HudPresenter.outcome_text(outcome), "warn")
if String(outcome.get("reject_reason", "")) == "resource_face":
ap_bar.pulse_ap()
ui.battle.board_zone.ap_bar.pulse_ap()
# ==================== 全屏 States 状态机(挖掘/结算/窗口/终局)====================
@@ -816,7 +628,7 @@ func _refresh_used_snapshot() -> int:
func _refresh_presentations() -> void:
# 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查)
var refresh_remaining: int = maxi(0, int(db.run.action_economy.refresh_cap_per_round) - _refresh_used_snapshot())
HudPresenter.refresh(stage, info_panel, ap_bar, {"settlement": run_scope.settlement, "runner": run_scope.runner, "level_active": run_scope.level_active, "db": db, "refresh_remaining": refresh_remaining})
HudPresenter.refresh(ui.battle.stage, ui.battle.info_panel, ui.battle.board_zone.ap_bar, {"settlement": run_scope.settlement, "runner": run_scope.runner, "level_active": run_scope.level_active, "db": db, "refresh_remaining": refresh_remaining})
_treasure_ui.refresh_slots() # C 区秘宝槽重查(试炼镐积点进度随挖格刷新悬停浮窗)
_sync_settlement_visuals()
@@ -825,9 +637,9 @@ func _sync_settlement_visuals() -> void:
# 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开 84px
var settling := _is_settling()
if settling and not run_scope.was_settling:
settlement_fx.play_round_sweep(board_view.get_global_rect()) # 结算轮光带——时序敏感演出触发,例外留在组合根
ui.battle.settlement_fx.play_round_sweep(ui.battle.board_zone.board_view.get_global_rect()) # 结算轮光带——时序敏感演出触发,例外留在组合根
run_scope.was_settling = settling
HudPresenter.sync_settlement_visuals(stage, board_view, log_view, settling,
HudPresenter.sync_settlement_visuals(ui.battle.stage, ui.battle.board_zone.board_view, ui.battle.log_view, settling,
HudPresenter.phase_text(run_scope.settlement, run_scope.runner, run_scope.boss_active, db))
@@ -836,13 +648,13 @@ func _is_settling() -> bool:
# 判据若只跟逻辑态,演播期棋盘会提前恢复可挖观感=「没意识到在结算演出」感知缺口根源;
# 演播期维持去饱和+禁挖光(结算中语义),放完自然回挖掘认知。判据=纯队列(排播泵尾迹
# 窗口不可作演播中标志——row_pacer._process 注)。battle 收尾演播例外:
# overlay 开启即 _flush_row_queue 排空,视觉随队列空同步交接结算框。
# overlay 开启经 flow.flush_rows 钩子排空,视觉随队列空同步交接结算框。
# 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透)。
if run_scope.settlement == null or not run_scope.settlement.is_battle_live():
return not _row_queue.is_empty()
return not ui.screens.row_pacer.pending_rows().is_empty()
if String(run_scope.settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING:
return true
return not _row_queue.is_empty()
return not ui.screens.row_pacer.pending_rows().is_empty()
# 相位行 —— 承接体见 HudPresenter.phase_text(caller 直接改引)
@@ -908,8 +720,8 @@ func _seed_value() -> int:
# 重放模板 0 即在此种子下标定〕;置位 true=教学局已收束的非首局走 ③ 随机);
# ③非首局空/非法框=引擎全局熵 randi()(Godot 4 启动自播种子,headless 同;不走 rng_service
# 封闭流、禁 randomize()/自建 RNG——组合根建种子属引擎熵豁免面,不进玩法 RNG 消费)
if _seed_edit.text.is_valid_int():
return int(_seed_edit.text)
if ui.modals.debug_overlay.seed_edit.text.is_valid_int():
return int(ui.modals.debug_overlay.seed_edit.text)
if save_manager.meta_state.career_state().first_adventure_done:
return randi() # rng-seed-exempt(非首局空框引擎熵——组合根建种子不进玩法 RNG 消费,static_scan S1/S2 标记豁免)
return 42
+46 -46
View File
@@ -30,10 +30,10 @@ static func _on_adventure_level_started(root: Variant, level_index: int, display
# 开场行只显示 atk/max_hp 两值,注记指向不存在的显示轴反成失真),白板局不带
var ov: Dictionary[Variant, Variant] = root.meta.hero_override()
var boosted: bool = int(ov.get("atk", 0)) > 0 or int(ov.get("max_hp", 0)) > 0 \
or root.debug_overlay.atk_pct() > 0 or root.debug_overlay.hp_pct() > 0 \
or root.ui.modals.debug_overlay.atk_pct() > 0 or root.ui.modals.debug_overlay.hp_pct() > 0 \
or root.run_scope.runner.journal().bonus_total("atk") > 0 \
or root.run_scope.runner.journal().bonus_total("max_hp") > 0
root.log_view.append_bb(root.Narrator.level_open_line(display_name, int(root.run_scope.adventure.master_seed),
root.ui.battle.log_view.append_bb(root.Narrator.level_open_line(display_name, int(root.run_scope.adventure.master_seed),
root.run_scope.runner.effective_atk(), root.run_scope.runner.effective_max_hp(), boosted))
# runner 六信号重连(is_connected 守卫——未连即接;守卫失败 push_error=重连恰好一次契约承载)
@@ -62,30 +62,30 @@ static func _on_runner_node(root: Variant, node: Dictionary, info: Dictionary) -
root.logger.attach_battle(root.run_scope.settlement)
root.run_scope.boss_active = t == "boss"
root._show_board_fx_for_node(t)
root.log_view.append_bb(root.Narrator.node_banner(root.run_scope.runner, node, t))
root.ui.battle.log_view.append_bb(root.Narrator.node_banner(root.run_scope.runner, node, t))
elif t == "reward":
# 奖励节点三选一(与关卡完成三选一共享同池同权重同上限;CurioPickOverlay 共用面)。
# 呈现编排归 curio_interact_controller.present_node_curio。
root._bonus.present_node_curio()
root.log_view.append_bb(root.Narrator.reward_line())
root.ui.battle.log_view.append_bb(root.Narrator.reward_line())
elif t == "rest":
var healed: int = int(info.get("healed", 0))
root._open_modal(root.node_overlay)
root.node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0)))
root.log_view.append_bb(root.Narrator.rest_line(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0))))
root.ui.modals.flow.open(root.ui.modals.node_overlay)
root.ui.modals.node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0)))
root.ui.battle.log_view.append_bb(root.Narrator.rest_line(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0))))
elif t == "shop":
# 商店房兑付:到达即开店——商品/货架快照与余额经 runner 只读投影喂入;
# 购买/离开归玩家意图(buy_requested/leave_pressed 接线),零自动推进。
# pool 面 = 共享池货架三选一同构卡;legacy 面 = 固定三件(off 对照承载)
root._open_modal(root.shop_overlay)
if root.run_scope.runner.is_shop_pool():
root.shop_overlay.open_shelf(root.run_scope.runner.shop_shelf(), int(root.run_scope.runner.gp))
root.shop_overlay.set_visit_info(int(root.run_scope.runner.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行
root.shop_overlay.set_refresh_display(root.run_scope.runner.shop_refresh_free_left(), root.run_scope.runner.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop_refresh_cost()) # MC-1
root.ui.modals.flow.open(root.ui.modals.shop_overlay)
if root.run_scope.runner.shop.is_shop_pool():
root.ui.modals.shop_overlay.open_shelf(root.run_scope.runner.shop.shop_shelf(), int(root.run_scope.runner.gp))
root.ui.modals.shop_overlay.set_visit_info(int(root.run_scope.runner.shop.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行
root.ui.modals.shop_overlay.set_refresh_display(root.run_scope.runner.shop.shop_refresh_free_left(), root.run_scope.runner.shop.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop.shop_refresh_cost()) # MC-1
else:
root.shop_overlay.open_shop(root.run_scope.runner.shop_products(), int(root.run_scope.runner.gp))
root.shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载)
root.log_view.append_bb(root.Narrator.shop_line(int(root.run_scope.runner.gp)))
root.ui.modals.shop_overlay.open_shop(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp))
root.ui.modals.shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载)
root.ui.battle.log_view.append_bb(root.Narrator.shop_line(int(root.run_scope.runner.gp)))
root.logger.log_node(node, info, root.run_scope.runner.node_index)
root._refresh_presentations()
@@ -101,7 +101,7 @@ static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void:
_exit_level_screen(root) # 终局兜底:独立屏在开态(放弃入口)也先回战斗屏呈现摘要——屏级互斥单一出口
root._sync_status_entry() # 离局熄灭状态面板入口角标(防菜单误现)
root._sync_settlement_visuals() # → 全关
HudPresenter.battle_end_feedback(root.stage, root.ap_bar, false) # 灰化/idle 回正/状态全清——HUD 单通道
HudPresenter.battle_end_feedback(root.ui.battle.stage, root.ui.battle.board_zone.ap_bar, false) # 灰化/idle 回正/状态全清——HUD 单通道
# 存档入账:settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变——
# 调试两层不经过本处理器,天然不写档不污染生涯数据)。
var detail: Dictionary[String, Variant] = {}
@@ -110,30 +110,30 @@ static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void:
if not detail.is_empty():
text += "\n" + root.Narrator.keystone_settle_text(detail, root._bonus.item_name_lookup())
if result == "completed":
root._end_overlay.open_victory(text, "返回冒险", root.board_view.get_global_rect(),
root.ui.modals.end_overlay.open_victory(text, "返回冒险", root.ui.battle.board_zone.board_view.get_global_rect(),
{"gp_summary": "本局 GP 入账 · 合计 %d" % int(summary.get("gp_final", 0))}) # GP 行(取数=summary gp_final 清账前快照);钥石明细已在 text(keystone_settle_text)——三行账
else:
root._end_overlay.open_defeat(text, "返回", root.board_view.get_global_rect())
root.ui.modals.end_overlay.open_defeat(text, "返回", root.ui.battle.board_zone.board_view.get_global_rect())
root.logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷)
if root.run_scope.runner != null:
root.run_scope.runner.restore_hero_base()
root._open_modal_after_playback(root._end_overlay)
root.ui.modals.flow.open_after_playback(root.ui.modals.end_overlay)
root._refresh_presentations()
static func _on_battle_end_continue(root: Variant) -> void:
# 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石在收束时入存档)
root._close_modal(root._end_overlay)
root._end_overlay.close()
root.ui.modals.flow.close(root.ui.modals.end_overlay)
root.ui.modals.end_overlay.close()
# 三选一停点挂起分支:战斗总结结算(Boss 胜利钥石增量行)确认后进三选一面
if root._bonus.resume_if_pending(): # 三选一停点在途恢复入口(curio_interact_controller)
return
# 挂起分派:战斗胜利总结结算框的「下一节点」≠ 终局——进选路页(暂存候选恢复呈现)
if root._settlement_stop_pending:
root._settlement_stop_pending = false
if root.run_scope.settlement_stop_timer.is_armed():
root.run_scope.settlement_stop_timer.disarm()
_enter_level_screen(root)
root.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
root.level_screen.open_choices(root.run_scope.pending_candidates, root.run_scope.runner)
root.level_screen.apply_status(level_map_status_payload(root.run_scope.runner))
root.ui.modals.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
root.ui.modals.level_screen.open_choices(root.run_scope.pending_candidates, root.run_scope.runner)
root.ui.modals.level_screen.apply_status(level_map_status_payload(root.run_scope.runner))
root.run_scope.pending_candidates = []
root._refresh_presentations()
return
@@ -141,12 +141,12 @@ static func _on_battle_end_continue(root: Variant) -> void:
# 局态封印/账本/模态挂起全复位(局态字段清单见 run_scope.gd)
root._debug.reset()
root._begin_session(false)
root.debug_overlay.reset_sliders()
root._start_overlay.visible = true
root.ui.modals.debug_overlay.reset_sliders()
root.ui.screens.start_overlay.visible = true
root._refresh_presentations()
static func _on_inter_next_pressed(root: Variant) -> void:
root._inter_overlay.visible = false
root.ui.screens.inter_overlay.visible = false
if root.run_scope.is_adventure:
# 冒险模式:大奖励屏确认 → 进下一关(AdventureRunner 序列推进——零自动推进的冒险级停点)
if root.run_scope.adventure != null and not root.run_scope.adventure.continue_to_next_level():
@@ -158,7 +158,7 @@ static func _on_inter_next_pressed(root: Variant) -> void:
static func _on_node_continue(root: Variant) -> void:
root._close_modal(root.node_overlay)
root.ui.modals.flow.close(root.ui.modals.node_overlay)
if root.run_scope.runner != null:
# 确认 ≠ 推进——兑付确认后经 choices_presented 弹出选路口
root.run_scope.runner.continue_after_node()
@@ -167,33 +167,33 @@ static func _on_choices_presented(root: Variant, candidates: Array) -> void:
# 零自动推进的停点呈现:整幅节点图 + 图上点击选路(战斗胜利/兑付确认统一到达此处)
if root.run_scope.runner == null:
return
root.log_view.append_bb(root.Narrator.route_line(candidates, root.run_scope.runner))
root.ui.battle.log_view.append_bb(root.Narrator.route_line(candidates, root.run_scope.runner))
# 战斗胜利总结结算(battle.md OQ14):战斗胜利停点先弹 BattleEndOverlay
# 胜利形态(金蜡封+GP 增量+总量+判定行+「下一节点」+明细回看),确认后进选路页。判据 = 战斗节点
# 的选路停点且战斗已收场(battle 收尾分支 _settlement 置 null 先于本信号;入口/兑付/商店停点
# 的当前节点非 battle 型——三态天然分流)。Boss 胜/失败走 level_finished 另路不经此处。
if root.run_scope.is_adventure and root.run_scope.runner.current_battle() == null \
and String(root.run_scope.runner.current_node().get("type", "")) == "battle" and not candidates.is_empty():
root._settlement_stop_pending = true
root.run_scope.settlement_stop_timer.arm()
root.run_scope.pending_candidates = candidates.duplicate()
root._end_overlay.open_victory(root.Narrator.battle_end_text("victory", false), "下一节点",
root.board_view.get_global_rect(),
root.ui.modals.end_overlay.open_victory(root.Narrator.battle_end_text("victory", false), "下一节点",
root.ui.battle.board_zone.board_view.get_global_rect(),
{"gp_summary": "战利品 GP +%d · 合计 %d" % [root.run_scope.last_gain_amount, int(root.run_scope.runner.gp)]})
root._open_modal_after_playback(root._end_overlay)
root.ui.modals.flow.open_after_playback(root.ui.modals.end_overlay)
root._refresh_presentations()
return
_enter_level_screen(root)
root.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
root.level_screen.open_choices(candidates, root.run_scope.runner)
root.level_screen.apply_status(level_map_status_payload(root.run_scope.runner))
root.ui.modals.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
root.ui.modals.level_screen.open_choices(candidates, root.run_scope.runner)
root.ui.modals.level_screen.apply_status(level_map_status_payload(root.run_scope.runner))
root._refresh_presentations()
static func _on_route_chosen(root: Variant, node_id: int) -> void:
# 视图已按节点类型定去留——战斗/Boss 出屏(visible=false,此处收栈
# 记录一致性+回战斗屏);奖励/恢复/商店本屏保持激活(兑付覆盖层由节点事件域内弹出,模态栈
# 打开低层时自动隐藏本屏、关闭自动恢复)
if not root.level_screen.visible:
root._close_modal(root.level_screen)
if not root.ui.modals.level_screen.visible:
root.ui.modals.flow.close(root.ui.modals.level_screen)
_exit_level_screen(root)
if root.run_scope.runner != null and not root.run_scope.runner.pick_next_node(node_id):
push_error("[组合根] 选路被拒(编号 %d 不在候选)" % node_id)
@@ -202,15 +202,15 @@ static func _on_route_chosen(root: Variant, node_id: int) -> void:
static func _enter_level_screen(root: Variant) -> void:
root.run_scope.level_screen_active = true
root._battle_root.visible = false
root._open_modal(root.level_screen)
root.ui.screens.battle_root.visible = false
root.ui.modals.flow.open(root.ui.modals.level_screen)
root._sync_status_entry() # 屏级互斥切换同步(_open_modal 单口不识屏级状态)
static func _exit_level_screen(root: Variant) -> void:
root.run_scope.level_screen_active = false
root._battle_root.visible = true
if root.level_screen.visible:
root._close_modal(root.level_screen)
root.ui.screens.battle_root.visible = true
if root.ui.modals.level_screen.visible:
root.ui.modals.flow.close(root.ui.modals.level_screen)
root._sync_status_entry() # 同上
@@ -251,7 +251,7 @@ static func _on_abandon_requested(root: Variant) -> void:
return
if not root.run_scope.adventure.abandon():
root.logger.log_ux("ux_reject", {"source": "abandon", "sub_reason": "battle_in_flight"})
root.board_zone.toast.show_toast("战斗进行中——无法放弃", "warn")
root.ui.battle.board_zone.toast.show_toast("战斗进行中——无法放弃", "warn")
# ---- 营地覆盖层与钥石账本(写调用唯一归组合根)----
+15 -15
View File
@@ -13,7 +13,7 @@ static func _on_diagnose_pressed(root: Variant) -> void:
if root.run_scope.settlement != null:
ctx["settlement"] = root.run_scope.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
var path: String = root.logger.dump_diagnosis(ctx)
root.board_zone.toast.show_toast("已转储诊断: " + path, "info")
root.ui.battle.board_zone.toast.show_toast("已转储诊断: " + path, "info")
@@ -21,17 +21,17 @@ static func _show_debug_overlay(root: Variant) -> void:
# 门禁:对局中可开;本函数仍是唯一开窗入口,门禁逻辑收函数内:
# 棋盘输入锁/状态角标判据排除浮窗,
# 遮挡语义=ModalStack 登记制(他模态开时暂隐、关后自动恢复),此处零场景限制
if root.debug_overlay == null:
if root.ui.modals.debug_overlay == null:
return
root._open_modal(root.debug_overlay)
root.ui.modals.flow.open(root.ui.modals.debug_overlay)
_sync_debug_overlay(root)
# 调试浮窗角标点击(两屏同款处理器——与状态角标同形制守卫;仍收口 _show_debug_overlay 单口)
static func _on_debug_badge_pressed(root: Variant) -> void:
var map_on: bool = root.level_screen != null and root.level_screen.visible
if (map_on and root._modal_flow.badge_greyed_map()) or (not map_on and root._modal_flow.badge_greyed_battle()):
var map_on: bool = root.ui.modals.level_screen != null and root.ui.modals.level_screen.visible
if (map_on and root.ui.modals.flow.badge_greyed_map()) or (not map_on and root.ui.modals.flow.badge_greyed_battle()):
return
_show_debug_overlay(root)
@@ -40,10 +40,10 @@ static func _on_debug_badge_pressed(root: Variant) -> void:
# 浮窗上下文同步(当局数据刷新+对局中按钮禁用单点):调用面=开窗口与模态开合
# 单口(凡影响开态/对局态的路径必经其一——与 _sync_status_entry 同款纪律)
static func _sync_debug_overlay(root: Variant) -> void:
if root.debug_overlay == null:
if root.ui.modals.debug_overlay == null:
return
root.debug_overlay.set_context_locked(root.run_scope.settlement != null or root.run_scope.is_adventure) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放)
root.debug_overlay.update_run_data(_debug_run_data(root)) # 当局数据只读区块喂入(开窗+模态开合信号驱动)
root.ui.modals.debug_overlay.set_context_locked(root.run_scope.settlement != null or root.run_scope.is_adventure) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放)
root.ui.modals.debug_overlay.update_run_data(_debug_run_data(root)) # 当局数据只读区块喂入(开窗+模态开合信号驱动)
@@ -63,7 +63,7 @@ static func _debug_run_data(root: Variant) -> Dictionary:
static func _close_debug_overlay(root: Variant) -> void:
root._close_modal(root.debug_overlay) # 模态栈单口——开始层全程未隐藏,无需恢复
root.ui.modals.flow.close(root.ui.modals.debug_overlay) # 模态栈单口——开始层全程未隐藏,无需恢复
static func _on_tune_apply(root: Variant, atk_pct: int, hp_pct: int) -> void:
# 加成无持久态可"落"——各战斗开局经 board_resolvers.debug_hero_override/runner 现算生效(应用=关闭浮窗)
@@ -83,11 +83,11 @@ static func _start_debug_levels(root: Variant) -> void:
# settlement/编排态=状态机破坏;模拟在跑时 reset_sliders 会作废测量读数(d5 置灰之外的信号绕过闸)
if root.run_scope.is_adventure or root.run_scope.settlement != null or root._tune_sim.is_running():
return
root.debug_overlay.reset_sliders()
root._close_modal(root.debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay)
root._start_overlay.visible = false
root.log_view.clear()
root.log_view.append_bb(root.Narrator.debug_open_line(root._seed_value(), 1, true))
root.ui.modals.debug_overlay.reset_sliders()
root.ui.modals.flow.close(root.ui.modals.debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay)
root.ui.screens.start_overlay.visible = false
root.ui.battle.log_view.clear()
root.ui.battle.log_view.append_bb(root.Narrator.debug_open_line(root._seed_value(), 1, true))
root._begin_session(false)
if not root._debug.start_debug_levels():
root._fatal_screen("战斗启动失败——见输出日志")
@@ -100,6 +100,6 @@ static func _on_debug_battle_created(root: Variant, settlement_ref: BattleOrches
static func _on_debug_battle_ready(root: Variant, settlement_ref: BattleOrchestrator, _level: int, _carry: int) -> void:
root.run_scope.settlement = settlement_ref
root.board_view.rebuild(root.run_scope.settlement.minesweeper.current_board(), root.run_scope.settlement.minesweeper)
root.ui.battle.board_zone.board_view.rebuild(root.run_scope.settlement.minesweeper.current_board(), root.run_scope.settlement.minesweeper)
root._check_board_hints()
root._refresh_presentations()
+35 -35
View File
@@ -5,12 +5,12 @@
extends RefCounted
static func _open_settings(root: Variant) -> void:
root._start_overlay.visible = false
root._open_modal(root.settings_overlay) # 模态栈单口
root.ui.screens.start_overlay.visible = false
root.ui.modals.flow.open(root.ui.modals.settings_overlay) # 模态栈单口
static func _on_settings_closed(root: Variant) -> void:
root._close_modal(root.settings_overlay) # 模态栈单口
root._start_overlay.visible = true
root.ui.modals.flow.close(root.ui.modals.settings_overlay) # 模态栈单口
root.ui.screens.start_overlay.visible = true
# ---- 局内状态面板·只读(入口角标在两屏装配段;开合与数据喂入归组合根)----
@@ -25,22 +25,22 @@ static func _status_data(root: Variant) -> Dictionary:
return HudPresenter.status_data(root.db, root.meta, root.run_scope.runner, root._treasure_ui)
static func _open_status_panel(root: Variant) -> void:
root._open_modal(root.status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项)
root.status_panel.open_status(_status_data(root))
root.ui.modals.flow.open(root.ui.modals.status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项)
root.ui.modals.status_panel.open_status(_status_data(root))
static func _on_status_closed(root: Variant) -> void:
root._close_modal(root.status_panel) # 模态栈单口
root.ui.modals.flow.close(root.ui.modals.status_panel) # 模态栈单口
static func _open_career(root: Variant) -> void:
root._start_overlay.visible = false
root._open_modal(root.career_overlay) # 模态栈单口
root.career_overlay.open_career(HudPresenter.career_data(root.db, root.save_manager, root.meta))
root.ui.screens.start_overlay.visible = false
root.ui.modals.flow.open(root.ui.modals.career_overlay) # 模态栈单口
root.ui.modals.career_overlay.open_career(HudPresenter.career_data(root.db, root.save_manager, root.meta))
static func _on_career_closed(root: Variant) -> void:
root._close_modal(root.career_overlay) # 模态栈单口
root._start_overlay.visible = true
root.ui.modals.flow.close(root.ui.modals.career_overlay) # 模态栈单口
root.ui.screens.start_overlay.visible = true
# 生涯页快照投影 —— 承接体见 HudPresenter.career_data(开层调用点直接改引)
@@ -52,13 +52,13 @@ static func _on_career_closed(root: Variant) -> void:
# 开层(模态栈单口;与开始层互斥=career 同款先例)。「进入存档」钮归卡② mn3——本函数为
# 该钮的唯一落点(卡③ 批内探针函数直调=中间态,实现报告「已知中间态」登记)。
static func _open_save_select(root: Variant) -> void:
root._start_overlay.visible = false
root._open_modal(root.save_select_overlay) # 模态栈单口
root.save_select_overlay.open_save_select(HudPresenter.save_select_data(root.save_manager))
root.ui.screens.start_overlay.visible = false
root.ui.modals.flow.open(root.ui.modals.save_select_overlay) # 模态栈单口
root.ui.modals.save_select_overlay.open_save_select(HudPresenter.save_select_data(root.save_manager))
static func _on_save_select_closed(root: Variant) -> void:
root._close_modal(root.save_select_overlay) # 模态栈单口
root._start_overlay.visible = true
root.ui.modals.flow.close(root.ui.modals.save_select_overlay) # 模态栈单口
root.ui.screens.start_overlay.visible = true
# 槽数据快照 —— 承接体见 HudPresenter.save_select_data(开层调用点直接改引)
@@ -85,18 +85,18 @@ static func _on_save_select_load(root: Variant, slot: int) -> void:
static func _on_save_select_delete(root: Variant, slot: int) -> void:
root._confirm_consumer = "delete_slot"
root._confirm_consumer_slot = slot
root._open_modal(root.confirm_dialog) # 模态栈单口(显示编排/挂起兜底/遮挡记账——与其余覆盖层同口)
root.confirm_dialog.open("删除存档位 %d?删除后该档不可恢复。" % slot, "删除", "取消")
root.ui.modals.flow.open(root.ui.modals.confirm_dialog) # 模态栈单口(显示编排/挂起兜底/遮挡记账——与其余覆盖层同口)
root.ui.modals.confirm_dialog.open("删除存档位 %d?删除后该档不可恢复。" % slot, "删除", "取消")
# 入口角标点击(两屏同款处理器):P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,
# 探针 pressed.emit 绕过 disabled 时由此兜住;地图屏自身的登记开态不计——has_open_excluding)
static func _on_status_badge_pressed(root: Variant) -> void:
var map_on: bool = root.level_screen != null and root.level_screen.visible
var map_on: bool = root.ui.modals.level_screen != null and root.ui.modals.level_screen.visible
# P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,探针 pressed.emit 绕过 disabled 时由此兜住;
# 地图屏自身的登记开态不计;判据排除调试浮窗:对局中浮窗开着时状态角标仍可点)
if (map_on and root._modal_flow.badge_greyed_map()) or (not map_on and root._modal_flow.badge_greyed_battle()):
if (map_on and root.ui.modals.flow.badge_greyed_map()) or (not map_on and root.ui.modals.flow.badge_greyed_battle()):
return
_open_status_panel(root)
@@ -106,17 +106,17 @@ static func _on_status_badge_pressed(root: Variant) -> void:
# 地图屏角标=随屏;置灰=登记册上除地图屏自身外任一层开着(防「看得见点不着」态)。
# 调用面=模态开合单口与屏级互斥口(凡影响开态/屏态的路径必经其一)。
static func _sync_status_entry(root: Variant) -> void:
if root.status_panel == null or root._modals == null or root.board_zone == null or root.level_screen == null:
if root.ui.modals.status_panel == null or root.ui.modals.flow == null or root.ui.battle.board_zone == null or root.ui.modals.level_screen == null:
return
var map_on: bool = root.level_screen.visible
root.board_zone.status_badge.visible = root.run_scope.level_active and not map_on
root.level_screen.status_badge.visible = map_on
var map_on: bool = root.ui.modals.level_screen.visible
root.ui.battle.board_zone.status_badge.visible = root.run_scope.level_active and not map_on
root.ui.modals.level_screen.status_badge.visible = map_on
# 置灰判据为排除式(调试浮窗开态不计入:对局中浮窗开着时角标仍可点)
root.board_zone.status_badge.disabled = root._modal_flow.badge_greyed_battle() # 互斥域谓词
root.level_screen.status_badge.disabled = root._modal_flow.badge_greyed_map()
root.ui.battle.board_zone.status_badge.disabled = root.ui.modals.flow.badge_greyed_battle() # 互斥域谓词
root.ui.modals.level_screen.status_badge.disabled = root.ui.modals.flow.badge_greyed_map()
# 调试浮层角标(全局浮层任何 UI 不遮挡——可见性不随场景变,只抬 offset
# 避让:地图屏底栏 85px 常驻,角标抬至底栏上方 16px;其余场景右下角 16px 贴底)
root.debug_badge.offset_bottom = -101.0 if map_on else -16.0
root.ui.screens.debug_badge.offset_bottom = -101.0 if map_on else -16.0
@@ -131,19 +131,19 @@ static func _on_wallet_changed(root: Variant, event: String, gp: int, extra: Dic
root.logger.log_wallet(event, gp, extra)
if String(event) == "gain":
root.run_scope.last_gain_amount = int(extra.get("amount", 0)) # GP 增量取数(每战恰一笔 gain)
if root.shop_overlay.visible and root.run_scope.runner != null:
if root.run_scope.runner.is_shop_pool():
root.shop_overlay.refresh_shelf_state(int(root.run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变)
if root.ui.modals.shop_overlay.visible and root.run_scope.runner != null:
if root.run_scope.runner.shop.is_shop_pool():
root.ui.modals.shop_overlay.refresh_shelf_state(int(root.run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变)
else:
root.shop_overlay.refresh_state(root.run_scope.runner.shop_products(), int(root.run_scope.runner.gp))
root.ui.modals.shop_overlay.refresh_state(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp))
root._refresh_presentations()
static func _on_legend_toggled(root: Variant) -> void:
if root.legend_overlay.is_open():
root._close_modal(root.legend_overlay)
if root.ui.modals.legend_overlay.is_open():
root.ui.modals.flow.close(root.ui.modals.legend_overlay)
else:
root._open_modal(root.legend_overlay)
root.ui.modals.flow.open(root.ui.modals.legend_overlay)
+116 -121
View File
@@ -1,7 +1,7 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 组合根 UI 装配分步承接件:三屏壳实例化、视图句柄接线、覆盖层装配与模态编排注入按步拆开。
## 本件是 main._build_ui 的逐字分步搬运(成员访问改 root 前缀)——main 侧留同名薄委托,装配序与 z 序零改动。
## 层序唯一事实源仍是 main.LAYER_CONTRACT + _apply_layer_contract(本件不持有层序逻辑)。
## 层序唯一事实源=root.ui.LAYER_CONTRACT + root.ui.apply_layer_contract(ui_registry.gd——本件不持有层序逻辑)。
extends RefCounted
const UiAssembler := preload("res://presentation/ui_assembler.gd")
@@ -35,50 +35,48 @@ static func _build_screens(root: Variant) -> void:
# 屏内 level_screen 与 battle_root 无同时可见态(屏级互斥+模态栈遮挡),层内先后零视觉差;
# 视觉共存对逐对验算:legend/battle(图例战斗中开——legend 在冒险屏壳之后=其上 ✓)、
# debug/level_screen(浮窗最后子=其上 ✓)、shop/level_screen(商店节点域弹出——模态均在屏壳上 ✓)。
root._adventure_screen = AdventureScreenScene.instantiate()
root.add_child(root._adventure_screen)
root._menu_screen = MenuScreenScene.instantiate()
root.add_child(root._menu_screen)
root._start_overlay = root._menu_screen.start_overlay
root._battle_root = root._adventure_screen.battle_root
root.stage = root._adventure_screen.stage
root.log_view = root._adventure_screen.log_view
root.info_panel = root._adventure_screen.info_panel
root.board_zone = root._adventure_screen.board_zone
root.board_view = root._adventure_screen.board_view # 探针兼容别名(= board_zone.board_view)
root.ap_bar = root._adventure_screen.ap_bar # 探针兼容别名(= board_zone.ap_bar)
root.settlement_fx = root._adventure_screen.settlement_fx
root.level_screen = root._adventure_screen.level_screen
root.ui.screens.adventure_screen = AdventureScreenScene.instantiate()
root.add_child(root.ui.screens.adventure_screen)
root.ui.screens.menu_screen = MenuScreenScene.instantiate()
root.add_child(root.ui.screens.menu_screen)
root.ui.screens.start_overlay = root.ui.screens.menu_screen.start_overlay
root.ui.screens.battle_root = root.ui.screens.adventure_screen.battle_root
root.ui.battle.stage = root.ui.screens.adventure_screen.stage
root.ui.battle.log_view = root.ui.screens.adventure_screen.log_view
root.ui.battle.info_panel = root.ui.screens.adventure_screen.info_panel
root.ui.battle.board_zone = root.ui.screens.adventure_screen.board_zone
root.ui.battle.settlement_fx = root.ui.screens.adventure_screen.settlement_fx
root.ui.modals.level_screen = root.ui.screens.adventure_screen.level_screen
static func _build_battle_wiring(root: Variant) -> void:
# 结算行排播泵(本体在 row_pacer.gd;本类注入视图句柄与呈现刷新回调,零信号连接)
root._row_pacer = RowPacer.new()
root._row_pacer.log_view = root.log_view
root._row_pacer.stage = root.stage
root._row_pacer.board_zone = root.board_zone
root._row_pacer.settlement_fx = root.settlement_fx
root._row_pacer.board_view = root.board_view
root._row_pacer.refresh_request = root._refresh_presentations
root._row_pacer.pump_ended = root._on_pump_ended # 演播完边沿统一派发(挂起 modal 优先,其次锁期点击重放)
root.add_child(root._row_pacer)
root.ui.screens.row_pacer = RowPacer.new()
root.ui.screens.row_pacer.log_view = root.ui.battle.log_view
root.ui.screens.row_pacer.stage = root.ui.battle.stage
root.ui.screens.row_pacer.board_zone = root.ui.battle.board_zone
root.ui.screens.row_pacer.settlement_fx = root.ui.battle.settlement_fx
root.ui.screens.row_pacer.board_view = root.ui.battle.board_zone.board_view
root.ui.screens.row_pacer.refresh_request = root._refresh_presentations
root.ui.screens.row_pacer.pump_ended = root.ui.modals.flow.pump_ended # 演播完边沿统一派发(挂起 modal 优先,其次锁期点击重放)
root.add_child(root.ui.screens.row_pacer)
# 秘宝两步交互(本体在 treasure_interact_controller.gd;settlement/runner/grant 随关卡/战斗更替,
# 经现值回调注入——仿 identity_resolver 只读回调先例;grant 每战新建编排器,现值取件防悬挂)
root._treasure_ui = TreasureInteractController.new({
"db": root.db, "meta": root.meta, "info_panel": root.info_panel, "toast": root.board_zone.toast,
"board_view": root.board_view,
"db": root.db, "meta": root.meta, "info_panel": root.ui.battle.info_panel, "toast": root.ui.battle.board_zone.toast,
"board_view": root.ui.battle.board_zone.board_view,
"settlement_get": func() -> Variant: return root.run_scope.settlement,
"runner_get": func() -> Variant: return root.run_scope.runner,
"grant_get": func() -> Variant: return root.run_scope.settlement.reward_grant() if root.run_scope.settlement != null else null,
})
root.board_zone.action_requested.connect(root._on_board_action)
root.board_zone.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(战斗屏 HUD 角标)
root.info_panel.treasure_slot_clicked.connect(root._treasure_ui.on_slot_clicked) # 两步交互第一步
root.board_zone.stop_pressed.connect(root._on_stop_pressed)
root.board_zone.refresh_confirmed.connect(root._on_refresh_confirmed)
root.board_zone.legend_toggled.connect(root._on_legend_toggled)
root.board_zone.hover_forbidden.connect(root._on_hover_forbidden)
root.board_zone.chord_attempted.connect(root._on_chord_attempted)
root.ui.battle.board_zone.action_requested.connect(root._on_board_action)
root.ui.battle.board_zone.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(战斗屏 HUD 角标)
root.ui.battle.info_panel.treasure_slot_clicked.connect(root._treasure_ui.on_slot_clicked) # 两步交互第一步
root.ui.battle.board_zone.stop_pressed.connect(root._on_stop_pressed)
root.ui.battle.board_zone.refresh_confirmed.connect(root._on_refresh_confirmed)
root.ui.battle.board_zone.legend_toggled.connect(root._on_legend_toggled)
root.ui.battle.board_zone.hover_forbidden.connect(root._on_hover_forbidden)
root.ui.battle.board_zone.chord_attempted.connect(root._on_chord_attempted)
root.logger = PlaytestLogger.new()
root.logger.ap_per_round = int(root.db.run.action_economy.ap_per_round)
@@ -88,136 +86,135 @@ static func _build_overlays(root: Variant) -> void:
# < 商店 < 营地 < 设置 < 状态面板(只读) < 生涯页(只读) < 确认弹窗 < 调试浮窗;
# 关卡画面住冒险屏壳(树序在模态之下)——登记序仍夹在节点层与三选一层之间,遮挡语义零改动)
var overlays: Dictionary[Variant, Variant] = UiAssembler.build_overlays(root, root.db)
root.legend_overlay = overlays.legend_overlay
root.node_overlay = overlays.node_overlay
root._end_overlay = overlays.end_overlay
root.shop_overlay = overlays.shop_overlay
root._camp_screen = CampScreenScene.instantiate()
root.add_child(root._camp_screen)
root.camp_overlay = root._camp_screen.camp_overlay
root.ui.modals.legend_overlay = overlays.legend_overlay
root.ui.modals.node_overlay = overlays.node_overlay
root.ui.modals.end_overlay = overlays.end_overlay
root.ui.modals.shop_overlay = overlays.shop_overlay
root.ui.screens.camp_screen = CampScreenScene.instantiate()
root.add_child(root.ui.screens.camp_screen)
root.ui.modals.camp_overlay = root.ui.screens.camp_screen.camp_overlay
# 营地屏壳插位(夹商店层与设置层之间)由 LAYER_CONTRACT 营地槽统一落位——禁散落 move_child
root.legend_overlay.closed.connect(root._on_modal_closed.bind(root.legend_overlay))
root.node_overlay.continue_pressed.connect(root._on_node_continue)
root.level_screen.route_chosen.connect(root._on_route_chosen)
root.level_screen.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(地图屏顶栏角标)
root.level_screen.abandon_requested.connect(root._on_abandon_requested) # 放弃入口
root._end_overlay.continue_pressed.connect(root._on_battle_end_continue)
root.ui.modals.legend_overlay.closed.connect(root.ui.modals.flow.close.bind(root.ui.modals.legend_overlay))
root.ui.modals.node_overlay.continue_pressed.connect(root._on_node_continue)
root.ui.modals.level_screen.route_chosen.connect(root._on_route_chosen)
root.ui.modals.level_screen.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(地图屏顶栏角标)
root.ui.modals.level_screen.abandon_requested.connect(root._on_abandon_requested) # 放弃入口
root.ui.modals.end_overlay.continue_pressed.connect(root._on_battle_end_continue)
static func _build_aux_panels(root: Variant) -> void:
root.settings_overlay = SettingsOverlay.new(root.settings_store) # 最小设置面(音量/全屏/减动效)
root.add_child(root.settings_overlay)
root.settings_overlay.closed.connect(root._on_settings_closed)
root.ui.modals.settings_overlay = SettingsOverlay.new(root.settings_store) # 最小设置面(音量/全屏/减动效)
root.add_child(root.ui.modals.settings_overlay)
root.ui.modals.settings_overlay.closed.connect(root._on_settings_closed)
# 局内状态面板·只读(登记设置层同档带 dim 遮罩,确认弹窗之前;
# 数据=组合根开层实时读 MetaProgression 访问器喂入,本层零写通路)
root.status_panel = StatusPanel.new()
root.add_child(root.status_panel)
root.status_panel.closed.connect(root._on_status_closed)
root.ui.modals.status_panel = StatusPanel.new()
root.add_child(root.ui.modals.status_panel)
root.ui.modals.status_panel.closed.connect(root._on_status_closed)
# 生涯页·只读全屏页(登记序=营地同档,设置层旁、确认弹窗之前;
# 树序同步=addChild 同位(登记序=树序=z 序纪律);数据=组合根开层实时读 SaveManager 计数
# 访问器+MetaProgression 访问器喂入,本层零写通路零跳转;入口=开始层「营地与系统」组生涯钮)
root.career_overlay = CareerOverlay.new()
root.add_child(root.career_overlay)
root.career_overlay.closed.connect(root._on_career_closed)
root.ui.modals.career_overlay = CareerOverlay.new()
root.add_child(root.ui.modals.career_overlay)
root.ui.modals.career_overlay.closed.connect(root._on_career_closed)
# 选档界面(营地交互重做卡③):登记序=生涯同档(树序同步=addChild 同位);三动作只发意图信号
#(激活/创建/删除+确认分派归组合根——视图零数据层依赖);入口=「进入存档」钮(卡② mn3 接线)。
root.save_select_overlay = SaveSelectOverlay.new()
root.add_child(root.save_select_overlay)
root.save_select_overlay.closed.connect(root._on_save_select_closed)
root.save_select_overlay.new_slot_requested.connect(root._on_save_select_new)
root.save_select_overlay.load_slot_requested.connect(root._on_save_select_load)
root.save_select_overlay.delete_slot_requested.connect(root._on_save_select_delete)
root.ui.modals.save_select_overlay = SaveSelectOverlay.new()
root.add_child(root.ui.modals.save_select_overlay)
root.ui.modals.save_select_overlay.closed.connect(root._on_save_select_closed)
root.ui.modals.save_select_overlay.new_slot_requested.connect(root._on_save_select_new)
root.ui.modals.save_select_overlay.load_slot_requested.connect(root._on_save_select_load)
root.ui.modals.save_select_overlay.delete_slot_requested.connect(root._on_save_select_delete)
static func _build_curio(root: Variant) -> void:
# 提升三选一覆盖层(z 序 = 节点层之上、终局层之下;确认后同拍推进;
# 插位由 LAYER_CONTRACT curio 槽统一落位——禁散落 move_child)
root.curio_overlay = CurioPickOverlay.new()
root.add_child(root.curio_overlay)
root.ui.modals.curio_overlay = CurioPickOverlay.new()
root.add_child(root.ui.modals.curio_overlay)
# 提升三选一与替换子交互(本体在 curio_interact_controller.gd;本类只接线与回调注入,
# 模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调)
root._bonus = CurioInteractController.new(CurioFlowContext.new(root.db,
{"curio_overlay": root.curio_overlay, "end_overlay": root._end_overlay,
"board_view": root.board_view, "toast": root.board_zone.toast},
{"curio_overlay": root.ui.modals.curio_overlay, "end_overlay": root.ui.modals.end_overlay,
"board_view": root.ui.battle.board_zone.board_view, "toast": root.ui.battle.board_zone.toast},
{"meta_get": func() -> Variant: return root.meta, "save_get": func() -> Variant: return root.save_manager,
"adventure_get": func() -> Variant: return root.run_scope.adventure,
"runner_get": func() -> Variant: return root.run_scope.runner,
"adventure_mode_get": func() -> Variant: return root.run_scope.is_adventure},
{"open_modal": root._open_modal, "open_modal_after_playback": root._open_modal_after_playback,
"close_modal": root._close_modal, "refresh_presentations": root._refresh_presentations,
{"open_modal": root.ui.modals.flow.open, "open_modal_after_playback": root.ui.modals.flow.open_after_playback,
"close_modal": root.ui.modals.flow.close, "refresh_presentations": root._refresh_presentations,
"fatal_screen": root._fatal_screen,
"gp_summary": _gp_summary_cb(root)})) # 构造参数收拢 context(u20 三束)
root.curio_overlay.pick_confirmed.connect(root._bonus.on_pick_confirmed)
root.curio_overlay.continue_pressed.connect(root._bonus.on_continue)
root.curio_overlay.refresh_requested.connect(root._bonus.on_refresh)
root.ui.modals.curio_overlay.pick_confirmed.connect(root._bonus.on_pick_confirmed)
root.ui.modals.curio_overlay.continue_pressed.connect(root._bonus.on_continue)
root.ui.modals.curio_overlay.refresh_requested.connect(root._bonus.on_refresh)
static func _build_menu_wiring(root: Variant) -> void:
# 菜单屏接线(句柄经屏壳暴露;三入口化——进入存档/设置/退出游戏,卡② mn3:
# 进入存档钮落点=卡③选档层 _open_save_select,点击链在此闭环)
root._menu_screen.entersavebtn.pressed.connect(root._open_save_select)
root.ui.screens.menu_screen.entersavebtn.pressed.connect(root._open_save_select)
# 调试入口=UI 最顶层全局浮层角标(任何 UI 不遮挡——菜单/战斗/地图三枚
# 屏内角标由全局浮层承载;连接归组合根,树序/登记面见 LAYER_CONTRACT 末位)
root.debug_badge = UiAssembler.build_debug_badge(root)
root.add_child(root.debug_badge)
root.debug_badge.pressed.connect(root._on_debug_badge_pressed)
root.ui.screens.debug_badge = UiAssembler.build_debug_badge(root)
root.add_child(root.ui.screens.debug_badge)
root.ui.screens.debug_badge.pressed.connect(root._on_debug_badge_pressed)
# 营地/商店投影(本体在 camp_shop_presenter.gd;本类只接线、探针缝保缝转发;
# 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入)
root._camp_pres = CampShopPresenter.new(CampFlowContext.new(root.db,
{"camp_overlay": root.camp_overlay, "shop_overlay": root.shop_overlay,
"toast": root.board_zone.toast, "start_overlay": root._start_overlay, "logger": root.logger},
{"camp_overlay": root.ui.modals.camp_overlay, "shop_overlay": root.ui.modals.shop_overlay,
"toast": root.ui.battle.board_zone.toast, "start_overlay": root.ui.screens.start_overlay, "logger": root.logger},
{"meta_get": func() -> Variant: return root.meta, "save_get": func() -> Variant: return root.save_manager,
"runner_get": func() -> Variant: return root.run_scope.runner},
{"open_modal": root._open_modal, "close_modal": root._close_modal,
{"open_modal": root.ui.modals.flow.open, "close_modal": root.ui.modals.flow.close,
"refresh_presentations": root._refresh_presentations})) # 构造参数收拢 context(u21 三束)
root.shop_overlay.buy_requested.connect(root._camp_pres.on_shop_buy)
root.shop_overlay.refresh_requested.connect(root._camp_pres.on_shop_refresh) # MC-1
root.shop_overlay.leave_pressed.connect(root._camp_pres.on_shop_leave)
root.camp_overlay.buy_requested.connect(root._camp_pres.on_camp_buy)
root.camp_overlay.attune_toggled.connect(root._camp_pres.on_camp_attune_toggle)
root.camp_overlay.tree_buy_requested.connect(root._camp_pres.on_camp_tree_buy)
root.camp_overlay.tree_fork_toggled.connect(root._camp_pres.on_camp_fork_toggle)
root.camp_overlay.forge_buy_requested.connect(root._camp_pres.on_camp_forge_buy) # 锻造三意图(写口唯一归 MetaProgression)
root.camp_overlay.forge_unlock_requested.connect(root._camp_pres.on_camp_forge_unlock)
root.camp_overlay.forge_select_requested.connect(root._camp_pres.on_camp_forge_select)
root.camp_overlay.closed.connect(root._camp_pres.on_camp_closed)
root._menu_screen.settingsbtn.pressed.connect(root._open_settings) # 最小设置面入口
root._menu_screen.quitbtn.pressed.connect(root._quit_game) # 退出游戏提级一级菜单
root.ui.modals.shop_overlay.buy_requested.connect(root._camp_pres.on_shop_buy)
root.ui.modals.shop_overlay.refresh_requested.connect(root._camp_pres.on_shop_refresh) # MC-1
root.ui.modals.shop_overlay.leave_pressed.connect(root._camp_pres.on_shop_leave)
root.ui.modals.camp_overlay.buy_requested.connect(root._camp_pres.on_camp_buy)
root.ui.modals.camp_overlay.attune_toggled.connect(root._camp_pres.on_camp_attune_toggle)
root.ui.modals.camp_overlay.tree_buy_requested.connect(root._camp_pres.on_camp_tree_buy)
root.ui.modals.camp_overlay.tree_fork_toggled.connect(root._camp_pres.on_camp_fork_toggle)
root.ui.modals.camp_overlay.forge_buy_requested.connect(root._camp_pres.on_camp_forge_buy) # 锻造三意图(写口唯一归 MetaProgression)
root.ui.modals.camp_overlay.forge_unlock_requested.connect(root._camp_pres.on_camp_forge_unlock)
root.ui.modals.camp_overlay.forge_select_requested.connect(root._camp_pres.on_camp_forge_select)
root.ui.modals.camp_overlay.closed.connect(root._camp_pres.on_camp_closed)
root.ui.screens.menu_screen.settingsbtn.pressed.connect(root._open_settings) # 最小设置面入口
root.ui.screens.menu_screen.quitbtn.pressed.connect(root._quit_game) # 退出游戏提级一级菜单
static func _build_debug_and_resolvers(root: Variant) -> void:
var inter: Dictionary[Variant, Variant] = UiAssembler.build_inter_overlay(root)
root._inter_overlay = inter.overlay
root._inter_label = inter.label
root.ui.screens.inter_overlay = inter.overlay
root.ui.screens.inter_label = inter.label
inter.next_btn.pressed.connect(root._on_inter_next_pressed)
# 确认弹窗(新冒险互斥确认弹窗化):树序落在开始层/过场层之后 = 子级最末
#(纪律「登记序 = 树序 = z 序」——confirm_dialog.open 禁运行时 move_child 自提抬层;
# 弹窗只在开始菜单域弹出,绘制序恒最上;
# 诊断转储入口在调试浮窗)
root.confirm_dialog = ConfirmDialog.new()
root.add_child(root.confirm_dialog)
root.ui.modals.confirm_dialog = ConfirmDialog.new()
root.add_child(root.ui.modals.confirm_dialog)
# 消费方编排(卡③ 契约 3/sl8——禁双常驻静态接线):open 前设 _confirm_consumer,
# confirmed/cancelled 后按该状态量分派;分派面可容纳后续消费方(卡④ 到位直接接入不返工)
root.confirm_dialog.confirmed.connect(root._on_confirm_accepted)
root.confirm_dialog.cancelled.connect(root._on_confirm_declined)
root.ui.modals.confirm_dialog.confirmed.connect(root._on_confirm_accepted)
root.ui.modals.confirm_dialog.cancelled.connect(root._on_confirm_declined)
# 调试浮窗(主菜单三散件收一:随机种子/数值调整/批量模拟/两层调试入口;
# 全局可开浮窗:调试会话恒开=启动即开〔_ready〕+两屏角标再开+当局数据只读区块;
# 诊断转储入口在本浮窗,玩家画面零常驻调试件)。
# 树序在开始层与确认弹窗之后 = 浮于菜单之上的 Dialog(不隐藏开始层——与确认弹窗同款形态);
# 开窗唯一入口 = _show_debug_overlay(C6 单口——角标/探针/启动路径全收口)
root.debug_overlay = DebugOverlay.new(root.db, int(root.db.combat.hero.atk), int(root.db.combat.hero.max_hp))
root.add_child(root.debug_overlay)
root._seed_edit = root.debug_overlay.seed_edit # 种子读取面在浮窗(_seed_value 唯一消费点不变)
root.debug_overlay.closed.connect(root._on_modal_closed.bind(root.debug_overlay))
root.debug_overlay.apply_requested.connect(root._on_tune_apply)
root.debug_overlay.simulate_requested.connect(root._on_tune_simulate)
root.debug_overlay.debug_levels_requested.connect(root._start_debug_levels)
root.ui.modals.debug_overlay = DebugOverlay.new(root.db, int(root.db.combat.hero.atk), int(root.db.combat.hero.max_hp))
root.add_child(root.ui.modals.debug_overlay)
root.ui.modals.debug_overlay.closed.connect(root.ui.modals.flow.close.bind(root.ui.modals.debug_overlay))
root.ui.modals.debug_overlay.apply_requested.connect(root._on_tune_apply)
root.ui.modals.debug_overlay.simulate_requested.connect(root._on_tune_simulate)
root.ui.modals.debug_overlay.debug_levels_requested.connect(root._start_debug_levels)
# 棋盘解析回调族(有态绑定件——board_resolvers.gd 承接;组合根只接线;局部持有不占类成员位)
var resolvers = BoardResolversScript.new(root.db, root.debug_overlay, func() -> Variant: return root.run_scope.settlement)
var resolvers = BoardResolversScript.new(root.db, root.ui.modals.debug_overlay, func() -> Variant: return root.run_scope.settlement)
# 奖励徽章数据源:地图战斗资源 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效)
root.board_view.set_identity_resolver(resolvers.identity_lookup)
root.ui.battle.board_zone.board_view.set_identity_resolver(resolvers.identity_lookup)
# 数字遮蔽数据源:地形变异 hide_number 解析只读回调(信息型呈现——board_view 渲染面读取)
root.board_view.set_variant_mask_resolver(resolvers.variant_mask_lookup)
root.board_view.set_variant_mark_resolver(resolvers.variant_mark_lookup) # flat_damage 未开格角标
root.ui.battle.board_zone.board_view.set_variant_mask_resolver(resolvers.variant_mask_lookup)
root.ui.battle.board_zone.board_view.set_variant_mark_resolver(resolvers.variant_mark_lookup) # flat_damage 未开格角标
# 调试会话(英雄生效值预览回调绑同一解析件——创建晚于浮窗、随 _build_ui 一次接线)
root._debug = DebugSessionScript.new(root.db, root.get_node(root.AUTOLOAD_PATH_RNG),
func() -> Dictionary: return resolvers.debug_hero_override(), root._seed_value)
@@ -228,24 +225,22 @@ static func _build_modal_flow(root: Variant) -> void:
# 模态登记(缺陷⑤纪律——登记序 = 树序 = z 序):层序唯一事实源=LAYER_CONTRACT,
# _apply_layer_contract 统一落位(关卡画面宿住冒险屏壳、营地屏壳夹商店与设置层之间、
# 三选一对位终局层之下等特例均在合同数据内表达,禁散落 move_child)
# 模态编排注入(ModalFlow 纯逻辑类:依赖仅 ModalStack+查询 Callable)
root._modal_flow = ModalFlow.new()
root._modal_flow.start_visible_get = func() -> bool: return root._start_overlay.visible
root._modal_flow.inter_visible_get = func() -> bool: return root._inter_overlay.visible
root._modal_flow.settlement_get = func() -> Variant: return root.run_scope.settlement
root._modal_flow.rows_empty_get = func() -> bool: return root._row_pacer.pending_rows().is_empty()
root._modal_flow.flush_rows = root._flush_row_queue
root._modal_flow.on_open_sync = func() -> void:
# 模态编排注入(ModalFlow 纯逻辑类:依赖仅 ModalStack+查询 Callable——实例随 ui.modals 容器构造建立)
root.ui.modals.flow.start_visible_get = func() -> bool: return root.ui.screens.start_overlay.visible
root.ui.modals.flow.inter_visible_get = func() -> bool: return root.ui.screens.inter_overlay.visible
root.ui.modals.flow.settlement_get = func() -> Variant: return root.run_scope.settlement
root.ui.modals.flow.rows_empty_get = func() -> bool: return root.ui.screens.row_pacer.pending_rows().is_empty()
root.ui.modals.flow.flush_rows = root.ui.screens.row_pacer.flush
root.ui.modals.flow.on_open_sync = func() -> void:
root._sync_status_entry() # 状态面板入口角标可见性/置灰随开态同步
root._sync_debug_overlay() # 浮窗当局数据/按钮态随开态同步
root._modal_flow.on_close_sync = func() -> void:
root.ui.modals.flow.on_close_sync = func() -> void:
root._sync_status_entry() # 关闭恢复角标可点态(无残留——同源口径)
root._sync_debug_overlay() # 他层关闭复活浮窗时同步刷新(关后自动恢复语义配套)
root._modal_flow.replay_click = root._replay_locked_click
root._modals = root._modal_flow.stack # 探针 _list 登记序断言缝(ui_struct 登记序=z 序纪律断言)
root.ui.modals.flow.replay_click = root._replay_locked_click
# 视觉层合同装配(唯一事实源=LAYER_CONTRACT:树序+登记序同源派生——改序只改合同表)
root._apply_layer_contract()
root._tune_sim = TuneSimController.new(root.db, root.debug_overlay, int(root.db.combat.hero.max_hp))
root.ui.apply_layer_contract(root)
root._tune_sim = TuneSimController.new(root.db, root.ui.modals.debug_overlay, int(root.db.combat.hero.max_hp))
static func fatal_screen(root: Variant, msg: String) -> void:
var l := UiKit.label(msg,UiKit.FS_SUB, true, UiKit.RED_ALERT)
+44 -6
View File
@@ -2,16 +2,28 @@
## 调整板批量模拟控制器:逐次驱动关卡自动策略(与 headless 扫描同口径),进度/报告经面板 setter 注入。
## 零核心写——表基线复位由组合根在移交前完成,加成施加只走自动策略内部 runner.set_hero_override 单路径。
## presentation 不 preload smoke:驱动实例由调用方注入(须供与 level_autoplay 同款 run_level 签名),缺省 null=运行时 load 兜底。
## 逐帧步进:每局恰占一帧——第 0 局在调用点同步段执行,第 i 局(i≥1)在第 i 帧信号发射点执行,收尾恒在第 n 帧发射点;
## n=0 退化路径为调用点同步收尾零帧。
## _batch_finished 信号契约:整批收尾放行——发射时点=报告注入与运行态复位之后;消费方=run() 自身收尾放行,
## 外部 await run(n) 经此挂起至整批完成(协程身份契约)。
extends RefCounted
const SEED_BASE := 4200 # 模拟种子基(与历史实现同源——同种子可回放)
const PlaytestLogger := preload("res://presentation/playtest_logger.gd") # 试玩日志(同层依赖——运行时 load 收敛为 preload 常量)
signal _batch_finished
var db
var panel # 调试浮窗 setter 面:atk_pct/hp_pct/set_sim_running/set_sim_progress/set_sim_report
var _base_hp := 0 # 报告中的"基础 HP"口径注记值
var _driver = null # 关卡自动策略驱动(调用方注入;null = 旧行为运行时 load 兜底)
var _running := false # 运行态(run 全程 true——组合根互查守卫用,与面板置灰解耦)
var _sim_n := 0 # 本批局数(run 期活成员——批末失效、下次 run 重写)
var _sim_i := 0 # 已执行局数(步进计数)
var _sim_stats: Dictionary[Variant, Variant] = {} # stats 六键束(run 期承载)
var _sim_autoplay = null # 自动策略驱动实例(run 期承载)
var _sim_logger = null # 共享 JSONL 日志会话(run 期承载)
var _finished := false # 批完成旗标(_finish_batch 置位——收尾段同步承载)
func _init(db_ref, panel_ref, base_hp: int, driver = null) -> void:
@@ -44,12 +56,38 @@ func run(n: int) -> void:
panel.set_sim_progress("模拟中:0/%d …" % n)
var stats := {"wins": 0, "dmg_all": [], "hp_left_win": [], "hp_boss": [], "rounds_all": [], "death_nodes": {}}
var autoplay = _autoplay()
for i in range(n):
_run_one_level(autoplay, stats, i, panel, logger)
panel.set_sim_progress("模拟中:%d/%d …" % [i + 1, n])
await panel.get_tree().process_frame
logger.close_session()
panel.set_sim_report(HudPresenter.tune_sim_report(n, panel.atk_pct(), panel.hp_pct(), stats, _base_hp))
_sim_n = n
_sim_i = 0
_sim_stats = stats
_sim_autoplay = autoplay
_sim_logger = logger
_finished = false
if n > 0:
if not panel.get_tree().process_frame.is_connected(_step_frame):
panel.get_tree().process_frame.connect(_step_frame) # 恰好连接一次(迭代 39 先例同款守卫)
_step_frame() # 首步同步执行(第 0 局在调用栈内跑完——与 await 形态对齐)
await _batch_finished
else:
_finish_batch() # n=0:循环零进同步收尾,不挂起(与 await 形态一致零帧)
# 逐帧步进回调:i<n 执行一局+进度串+计数推进(每局恰占一帧);i 达 n 次帧断连→收尾→放行
func _step_frame() -> void:
if _sim_i < _sim_n:
_run_one_level(_sim_autoplay, _sim_stats, _sim_i, panel, _sim_logger)
panel.set_sim_progress("模拟中:%d/%d …" % [_sim_i + 1, _sim_n])
_sim_i += 1
else:
panel.get_tree().process_frame.disconnect(_step_frame) # 先断连防再入
_finish_batch()
_batch_finished.emit()
# 整批收尾单点(收尾四语句同帧同相位:close_session→报告→运行态复位——次序与 run() 尾现行形态逐句对齐)
func _finish_batch() -> void:
_finished = true
_sim_logger.close_session()
panel.set_sim_report(HudPresenter.tune_sim_report(_sim_n, panel.atk_pct(), panel.hp_pct(), _sim_stats, _base_hp))
panel.set_sim_running(false)
_running = false
+10
View File
@@ -0,0 +1,10 @@
## 战区视图域句柄册:战斗屏 A–D 区视图与结算演出覆盖层的注册面域容器。
## 纯字段无方法;RefCounted 不挂树——节点本体树位与 add_child 归属由组合根与装配承接件负责。
class_name UiBattle
extends RefCounted
var stage: BattleStageView # B 区(含 log_view 明细带)
var log_view: BattleLogView
var info_panel: HeroInfoPanelView # C 区(攻击/AC 数据框+两预留区)
var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx)
var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下)
+1
View File
@@ -0,0 +1 @@
uid://d35j5hry1f40v
+32
View File
@@ -0,0 +1,32 @@
## 模态层域句柄册:十三个模态覆盖层句柄+模态栈编排(ModalFlow——登记/开合/输入锁谓词)的注册面域容器。
## RefCounted 不挂树;模态登记唯一口=register_layer(anchors 撑满宿主+排入 ModalStack)。
## flow 随容器构造即建(装配早期注入点经 root.ui.modals.flow.* 取 Callable——晚于 flow 建立时点)。
class_name UiModals
extends RefCounted
var flow: ModalFlow
var legend_overlay: LegendOverlay
var node_overlay: NodeOverlay
var level_screen: LevelMapScreen
var curio_overlay: CurioPickOverlay
var end_overlay: BattleEndOverlay
var shop_overlay: ShopOverlay
var camp_overlay: CampOverlay
var settings_overlay: SettingsOverlay
var status_panel: StatusPanel
var career_overlay: CareerOverlay
var save_select_overlay: SaveSelectOverlay
var confirm_dialog: ConfirmDialog
var debug_overlay: DebugOverlay
func _init() -> void:
flow = ModalFlow.new() # 查询/钩子 Callable 由 _build_modal_flow 注入(注入时点不变——注入值面向已在座实例)
func register_layer(m: Control, tags: Array = []) -> void:
# 撑满宿主(Control 默认锚 = 0×0——全屏变暗层与居中布局整体失效;route 选路层预览截图实证后
# 统一收口:六个模态同病,探针经信号直发绕过真实点击从未暴露。anchors_and_offsets 而非 anchors
# ——后者会把控件重置回最小内容尺寸,ui_assembler 头注同款纪律)
m.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
flow.register(m, tags) # 排除组随层登记(non_locking=调试浮窗/screen_state=关卡地图屏)
+1
View File
@@ -0,0 +1 @@
uid://dbs4qveqvr5hg
+101
View File
@@ -0,0 +1,101 @@
## 界面注册面:呈现层界面元素清单的唯一事实源——哪些层、什么序、地址在哪。
## 持有三域容器(screens/battle/modals)、LAYER_CONTRACT 合同表、node(key) 合同寻址、
## apply_layer_contract(root) 唯一落位口(全仓唯一 move_child 点)。
## RefCounted 不挂树:零信号连接、零会话期引用、零核心写调用——组合根单点装配不变。
class_name UiRegistry
extends RefCounted
const ModalStack := preload("res://presentation/modal_stack.gd")
# ==================== 视觉层合同(唯一事实源——树序=登记序=z 序;改序只改此表) ====================
# kind: shell=屏壳容器(成员随壳不占独立树位;level_screen 例外另占登记位)/modal=模态层(入登记册,
# tags 随登记)/state=全屏态(占树位不入登记册)/node=非视觉节点(占树位不入登记册)。
# host=宿住屏壳(树序随宿主、登记序按本合同);shell=模态成员的屏壳(成员与屏壳同槽:屏壳占该树位、
# 成员占该登记位)。落位=apply_layer_contract(全仓唯一层序落位点,add_child 之后统一排序+登记)。
const LAYER_CONTRACT: Array[Dictionary] = [
{"key": "adventure_screen", "kind": "shell"},
{"key": "menu_screen", "kind": "shell"},
{"key": "row_pacer", "kind": "node"},
{"key": "legend_overlay", "kind": "modal", "tags": []},
{"key": "node_overlay", "kind": "modal", "tags": []},
{"key": "level_screen", "kind": "modal", "tags": [ModalStack.TAG_SCREEN_STATE], "host": "adventure_screen"},
{"key": "curio_overlay", "kind": "modal", "tags": []},
{"key": "end_overlay", "kind": "modal", "tags": []},
{"key": "shop_overlay", "kind": "modal", "tags": []},
{"key": "camp_overlay", "kind": "modal", "tags": [], "shell": "camp_screen"},
{"key": "settings_overlay", "kind": "modal", "tags": []},
{"key": "status_panel", "kind": "modal", "tags": []},
{"key": "career_overlay", "kind": "modal", "tags": []},
{"key": "save_select_overlay", "kind": "modal", "tags": []}, # 选档界面(营地交互重做卡③——登记生涯同档)
{"key": "inter_overlay", "kind": "state", "tags": []},
{"key": "confirm_dialog", "kind": "modal", "tags": []},
{"key": "debug_overlay", "kind": "modal", "tags": [ModalStack.TAG_NON_LOCKING]},
{"key": "debug_badge", "kind": "float"}, # 调试入口全局浮层——末位=最顶绘制序;
# float 类不注册 ModalStack(open() 遮挡记账只隐藏登记层)→ 任何 UI 打开都不遮它
]
var screens: UiScreens
var battle: UiBattle
var modals: UiModals
func _init() -> void:
screens = UiScreens.new()
battle = UiBattle.new()
modals = UiModals.new()
# 合同层寻址(key→节点;纯查表不含层序语义——层序唯一事实源=LAYER_CONTRACT)
func node(key: String) -> Node:
match key:
"adventure_screen": return screens.adventure_screen
"menu_screen": return screens.menu_screen
"row_pacer": return screens.row_pacer
"legend_overlay": return modals.legend_overlay
"node_overlay": return modals.node_overlay
"level_screen": return modals.level_screen
"curio_overlay": return modals.curio_overlay
"end_overlay": return modals.end_overlay
"shop_overlay": return modals.shop_overlay
"camp_screen": return screens.camp_screen
"camp_overlay": return modals.camp_overlay
"settings_overlay": return modals.settings_overlay
"status_panel": return modals.status_panel
"career_overlay": return modals.career_overlay
"save_select_overlay": return modals.save_select_overlay
"inter_overlay": return screens.inter_overlay
"confirm_dialog": return modals.confirm_dialog
"debug_overlay": return modals.debug_overlay
"debug_badge": return screens.debug_badge
return null
# 合同装配(_build_ui 尾调用):按 LAYER_CONTRACT 序统一落位——直接子层 move_child 到合同槽、
# 模态层按合同序登记;宿住层(host)树序随宿主只登记;成员层(shell)与屏壳同槽(屏壳占树位、成员占登记位)。
# 本函数是全仓唯一层序落位点(move_child 单口)——改层序只改 LAYER_CONTRACT。
func apply_layer_contract(root: Control) -> void:
var slot := 0
for entry: Dictionary in LAYER_CONTRACT:
var tags: Array[Variant] = entry.get("tags", [])
if entry.has("shell"):
var shell: Node = node(String(entry["shell"]))
var member: Node = node(String(entry["key"]))
if shell != null:
root.move_child(shell, slot)
slot += 1
if member != null:
modals.register_layer(member as Control, tags)
else:
push_error("[合同装配] 层缺失: %s" % String(entry["key"]))
continue
var layer: Node = node(String(entry["key"]))
if layer == null:
push_error("[合同装配] 层缺失: %s" % String(entry["key"]))
continue
if entry.has("host"):
modals.register_layer(layer as Control, tags)
continue
root.move_child(layer, slot)
slot += 1
if String(entry["kind"]) == "modal":
modals.register_layer(layer as Control, tags)
+1
View File
@@ -0,0 +1 @@
uid://d1hahbtan1te8
+16
View File
@@ -0,0 +1,16 @@
## 屏壳与全屏容器域句柄册:三屏壳、屏内一级子层、全屏态、浮层角标、排播泵节点的注册面域容器。
## 纯字段无方法;RefCounted 不挂树——节点本体树位与 add_child 归属由组合根与装配承接件负责。
class_name UiScreens
extends RefCounted
const RowPacer := preload("res://presentation/row_pacer.gd") # 排播泵取型(row_pacer.gd 非 class_name 全局)
var menu_screen: Control
var camp_screen: Control
var adventure_screen: Control
var start_overlay: Control
var battle_root: Control
var inter_overlay: Control
var inter_label: Label
var debug_badge: Button
var row_pacer: RowPacer
+1
View File
@@ -0,0 +1 @@
uid://dd8ep7nhmyrdq
+69 -31
View File
@@ -246,25 +246,30 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void:
for y in _board.height:
for x in _board.width:
var c := Vector2i(x, y)
var u: Node2D = _CELL_UNIT.new()
u.st.cell = c
u.st.edge = _cell
u.position = Vector2(c) * stride
u.st.bevel = DEPTH_BEVEL # 新建格单元继承
u.use_parent_material = true # 去饱和材质挂 BoardWorld——单元随世界材质灰化
if _palette.tiles_ok:
# NEAREST 采样(32×32 源 × 45/36px 格边非整数缩放接受档,
# 与英雄小人 1.5× 豁免同级(圣经 §8.2);只在瓦片
# 模式启用——程序化回退路径渲染零扰动(含格面文字采样不回退不变化)
u.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
u.st.font = _world.board_font
_world.add_child(u)
_cells[c] = u
_cells[c] = _build_cell_unit(c, stride)
set_cell_area(size)
_install_entrance_fx()
refresh()
# 逐格构建器(rebuild 建格循环唯一出函面):.new() 与属性装配逐字迁入,循环体只调构建器
func _build_cell_unit(c: Vector2i, stride: float) -> Node2D:
var u: Node2D = _CELL_UNIT.new()
u.st.cell = c
u.st.edge = _cell
u.position = Vector2(c) * stride
u.st.bevel = DEPTH_BEVEL # 新建格单元继承
u.use_parent_material = true # 去饱和材质挂 BoardWorld——单元随世界材质灰化
if _palette.tiles_ok:
# NEAREST 采样(32×32 源 × 45/36px 格边非整数缩放接受档,
# 与英雄小人 1.5× 豁免同级(圣经 §8.2);只在瓦片
# 模式启用——程序化回退路径渲染零扰动(含格面文字采样不回退不变化)
u.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
u.st.font = _world.board_font
_world.add_child(u)
return u
# 逐格重绘(挖掘/结算后调用)
func refresh() -> void:
if _board == null or _minesweeper == null:
@@ -606,9 +611,6 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
var seal: bool = _minesweeper.has_seal(c)
var removed: bool = _minesweeper.is_removed(c) # 中立移除格(退色降档)
var is_hero: bool = c == hero
var glyph := ""
var bg: Color
var digit_col := COL_DIGIT
# 封印槽统一维护:封印格=封印印记纹理;其余格清 null(封印格挖开后残留在 tex_seal
# 会让 _draw 无条件把封印印画在已开格上——tex_seal 绘制无 revealed 条件)。缺盘走字形回退。
u.st.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null
@@ -616,6 +618,23 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
revealed = false # 移除格固定未开呈现基座(退色暗幕层承载;危险格本无数字,周边数字冻结)
seal = false
u.st.tex_seal = null
# 绘制层五段(字形与色牌→基底瓦片→立面投影→秘宝/变异叠加→迷雾)——段间交换经单元状态面
#(u.st/u.glyph/u.glyph_color),唯一例外=基底色 bg 经 ink 段返回值交 base 段(无单元场域承载)
var bg: Color = _paint_cell_ink(c, u, revealed, seal)
_paint_cell_base(c, u, revealed, is_hero, bg)
_paint_cell_facade(c, u, revealed)
_paint_cell_overlays(c, u, revealed)
_paint_cell_fog(c, u, revealed, hero)
u.st.removed_dim = removed
u.queue_redraw()
# 字形与色牌段:reveal 记录→字形/墨色判定+u.glyph/u.glyph_color 终写+双峰纹理板落牌;
# 返回基底色 bg(平色回退 sb 构建的唯一消费方在基底段)
func _paint_cell_ink(c: Vector2i, u: Node2D, revealed: bool, seal: bool) -> Color:
var glyph := ""
var bg: Color
var digit_col := COL_DIGIT
if revealed:
var rec: Dictionary[Variant, Variant] = _minesweeper.reveal_record(c) # 已开记录经查询面
if rec.get("kind", "") == "mine":
@@ -639,6 +658,21 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
digit_col = COL_SEAL
else:
bg = COL_BRICK
u.glyph = glyph
u.glyph_color = digit_col
# 色牌后备(数字直画退牌;「危」/「?」维持牌载保读性)+ v3.4:双峰纹理板
#(_palette.digit_plate——underdark 系亮白筋+近黑斑并存的直画不可救纹理)数字落牌,牌上深墨 12:1
var plate_digit: bool = _palette.digit_plate and glyph != "" and glyph != "危" and glyph != "?" and glyph != "封" and glyph != "人"
if plate_digit:
digit_col = COL_DIGIT # 牌上深墨族(素牌 12:1——双峰纹理免疫)
u.glyph_color = digit_col
u.plate = revealed and (glyph == "危" or glyph == "?" or plate_digit)
return bg
# 基底瓦片段:瓦片查表/平色回退 sb+英雄蓝框载体;英雄贴图缺位回退字形「人」在此段落
# u.glyph(先于叠加段消费,结果与原函数字形累计局部量逐字同值)
func _paint_cell_base(c: Vector2i, u: Node2D, revealed: bool, is_hero: bool, bg: Color) -> void:
# 瓦片模式:tex_base = 查表瓦片(未开=墙瓦片/已开=地面瓦片),
# bg_sb 降级为透明描边载体——只承载呼吸微光/主角蓝框(CellUnit 瓦片态画在瓦片之上),
# 瓦片分配=格坐标哈希,初始化落定后逐拍稳定(禁逐轮重掷——圣经原则三)
@@ -658,21 +692,16 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
sb = UiKit.panel_flat(Color.TRANSPARENT, Color.TRANSPARENT,UiKit.RADIUS_OBJECT, 0)
sb.set_border_width_all(3)
sb.border_color = COL_HERO
if glyph == "" and _hero_sprite.tex_idle == null:
if u.glyph == "" and _hero_sprite.tex_idle == null:
# 贴图缺位回退字形「人」(项目纹理槽惯例——贴图在位=格内字形退役,单位本体由独立精灵承载)
glyph = "人"
digit_col = COL_HERO
u.glyph = "人"
u.glyph_color = COL_HERO
u.st.tex_base = tile
u.st.bg_sb = sb
u.glyph = glyph
u.glyph_color = digit_col
# 色牌后备(数字直画退牌;「危」/「?」维持牌载保读性)+ v3.4:双峰纹理板
#(_palette.digit_plate——underdark 系亮白筋+近黑斑并存的直画不可救纹理)数字落牌,牌上深墨 12:1
var plate_digit: bool = _palette.digit_plate and glyph != "" and glyph != "危" and glyph != "?" and glyph != "封" and glyph != "人"
if plate_digit:
digit_col = COL_DIGIT # 牌上深墨族(素牌 12:1——双峰纹理免疫)
u.glyph_color = digit_col
u.plate = revealed and (glyph == "危" or glyph == "?" or plate_digit)
# 立面投影段:未开墙格南缘立面/轮廓描边码;已开格北缘投影暗带/E 格高墙面——段内重置与赋值同序
func _paint_cell_facade(c: Vector2i, u: Node2D, revealed: bool) -> void:
u.st.outline_sides.assign([]) # typed Array[int](assign 保持引用语义)
u.st.facade_south = false
u.st.proj_band = 0.0
@@ -707,8 +736,15 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
if south_in and not _minesweeper.is_revealed(south_n) and _palette.tiles_ok:
u.st.facade_strip = true
u.st.tex_facade = _palette.tiles.get("wall_center")
# 秘宝/变异叠加段:真视红框/预读徽章/变异角标/奖励徽章/类型微光组号——glyph/digit_col 局部镜像
# 自 u.glyph/u.glyph_color(原函数字形累计局部量的段内重建,预读徽章分支的无效局部写入逐字保留)
func _paint_cell_overlays(c: Vector2i, u: Node2D, revealed: bool) -> void:
# ②( 秘宝叠加层:真视格=红内缩框;预读格=奖励身份徽章(identity_resolver 既有
# 只读面取字形——零数据复制,badge 位复用);已开格标记自然退位(refresh 逐拍重算)
var glyph: String = u.glyph
var digit_col: Color = u.glyph_color
if not revealed:
u.st.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else ""
if _treasure_preview.has(c) and glyph == "":
@@ -725,6 +761,10 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
u.st.mutation_mark = ""
u.st.badge_glyph = _update_badge(c)
u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE))
# 迷雾段:灯光区外未开格全黑/已开格记忆态灰显——纯呈现零逻辑(🔴 契约 1)
func _paint_cell_fog(c: Vector2i, u: Node2D, revealed: bool, hero: Vector2i) -> void:
# 视野迷雾呈态:曼哈顿距离 > 半径 → 区外——未开格全黑(现行事实源照抄)/
# 已开格记忆态(灰显可读);以英雄格为心逐格重算,refresh()/notify_hero_move() 既有重绘驱动=移动实时跟随。
# 纯呈现零逻辑——格态/数字真值/清图判定零触碰(🔴 契约 1);结算期去饱和叠加互斥由 CellUnit 记忆态终态档承载。
@@ -734,8 +774,6 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
u.fog_state = (2 if revealed else 1) if fogged else 0
else:
u.fog_state = 0
u.st.removed_dim = removed
u.queue_redraw()
# ---- 可挖域接触带呼吸微光(挖掘期唯一在动元素——§3.2 v2.8 画法:光画英雄格周界空间侧,
+2 -2
View File
@@ -85,7 +85,7 @@ func _xnum(v: float) -> String:
return "×%d" % int(round(v)) if absf(v - roundf(v)) < 0.005 else "×%.2f" % v
# 打开商店(products = runner.shop_products() 投影——[{product_id, display_name, attr, flat_value, price, purchased}])
# 打开商店(products = runner.shop.shop_products() 投影——[{product_id, display_name, attr, flat_value, price, purchased}])
func open_shop(products: Array, gp: int) -> void:
for child in _cards_box.get_children():
child.queue_free()
@@ -149,7 +149,7 @@ func _emit_buy_requested(p: Dictionary) -> void:
# ---- 共享池货架面(M-4 迭代 11 第②批 T4——P3 甲程序占位,三选一同构卡;UiKit 既有件)----
# 打开/重刷货架(cards = runner.shop_shelf() 投影——[{kind_id, display_name, value, price, stock_left}])。
# 打开/重刷货架(cards = runner.shop.shop_shelf() 投影——[{kind_id, display_name, value, price, stock_left}])。
# 买后组合根重投喂新货架 = 整架重建呈现(三件全重抽反馈——未买不保留);空架 = 池空硬闸合法态。
func open_shelf(cards: Array, gp: int) -> void:
for child in _cards_box.get_children():
+93 -33
View File
@@ -8,12 +8,10 @@
extends Node2D
const CHARGE_SKILL_IDS := ["heavy"]
# 护甲类型图标占位——形状+色相双通道(art-bible):轻甲=◇ 灰石中性 / 魔法护甲=◆ 小人蓝 /
# 重甲=▣ 深金(避开 Boss 专属紫赤与妨害紫)。
# 护甲类型图标三键(light/ward/heavy)——终稿 = 三枚 24×24 PNG(UiKit.TEX_ARMOR_*,art-bible §5.3
# 工单:形状承载身份、色相不承载身份;挂载底色 = 名字行素底 HP_BAR_BACK)。字形占位仅作素材缺位
# 回退(与立绘缺位回退链同构——素材不入 git,缺位不炸)。
const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"}
# 名行/底座纵向预算余量(盒底−1px「声明值刚好=容器值」系高危写法;
# ≥8px 硬余量 + 字形下探预算 ⇒ 12;取值不属色板,不进 ui_kit)
const STANDEE_FOOT_MARGIN := 12.0
const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值)
const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍)
@@ -97,9 +95,18 @@ func intent_probe() -> Dictionary:
return {"visible": st._intent_visible, "text": st._intent_glyph}
# 名字行探针出口
# 名字行探针出口(ui_probe 呈现口径 + 本批 V6/V8 断言面:armor_icon = 绑定纹理路径〔缺位回退字形〕、
# 几何 = 素底牌矩形与文字基线、素底色——V6 判据 = baseline_y < plinth_top_y)
func name_line_probe() -> Dictionary:
return {"name": st._display_name, "armor_icon": String(ARMOR_ICONS.get(st._armor, "◇"))}
var f := _efont()
var tex := UiKit.load_tex(_armor_tex_path(st._armor))
var plate := _name_plate_rect(f, tex)
return {"name": st._display_name,
"armor_icon": _armor_tex_path(st._armor) if tex != null else String(ARMOR_ICONS.get(st._armor, "◇")),
"armor_tex_bound": tex != null,
"plate": plate, "baseline_y": _name_baseline(plate).y,
"plinth_top_y": _pedestal_rect().position.y,
"plate_color": UiKit.HP_BAR_BACK}
# ---- setter 面(presenter/组合根零改排) ----
@@ -420,6 +427,36 @@ func _pedestal_rect() -> Rect2:
return Rect2(18.0, box.y - 18.0, box.x - 36.0, 12.0)
# 护甲图标纹理路径(甲档键 → UiKit.TEX_ARMOR_*——显式 match 单点,禁动态拼键取常量;
# 未知键回退轻甲路径,由 load_tex exists 守卫接住)
func _armor_tex_path(armor: String) -> String:
match armor:
"ward":
return UiKit.TEX_ARMOR_WARD
"heavy":
return UiKit.TEX_ARMOR_HEAVY
_:
return UiKit.TEX_ARMOR_LIGHT
# 名字行素底牌矩形(_draw 与 name_line_probe 共用几何——单一来源防两处漂移):
# 牌高 26 = 护甲图标 24px(甲类 1× 禁缩放)+ 上下各 1px;牌底沿 = 地台顶(box.y-18)——
# 文字禁直接压地台(§6.1/§7.2 素底落法);牌宽 = 行宽(图标+间距+名字)+ 8,水平居中
func _name_plate_rect(f: Font, tex: Texture2D) -> Rect2:
var icon_w := 24.0
if tex == null:
icon_w = f.get_string_size(String(ARMOR_ICONS.get(st._armor, "◇")),
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION).x
var name_w := f.get_string_size(st._display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION).x
var total_w := icon_w + 3.0 + name_w
return Rect2((box.x - total_w - 8.0) * 0.5, box.y - 18.0 - 26.0, total_w + 8.0, 26.0)
# 名字行文字基线(牌内几何单点——图标 24px 垂直居中牌内〔中心=牌顶+13〕,12px 字形基线=中心+4)
func _name_baseline(plate: Rect2) -> Vector2:
return plate.position + Vector2(4.0, 17.0)
# 绘制内容矩形(世界局部;ui_struct_probe ⑥standee「不出框+下余量≥8px」断言读数面——
# 最低绘制元素 = 底座下缘)
func content_rect() -> Rect2:
@@ -438,8 +475,9 @@ func _draw() -> void:
## 地面域:椭圆落影(圣经 §6.1 地台落影压脚:方向稿椭圆 (0,0,0,70),压在地台
## 上缘周围;24 段多边形近似椭圆,中心=台面上方 6px、横半径=台宽一半)
## + 石板地台(v3.5——方向稿取色:板身/描边/顶缘受光线 2px——取色单点收 UiKit.STAND_STONE 族
## ;几何沿用现行底座位〔margin 18/高 12——按方向稿对齐〕)
## + 石板地台(§6.1 近景地台独立件:TEX_STAGE_PLAZA 源 48×12 九宫格横向拉伸、顶缘受光线 2px
## 不随拉伸变形、跨三关恒定;素材缺位回退程序化三段绘制〔STAND_STONE 族方向稿取色,几何同源
## _pedestal_rect——与 panel_skinned 缺位回退同构〕)
func _draw_ground(f: Font) -> void:
var sh_center := Vector2(box.x * 0.5, box.y - 24.0)
var sh_rx := (box.x - 36.0) * 0.5
@@ -450,26 +488,41 @@ func _draw_ground(f: Font) -> void:
pts.append(sh_center + Vector2(cos(ang) * sh_rx, sin(ang) * sh_ry))
draw_colored_polygon(pts, Color(0, 0, 0, 0.27))
var ped := _pedestal_rect()
draw_rect(ped, UiKit.STAND_STONE, true)
draw_rect(ped, UiKit.STAND_STONE_EDGE, false, 1.0)
draw_rect(Rect2(ped.position.x, ped.position.y, ped.size.x, 2.0), UiKit.STAND_STONE_RIM, true)
var plaza := UiKit.load_tex(UiKit.TEX_STAGE_PLAZA)
if plaza != null:
_draw_plinth_stretched(plaza, ped)
else:
draw_rect(ped, UiKit.STAND_STONE, true)
draw_rect(ped, UiKit.STAND_STONE_EDGE, false, 1.0)
draw_rect(Rect2(ped.position.x, ped.position.y, ped.size.x, 2.0), UiKit.STAND_STONE_RIM, true)
## 血条域(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③);血条几何左 8 / 右 -8;
## 血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于 10px 条带垂直居中;
## 数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读)
## 地台九宫格横向拉伸域(循环体提函数——左右 4px 端头切片 + 中段拉伸;纵向恒 12px 源档,
## 顶缘受光线 2px 落纵向不缩放区,工单 §5.2 工艺要点)
func _draw_plinth_stretched(tex: Texture2D, ped: Rect2) -> void:
draw_texture_rect_region(tex, Rect2(ped.position, Vector2(4.0, 12.0)), Rect2(0, 0, 4, 12))
draw_texture_rect_region(tex, Rect2(ped.position + Vector2(4.0, 0.0),
Vector2(ped.size.x - 8.0, 12.0)), Rect2(4, 0, 40, 12))
draw_texture_rect_region(tex, Rect2(ped.position + Vector2(ped.size.x - 4.0, 0.0),
Vector2(4.0, 12.0)), Rect2(44, 0, 4, 12))
## 血条域(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③,字号 FS_HINT——art-bible §7.4 清单外
## 11–13px 提档归档);血条几何左 8 / 右 -8;血条填充 = 危险红 HP_BAR_FILL(§4.2 语义物件——
## 不因敌我分色);条底 = HP_BAR_TRACK;血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于
## 10px 条带垂直居中;数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读)
func _draw_hp_bar(f: Font) -> void:
var bar_x := 8.0
if st._kind == "enemy":
var order_txt := String("①②③"[clampi(st._order - 1, 0, 2)])
f.draw_string(get_canvas_item(), Vector2(bar_x, 12), order_txt,
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_BRIGHT)
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, UiKit.GOLD_BRIGHT)
bar_x += 20.0
var bar_w := box.x - 8.0 - bar_x
draw_rect(Rect2(bar_x, 2, bar_w, 10), Color(0.10, 0.10, 0.11)) # 条底(引擎默认主题深底近似平移)
draw_rect(Rect2(bar_x, 2, bar_w, 10), UiKit.HP_BAR_TRACK) # 条底(值单点收 ui_kit)
var fill_w := bar_w * st.hp_value
if fill_w > 0.0:
draw_rect(Rect2(bar_x, 2, fill_w, 10), Color(0.41, 0.61, 0.91)) # 直角填充(默认主题蓝近似——直角递减)
draw_rect(Rect2(bar_x, 2, fill_w, 10), UiKit.HP_BAR_FILL) # 直角填充(危险红——§4.2 血条显式句)
var hp_txt := "%d/%d" % [st._hp_cur, int(st._hp_max)]
var hts: Vector2 = f.get_string_size(hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11)
# 对比度垫牌(白 α.95 直压蓝条 2.67:1 违 §7.2;
@@ -513,19 +566,26 @@ func _draw_intent_badge(f: Font) -> void:
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
## 底座下 护甲类型图标+名字 常驻行(敌 only——AC 数值不展示,类型图标化)
## 名字行域(敌 only——AC 数值不展示,类型图标化):整行垫 HP_BAR_BACK 素底牌(§6.1/§7.2
## 「文字禁直接压地台」素底落法——牌底沿 = 地台顶,几何单点 _name_plate_rect);护甲图标 =
## 24×24 PNG(甲类 1×,缺位回退字形,色随甲档亮档族);名字 = 暖白(暗底双色墨制)
func _draw_armor_name_line(f: Font) -> void:
var armor_txt := String(ARMOR_ICONS.get(st._armor, "◇"))
var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制)
match st._armor:
"ward":
col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档
"heavy":
col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档
var name_w := f.get_string_size(st._display_name, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x
var armor_w := f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x
var total_w := armor_w + 3.0 + name_w
var baseline := Vector2((box.x - total_w) * 0.5, box.y - 1.0 - STANDEE_FOOT_MARGIN) # 纵向余量纪律
f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 12, col)
f.draw_string(get_canvas_item(), baseline + Vector2(armor_w + 3.0, 0), st._display_name,
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, UiKit.WARM) # v3.5 暗底双色墨制
var tex := UiKit.load_tex(_armor_tex_path(st._armor))
var plate := _name_plate_rect(f, tex)
draw_rect(plate, UiKit.HP_BAR_BACK, true)
var baseline := _name_baseline(plate)
var icon_w := 24.0
if tex != null:
draw_texture_rect(tex, Rect2(baseline.x, plate.position.y + 1.0, 24.0, 24.0), false)
else:
var armor_txt := String(ARMOR_ICONS.get(st._armor, "◇"))
icon_w = f.get_string_size(armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION).x
var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制)
match st._armor:
"ward":
col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档
"heavy":
col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档
f.draw_string(get_canvas_item(), baseline, armor_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION, col)
f.draw_string(get_canvas_item(), baseline + Vector2(icon_w + 3.0, 0.0), st._display_name,
HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_CAPTION, UiKit.WARM) # v3.5 暗底双色墨制
+10 -1
View File
@@ -190,7 +190,9 @@ const STAND_STONE_RIM := Color8(196, 172, 132) # 站桩石板地台·顶缘
const CAMP_HEADER_BAND := Color8(110, 69, 38) # 营地柜台头带深棕 #6E4526(4-camp.png 方向稿取色;对齐 demo --wood-deep)
const CAMP_ROW_PLATE := Color8(240, 230, 206) # 营地行板暖米(同上)
const CAMP_BADGE_PANEL := Color8(40, 32, 24) # 营地钥石徽暗底(同上)
const HP_BAR_BACK := Color8(40, 34, 32) # 迷你血条深底 #282220(兼血数垫牌)
const HP_BAR_BACK := Color8(40, 34, 32) # 迷你血条深底 #282220(兼血数垫牌;站桩名字行素底同源——§6.1/§7.2 文字禁压地台)
const HP_BAR_FILL := Color8(168, 50, 31) # 血条填充·危险红 #A8321F(§4.2 语义物件显式落法——不因敌我分色、不因关卡换族;蓝族 #689CE8 已登记违例退役)
const HP_BAR_TRACK := Color(0.10, 0.10, 0.11) # 血条条底(视图原裸值收口单点——非色板新增,现渲染值原样入册)
# BBCode 十六进制(RichTextLabel 用;与上同值)
const HEX_GOLD := "f0a828"
@@ -244,6 +246,13 @@ const TEX_ENEMY_DRIDER := "res://assets/art/character/character_enemy_drider.png
const TEX_ENEMY_DUERGAR := "res://assets/art/character/character_enemy_duergar.png" # 灰矮人
const TEX_ENEMY_HOOK_HORROR := "res://assets/art/character/character_enemy_hook_horror.png" # 钩爪恐魔
const TEX_ENEMY_DUERGAR_KING := "res://assets/art/character/character_enemy_duergar_king.png" # 灰矮人石君王
# 近景地台独立件(§6.1——源 48×12 九宫格横向拉伸、顶缘受光线 2px 不随拉伸变形;跨三关恒定不吃 wall_ambient tint)
const TEX_STAGE_PLAZA := "res://assets/art/scene/scene_stage_plaza.png"
# 护甲类型图标三键(art-bible §5.3 工单:对象键 = 甲档变体;源 24×24 显示 24px = 1× 甲类整数倍;
# 缺位 load_tex→null→字形回退,冒烟安全)
const TEX_ARMOR_LIGHT := "res://assets/ui/icons/ui_armor_light.png"
const TEX_ARMOR_WARD := "res://assets/ui/icons/ui_armor_ward.png"
const TEX_ARMOR_HEAVY := "res://assets/ui/icons/ui_armor_heavy.png"
# 框材组纹理槽(♻ 复用件量化改色入包,产物 = tools/quantize_ui_skin.py,
# 全 CC0;缺位 = load_tex null → 程序化回退原路径,冒烟安全)
+3 -3
View File
@@ -37,14 +37,14 @@ func _ready() -> void:
# 等关卡画面出图(choices_presented 异步到达——最多 3s 轮询)
for i in 60:
await get_tree().process_frame
if main.level_screen != null and main.level_screen.visible:
if main.ui.modals.level_screen != null and main.ui.modals.level_screen.visible:
break
if main.level_screen == null or not main.level_screen.visible:
if main.ui.modals.level_screen == null or not main.ui.modals.level_screen.visible:
_fail("关卡画面未出现(函数直调中间态路径)")
return
for i in 5:
await get_tree().process_frame
main.level_screen.press_candidate(0)
main.ui.modals.level_screen.press_candidate(0)
for i in 20:
await get_tree().process_frame
var st = main.run_scope.settlement
+6 -5
View File
@@ -23,12 +23,12 @@ func _ready() -> void:
add_child(main)
await get_tree().process_frame
ProbeUiUtil.isolate_save(main, "smoke_layout_probe", true, false, true)
main.debug_overlay.seed_edit.text = "42" # 种子钉死(确定性禁依赖默认随机分支)
main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(确定性禁依赖默认随机分支)
main._start_adventure_from_menu()
for i in range(8):
await get_tree().process_frame
# 进战斗屏——三区(B 战斗画面/C 角色信息/D 扫雷图)同屏常驻,菜单屏不构成断言面
main.level_screen.press_candidate(0)
main.ui.modals.level_screen.press_candidate(0)
for i in range(20):
await get_tree().process_frame
for vp in VIEWPORTS:
@@ -57,8 +57,9 @@ func _check_one(label: String, main: Node) -> void:
if vr.size != Vector2(VIEWPORTS[0]["size"]) and vr.size.y < 1.0:
_problems.append("[%s] 逻辑视口退化 %s" % [label, vr.size])
var rects := {}
var battle_ui: Object = main.get("ui").battle # 注册面战区域容器(字段名与三区 key 同名——合同键即字段名)
for key in ["stage", "info_panel", "board_zone"]:
var ctl: Control = main.get(key)
var ctl: Control = battle_ui.get(key)
if ctl == null:
_problems.append("[%s] %s = null(三区未装配)" % [label, key])
return
@@ -73,7 +74,7 @@ func _check_one(label: String, main: Node) -> void:
# battle_root 内层根下的全幅 ColorRect)这一缺陷类不在本探针拦截面内,探针对此仍全绿。
# 要拦它得改为递归遍历全子树找重叠,那是另一套判据(且极易误报容器父子关系),
# 本探针不设此判据。
var bv: Control = main.board_view
var bv: Control = main.ui.battle.board_zone.board_view
if bv == null:
_problems.append("[%s] board_view = null" % label)
return
@@ -89,6 +90,6 @@ func _check_one(label: String, main: Node) -> void:
if rects[keys[i]].intersects(rects[keys[j]]):
_problems.append("[%s] 顶层区重叠 %s×%s(%s ∩ %s)"
% [label, keys[i], keys[j], rects[keys[i]], rects[keys[j]]])
var cell: float = main.board_view.get("cell_size")
var cell: float = main.ui.battle.board_zone.board_view.get("cell_size")
if cell < MIN_CELL:
_problems.append("[%s] 格子边长 %.1fpx < 可读门槛 %.0fpx" % [label, cell, MIN_CELL])
+1 -1
View File
@@ -130,7 +130,7 @@ static func open_camp_via_savesel(main, tag: String) -> bool:
if FileAccess.file_exists(sp):
DirAccess.remove_absolute(sp)
main._open_save_select()
var new_btn: Button = main.save_select_overlay._new_btns.get(1)
var new_btn: Button = main.ui.modals.save_select_overlay._new_btns.get(1)
if new_btn == null:
return false
new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp
+21 -21
View File
@@ -50,10 +50,10 @@ func _ready() -> void:
# 其余任意两屏同时为真=互斥失守。
func _screen_seals() -> Dictionary:
return {
"菜单": main._start_overlay != null and main._start_overlay.visible,
"营地": main.camp_overlay != null and main.camp_overlay.visible,
"菜单": main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible,
"营地": main.ui.modals.camp_overlay != null and main.ui.modals.camp_overlay.visible,
"冒险": main.run_scope != null and main.run_scope.is_adventure,
"终局": main._end_overlay != null and main._end_overlay.visible,
"终局": main.ui.modals.end_overlay != null and main.ui.modals.end_overlay.visible,
}
@@ -101,7 +101,7 @@ func _frames(n: int = 2) -> void:
func _step_boot_menu() -> void:
_expect_exclusive("①boot", "菜单")
_expect_adventure_seal("①boot", false)
if main.debug_overlay != null and main.debug_overlay.visible:
if main.ui.modals.debug_overlay != null and main.ui.modals.debug_overlay.visible:
_problems.append("①boot:调试浮窗 boot 自动开(菜单优先失守)")
@@ -117,10 +117,10 @@ func _step_menu_to_camp() -> void:
# ---- ③ 营地 → 菜单(回退路径:营地关闭发自覆盖层自身信号)----
func _step_camp_back_to_menu() -> void:
if main.camp_overlay == null:
if main.ui.modals.camp_overlay == null:
_problems.append("③营地→菜单:营地覆盖层句柄缺失")
return
main.camp_overlay.closed.emit()
main.ui.modals.camp_overlay.closed.emit()
await _frames()
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点)
await _frames()
@@ -133,22 +133,22 @@ func _step_camp_back_to_menu() -> void:
func _step_save_select_open_back() -> void:
ProbeUiUtil.isolate_save(main, "smoke_screen_flow_savesel", false, false, true)
main.save_manager.slots.attach_slot_template("user://smoke_screen_flow_savesel_slot_%d.json")
var esb: Button = ProbeUiUtil.find_button(main._start_overlay, "进入存档", "prefix")
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
if esb == null:
_problems.append("[savesel] mn3 进入存档钮缺失(卡② 交付物)")
return
esb.pressed.emit() # 真实按钮信号路径 → _open_save_select(卡② mn3 点击链——卡③ 直调中间态就此闭环)
await get_tree().process_frame
var seals := _screen_seals()
if not main.save_select_overlay.visible or not seals_check_single(seals):
_problems.append("[savesel] sl10 开层后屏级互斥失守(选档=%s)" % str(main.save_select_overlay.visible))
var back_btn: Button = ProbeUiUtil.find_button(main.save_select_overlay, "返回", "prefix")
if not main.ui.modals.save_select_overlay.visible or not seals_check_single(seals):
_problems.append("[savesel] sl10 开层后屏级互斥失守(选档=%s)" % str(main.ui.modals.save_select_overlay.visible))
var back_btn: Button = ProbeUiUtil.find_button(main.ui.modals.save_select_overlay, "返回", "prefix")
if back_btn == null:
_problems.append("[savesel] sl10 返回钮缺失(页壳顶条)")
else:
back_btn.pressed.emit()
await get_tree().process_frame
if main.save_select_overlay.visible or not main._start_overlay.visible:
if main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("[savesel] sl10 返回后选档未隐藏或开始层未恢复(关闭链失守)")
# ESC 语境:再开选档→合成 ESC 键→层不关、流程零扰动(ESC 不属选档语境——零实现断言)
main._open_save_select()
@@ -159,7 +159,7 @@ func _step_save_select_open_back() -> void:
ev.pressed = true
main.get_viewport().push_input(ev, true)
await get_tree().process_frame
if not main.save_select_overlay.visible:
if not main.ui.modals.save_select_overlay.visible:
_problems.append("[savesel] sl10 ESC 关闭了选档层(选档语境 ESC 应不响应——卡⑤ 范围外)")
main._on_save_select_closed()
await get_tree().process_frame
@@ -171,7 +171,7 @@ func seals_check_single(f: Dictionary) -> bool:
for k in f:
if bool(f[k]):
on += 1
if main.save_select_overlay.visible:
if main.ui.modals.save_select_overlay.visible:
on += 1 # 选档自身计入(调用点已开层)
return on == 1
@@ -184,25 +184,25 @@ func _step_menu_to_adventure() -> void:
_expect_adventure_seal("④菜单→冒险", true)
if main.run_scope != null and main.run_scope.adventure == null:
_problems.append("④菜单→冒险:冒险实例未建立(入口停点未达)")
if main.level_screen != null and not main.level_screen.visible:
if main.ui.modals.level_screen != null and not main.ui.modals.level_screen.visible:
_problems.append("④菜单→冒险:入口停点未落在关卡地图屏(level_screen 不可见)")
# ---- ⑤ 冒险内屏切换:关卡地图屏 ⇄ 战斗屏(屏内两态,屏级仍独占)----
func _step_level_to_battle_and_back() -> void:
if not main.level_screen.visible:
if not main.ui.modals.level_screen.visible:
_problems.append("⑤冒险内切换:前置不在关卡地图屏")
return
main._exit_level_screen()
await _frames()
if main.level_screen.visible:
if main.ui.modals.level_screen.visible:
_problems.append("⑤冒险内切换:退出关卡屏后 level_screen 仍可见")
if main._battle_root != null and not main._battle_root.visible:
if main.ui.screens.battle_root != null and not main.ui.screens.battle_root.visible:
_problems.append("⑤冒险内切换:战斗屏未接手(battle_root 不可见)")
_expect_exclusive("⑤冒险内切换(关卡→战斗)", "冒险")
main._enter_level_screen()
await _frames()
if not main.level_screen.visible:
if not main.ui.modals.level_screen.visible:
_problems.append("⑤冒险内切换:回到关卡地图屏失败")
_expect_exclusive("⑤冒险内切换(战斗→关卡)", "冒险")
@@ -215,16 +215,16 @@ func _step_abandon_to_end() -> void:
main._on_abandon_requested()
await _frames(4)
_expect_exclusive("⑥放弃→终局", "终局")
if main._end_overlay != null and not main._end_overlay.visible:
if main.ui.modals.end_overlay != null and not main.ui.modals.end_overlay.visible:
_problems.append("⑥放弃→终局:终局覆盖层未开")
# ---- ⑦ 终局 → 菜单(终局出口返回开始界面)----
func _step_end_back_to_menu() -> void:
if main._end_overlay == null or not main._end_overlay.visible:
if main.ui.modals.end_overlay == null or not main.ui.modals.end_overlay.visible:
_problems.append("⑦终局→菜单:前置不在终局")
return
main._end_overlay.continue_pressed.emit()
main.ui.modals.end_overlay.continue_pressed.emit()
await _frames(3)
_expect_exclusive("⑦终局→菜单", "菜单")
if main.run_scope != null and main.run_scope.is_adventure:
+29 -29
View File
@@ -51,10 +51,10 @@ func _ready() -> void:
var top: Node = null
var top_rx := -1
var top_ry := -1
for c: Node in [main._start_overlay, main._battle_root, main._inter_overlay,
main.legend_overlay, main.node_overlay, main.level_screen, main.curio_overlay,
main._end_overlay, main.shop_overlay, main.camp_overlay, main.settings_overlay,
main.status_panel, main.career_overlay, main.confirm_dialog, main.debug_overlay]:
for c: Node in [main.ui.screens.start_overlay, main.ui.screens.battle_root, main.ui.screens.inter_overlay,
main.ui.modals.legend_overlay, main.ui.modals.node_overlay, main.ui.modals.level_screen, main.ui.modals.curio_overlay,
main.ui.modals.end_overlay, main.ui.modals.shop_overlay, main.ui.modals.camp_overlay, main.ui.modals.settings_overlay,
main.ui.modals.status_panel, main.ui.modals.career_overlay, main.ui.modals.confirm_dialog, main.ui.modals.debug_overlay]:
if c == null or not (c is CanvasItem) or not (c as CanvasItem).visible:
continue
var rx: int = c.get_parent().get_index()
@@ -63,14 +63,14 @@ func _ready() -> void:
top = c
top_rx = rx
top_ry = ry
if top != main._start_overlay:
if top != main.ui.screens.start_overlay:
_problems.append("boot 态顶层可见层≠start_overlay(实测 %s——菜单被埋)〔装配生命批 V4〕"
% (top.name if top != null else "null"))
# 自检二:菜单标题 label 必须 is_visible_in_tree 且 rect 落屏内
#(对比度只判反差不判「字在」——本条+checker 墨色占比共同补内容判据)
var vp := get_viewport().get_visible_rect()
for pair in [["menu_title", "Dungeon Sweeper"]]:
var lbl: Label = ProbeUiUtil.find_label(main._start_overlay, String(pair[1]))
var lbl: Label = ProbeUiUtil.find_label(main.ui.screens.start_overlay, String(pair[1]))
if lbl == null:
_problems.append("菜单文字缺失: %s(find_label 零命中)〔装配生命批 V4〕" % String(pair[1]))
continue
@@ -79,22 +79,22 @@ func _ready() -> void:
var gr: Rect2 = lbl.get_global_rect()
if not vp.encloses(gr):
_problems.append("菜单文字 rect 越出视口: %s(%s ⊄ %s)〔装配生命批 V4〕" % [String(pair[1]), str(gr), str(vp)])
_meta[String(pair[0])] = ProbeUiUtil.label_rect(main._start_overlay, String(pair[1]))
_meta[String(pair[0])] = ProbeUiUtil.label_rect(main.ui.screens.start_overlay, String(pair[1]))
await _snap("menu")
# ①b 设置面(settingsbtn 真实信号路径,开合同款真实钮)
var setbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "设置", "prefix")
var setbtn: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "设置", "prefix")
if setbtn == null:
_problems.append("设置入口缺失(开始层 settingsbtn)")
else:
setbtn.pressed.emit()
for i in 3:
await get_tree().process_frame
if not main.settings_overlay.visible:
if not main.ui.modals.settings_overlay.visible:
_problems.append("设置面未打开(settingsbtn 路径)")
else:
_meta["settings_title"] = ProbeUiUtil.label_rect(main.settings_overlay, "设置")
_meta["settings_title"] = ProbeUiUtil.label_rect(main.ui.modals.settings_overlay, "设置")
await _snap("settings")
var set_close: Button = ProbeUiUtil.find_button(main.settings_overlay, "返回", "prefix")
var set_close: Button = ProbeUiUtil.find_button(main.ui.modals.settings_overlay, "返回", "prefix")
if set_close == null:
_problems.append("设置面返回钮缺失")
else:
@@ -105,19 +105,19 @@ func _ready() -> void:
main._open_career()
for i in 3:
await get_tree().process_frame
if not main.career_overlay.visible or main._start_overlay.visible:
if not main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible:
_problems.append("生涯页未打开或开始层未隐藏(函数直调中间态路径)")
else:
_meta["career_title"] = ProbeUiUtil.label_rect(main.career_overlay, "生 涯")
_meta["career_title"] = ProbeUiUtil.label_rect(main.ui.modals.career_overlay, "生 涯")
await _snap("career")
var career_close: Button = ProbeUiUtil.find_button(main.career_overlay, "返回", "prefix")
var career_close: Button = ProbeUiUtil.find_button(main.ui.modals.career_overlay, "返回", "prefix")
if career_close == null:
_problems.append("生涯页返回钮缺失")
else:
career_close.pressed.emit()
for i in 3:
await get_tree().process_frame
if main.career_overlay.visible or not main._start_overlay.visible:
if main.ui.modals.career_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("生涯页关闭后页未隐藏或开始层未恢复")
# ② 营地(进档开层新路径——卡② mn9;槽模板注入隔离命名空间)
if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_shot_probe_camp"):
@@ -125,7 +125,7 @@ func _ready() -> void:
else:
await get_tree().process_frame
await _snap("camp")
var close_btn: Button = ProbeUiUtil.find_button(main.camp_overlay, "关闭营地", "contains")
var close_btn: Button = ProbeUiUtil.find_button(main.ui.modals.camp_overlay, "关闭营地", "contains")
if close_btn != null:
close_btn.pressed.emit()
await get_tree().process_frame
@@ -137,24 +137,24 @@ func _ready() -> void:
await get_tree().process_frame
await _snap("level")
# GP 常驻徽章断言(读数=runner.gp 现值,刷新=写路径回执后重查〔open_choices〕)
var gp_label: Label = ProbeUiUtil.find_label(main.level_screen, "◈ ")
var gp_label: Label = ProbeUiUtil.find_label(main.ui.modals.level_screen, "◈ ")
if gp_label == null or not gp_label.is_visible_in_tree():
_problems.append("GP 徽章不可见(关卡画面顶栏)")
elif main.level_screen.get("_runner") != null:
var expect := "◈ %d" % int(main.level_screen.get("_runner").gp)
elif main.ui.modals.level_screen.get("_runner") != null:
var expect := "◈ %d" % int(main.ui.modals.level_screen.get("_runner").gp)
if gp_label.text != expect:
_problems.append("GP 徽章读数不符: %s != %s" % [gp_label.text, expect])
# ④ 战斗(节点一——真实选路提交路径 press_candidate,ui_click_probe :199 同款)
if main.level_screen != null and main.level_screen.visible:
main.level_screen.press_candidate(0)
if main.ui.modals.level_screen != null and main.ui.modals.level_screen.visible:
main.ui.modals.level_screen.press_candidate(0)
for i in 10:
await get_tree().process_frame
await _snap("battle")
# 战斗屏文字框/明度带锚点(全局 rect——checker 取样坐标源;明细带锚 S1 裁后改认现行文案)
_meta["battle_log_hint"] = ProbeUiUtil.label_rect(main.stage, "上轮摘要")
_meta["battle_log_hint"] = ProbeUiUtil.label_rect(main.ui.battle.stage, "上轮摘要")
_meta["hero_panel"] = _node_rect(ProbeUiUtil.find_by_script(main, "hero_info_panel_view"))
_meta["stage_view"] = _node_rect(main.stage)
_meta["board_zone"] = _node_rect(main.board_zone)
_meta["stage_view"] = _node_rect(main.ui.battle.stage)
_meta["board_zone"] = _node_rect(main.ui.battle.board_zone)
# draw calls 真机测量(基线 270→目标 <200;窗口化渲染帧读数,
# 取值时机=战斗截图同帧后一帧——呈现终态满负荷)
await get_tree().process_frame
@@ -162,25 +162,25 @@ func _ready() -> void:
_meta["battle_draw_calls"] = dc
print("[shot_probe] battle draw_calls=%d(基线 270/目标 <200)" % dc)
# ⑤ 状态面板(战斗屏状态角标真实信号路径;层在战斗屏之上开合)
var pb: Button = main.board_zone.status_badge
var pb: Button = main.ui.battle.board_zone.status_badge
if pb == null or not pb.visible or pb.disabled:
_problems.append("状态面板入口角标缺失/置灰(战斗屏 board_zone)")
else:
pb.pressed.emit()
for i in 3:
await get_tree().process_frame
if not main.status_panel.visible:
if not main.ui.modals.status_panel.visible:
_problems.append("状态面板未打开(角标路径)")
else:
_meta["status_title"] = ProbeUiUtil.label_rect(main.status_panel, "状态")
_meta["status_title"] = ProbeUiUtil.label_rect(main.ui.modals.status_panel, "状态")
await _snap("status")
var status_close: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix")
var status_close: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix")
if status_close == null:
_problems.append("状态面板继续钮缺失")
else:
status_close.pressed.emit()
await get_tree().process_frame
if main.status_panel.visible:
if main.ui.modals.status_panel.visible:
_problems.append("状态面板关闭后未隐藏(继续钮路径)")
else:
_problems.append("关卡画面未出现(函数直调中间态路径)")
+9 -9
View File
@@ -29,14 +29,14 @@ func _ready() -> void:
main._start_adventure_from_menu() # 函数直调中间态(卡② mn10——开始冒险钮已撤)
for i in 60:
await get_tree().process_frame
if main.level_screen != null and main.level_screen.visible:
if main.ui.modals.level_screen != null and main.ui.modals.level_screen.visible:
break
if main.level_screen == null or not main.level_screen.visible:
if main.ui.modals.level_screen == null or not main.ui.modals.level_screen.visible:
_fail("关卡画面未出现")
return
for i in 5:
await get_tree().process_frame
main.level_screen.press_candidate(0)
main.ui.modals.level_screen.press_candidate(0)
for i in 20:
await get_tree().process_frame
if main.run_scope.settlement == null:
@@ -67,18 +67,18 @@ func _ready() -> void:
_fail("战斗未胜利(result='%s')" % str(main.run_scope.settlement.battle_result))
return
var t_settle := Time.get_ticks_msec()
var _rows_at_victory: int = main._row_pacer.pending_rows().size() # 时间戳①:结算入队完成帧(AP 耗尽/停手→五行段同步跑完→行全在队)
var _rows_at_victory: int = main.ui.screens.row_pacer.pending_rows().size() # 时间戳①:结算入队完成帧(AP 耗尽/停手→五行段同步跑完→行全在队)
# 断言①胜利同帧:终局面未开 + 挂起在案 + 队列非空(判别性——旧行为同帧开窗即红)
if main._end_overlay.is_open():
if main.ui.modals.end_overlay.is_open():
_problems.append("胜利同帧终局面已开(演播后排窗失效——同帧 flush 旧路径回潮)")
if main._deferred_modal == null:
if main.ui.modals.flow._deferred == null:
_problems.append("胜利同帧挂起态缺失(_deferred_modal == null)")
if main._row_pacer.pending_rows().is_empty():
if main.ui.screens.row_pacer.pending_rows().is_empty():
_problems.append("胜利同帧排播队列已空(结算行未走排播链)")
var _played_rows: int = _rows_at_victory
# 断言②排播放完:终局面开启(20s 守卫——0.22s×行数 ×10 余量)
var waited := 0.0
while not main._end_overlay.is_open() and waited < 20.0:
while not main.ui.modals.end_overlay.is_open() and waited < 20.0:
await get_tree().process_frame
waited += get_process_delta_time()
var t_popup := Time.get_ticks_msec()
@@ -86,7 +86,7 @@ func _ready() -> void:
print("[t17_defer] AP点用完: %s" % ap_clock)
print("[t17_defer] 触发结算: %s(与 AP 用完同帧——核心同步,相差 0ms)" % settle_clock)
print("[t17_defer] 弹窗出现: %s(距触发结算 %d ms = 逐行演播时长,行数 %d)" % [popup_clock, t_popup - t_ap_out, _played_rows])
if not main._end_overlay.is_open():
if not main.ui.modals.end_overlay.is_open():
_problems.append("排播放完后终局面未开(%.1fs——演播完边沿派发失守)" % waited)
if _problems.is_empty():
print("[t17_defer] PASS — 胜利同帧挂起+排播完开窗 两断言全过(waited=%.1fs)" % waited)
+171 -171
View File
@@ -51,7 +51,7 @@ func _ready() -> void:
var t_ui_ready := Time.get_ticks_msec() - t0
# 调试浮窗 boot 不开(菜单优先——入口=UI 最顶层全局浮层角标,
# 任何 UI 不遮挡;浮层角标遮挡判据见设置面段)
if main.debug_overlay.visible:
if main.ui.modals.debug_overlay.visible:
_problems.append("调试浮窗 boot 自动开(菜单优先失守)")
# ---- 营地购买合成点击路径(选档进档开层→购买→关闭全走真实按钮信号路径;
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json);陈旧档段首清理)----
@@ -65,17 +65,17 @@ func _ready() -> void:
_problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)")
else:
await get_tree().process_frame
if not main.camp_overlay.visible:
if not main.ui.modals.camp_overlay.visible:
_problems.append("营地层未打开(进档开层新路径)")
elif main.camp_overlay._tree_buy_btns.is_empty():
elif main.ui.modals.camp_overlay._tree_buy_btns.is_empty():
_problems.append("树淬炼柜台购买钮缺失(点击路径——四柜台重排后树=主界面)")
else:
# 树钮句柄用后即取(refresh_camp 随刷重建树行板——旧钮 queue_free,跨刷新句柄必失效)
iso_c.meta_state._add_keystone(9999)
main.camp_overlay.refresh_camp(main._camp_data())
main.ui.modals.camp_overlay.refresh_camp(main._camp_data())
await get_tree().process_frame
var buy_btn: Button = null
for card in main.camp_overlay._tree_buy_btns:
for card in main.ui.modals.camp_overlay._tree_buy_btns:
if String(card.node_id) == "sp_hp":
buy_btn = card.btn
if buy_btn == null:
@@ -93,16 +93,16 @@ func _ready() -> void:
_problems.append("营地树购买未持久(reload 后级 != 1)")
# Fork 双态切换点击路径(V20——购=解锁默认 A,钮切 B;前置=同调位 1,先点亮)
var socket_btn: Button = null
for card in main.camp_overlay._tree_buy_btns:
for card in main.ui.modals.camp_overlay._tree_buy_btns:
if String(card.node_id) == "att_socket1":
socket_btn = card.btn
if socket_btn != null:
socket_btn.pressed.emit()
await get_tree().process_frame
main.camp_overlay.refresh_camp(main._camp_data())
main.ui.modals.camp_overlay.refresh_camp(main._camp_data())
await get_tree().process_frame
var fork_btn: Button = null
for card in main.camp_overlay._tree_buy_btns:
for card in main.ui.modals.camp_overlay._tree_buy_btns:
if String(card.node_id) == "fork_posture":
fork_btn = card.btn
if fork_btn == null:
@@ -110,10 +110,10 @@ func _ready() -> void:
else:
fork_btn.pressed.emit()
await get_tree().process_frame
main.camp_overlay.refresh_camp(main._camp_data())
main.ui.modals.camp_overlay.refresh_camp(main._camp_data())
await get_tree().process_frame
var side_b: Button = null
for fk in main.camp_overlay._tree_fork_btns:
for fk in main.ui.modals.camp_overlay._tree_fork_btns:
if String(fk.group) == "fg_posture" and String(fk.side) == "b":
side_b = fk.btn
if side_b == null:
@@ -128,7 +128,7 @@ func _ready() -> void:
# ---- 营地六处灰字取色断言(迭代36 wc4——真实入口开营地后按文本锚取 label,
# font_color 逐处 == STONE_DEEP;六源位 :167 图鉴锁文/:417+:476 行状态字/:509 同调形态/:513 档位标签/:525 刷新行)----
var camp_labels: Array = []
_collect_labels_by_text(main.camp_overlay, camp_labels)
_collect_labels_by_text(main.ui.modals.camp_overlay, camp_labels)
var atlas_locks := _labels_with_text(camp_labels, "柜台结构位预留")
if atlas_locks.is_empty():
_problems.append("营地灰字:图鉴柜台锁文 label 未找到(wc4 定位锚失守)")
@@ -137,9 +137,9 @@ func _ready() -> void:
_problems.append("营地灰字:图鉴锁文未用 STONE_DEEP(wc4 :167)")
# 「未解锁」锚收窄到秘宝匣行容器子树(树行/锻造行也有「未解锁」态字样——非 wc4 目标位)
var locked_status: Array = []
if main.camp_overlay.get("_workshop_holder") != null:
if main.ui.modals.camp_overlay.get("_workshop_holder") != null:
var ws_labels: Array = []
_collect_labels_by_text(main.camp_overlay._workshop_holder, ws_labels)
_collect_labels_by_text(main.ui.modals.camp_overlay._workshop_holder, ws_labels)
locked_status = _labels_with_text(ws_labels, "未解锁")
if locked_status.is_empty():
_problems.append("营地灰字:秘宝匣行「未解锁」态 label 未找到(wc4 夹具前提失守——新档应有锁定秘宝行)")
@@ -177,7 +177,7 @@ func _ready() -> void:
await get_tree().process_frame
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点)
await get_tree().process_frame
if main.camp_overlay.visible or not main._start_overlay.visible:
if main.ui.modals.camp_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("营地关闭后未恢复开始层")
# ---- 选档界面行为走查(迭代 33 卡③):sl5 空槽新档链/sl6 有档槽进档链/sl7 删档二次确认双路径
# /sl8 消费方分派单口/sl12 动线序不回闪。开层=函数直调(「进入存档」钮归卡② mn3——中间态在案);
@@ -196,11 +196,11 @@ func _ready() -> void:
f_c2.close() # 槽2 有档异值(进档链夹具);槽1/3 空
main._open_save_select()
await get_tree().process_frame
if not main.save_select_overlay.visible:
if not main.ui.modals.save_select_overlay.visible:
_problems.append("[savesel] 选档层未打开(click 段前置)")
# sl12 动线序(新档链同拍采样):逐帧可见性次序——首帧选档隐藏时营地层必须已可见、开始层保持隐藏
#(禁先关后开——_close_modal 会恢复开始层=回闪一帧)
var new_btn: Button = main.save_select_overlay._new_btns.get(1)
var new_btn: Button = main.ui.modals.save_select_overlay._new_btns.get(1)
if new_btn == null:
_problems.append("[savesel] 槽1 新档钮缺失(sl5/sl12 驱动中断)")
else:
@@ -208,7 +208,7 @@ func _ready() -> void:
new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp
for fi in 3:
await get_tree().process_frame
samples.append([main.save_select_overlay.visible, main.camp_overlay.visible, main._start_overlay.visible])
samples.append([main.ui.modals.save_select_overlay.visible, main.ui.modals.camp_overlay.visible, main.ui.screens.start_overlay.visible])
if samples[0][0]:
_problems.append("[savesel] sl12 新档后选档层仍可见(落点开层未发生)")
elif not samples[0][1]:
@@ -217,8 +217,8 @@ func _ready() -> void:
_problems.append("[savesel] sl12 动线序违约:开始层在落点帧恢复(回闪一帧)")
if main.save_manager.slots.active_slot() != 1 or not FileAccess.file_exists(csel_tpl % 1):
_problems.append("[savesel] sl5 新档链失守(active=%d 应=1 且槽1 白板应落盘)" % main.save_manager.slots.active_slot())
var occluded: Array = main._modals._occluded.get(main.camp_overlay, [])
if not occluded.has(main.save_select_overlay):
var occluded: Array = main.ui.modals.flow.stack._occluded.get(main.ui.modals.camp_overlay, [])
if not occluded.has(main.ui.modals.save_select_overlay):
_problems.append("[savesel] sl12 选档层被关闭而非栈记账挂起(先关后开形态——遮挡挂起中间态钉死)")
if int((JSON.parse_string(FileAccess.get_file_as_string(csel_tpl % 1)) as Dictionary).get("meta", {}).get("keystone_balance", -1)) != 0:
_problems.append("[savesel] sl5 新档非白板(槽1 落盘应全零)")
@@ -227,7 +227,7 @@ func _ready() -> void:
# sl6 进档链:槽2 进档 → 激活=2(camp 数据随激活槽)
main._open_save_select()
await get_tree().process_frame
var load_btn: Button = main.save_select_overlay._load_btns.get(2)
var load_btn: Button = main.ui.modals.save_select_overlay._load_btns.get(2)
if load_btn == null:
_problems.append("[savesel] 槽2 进档钮缺失(sl6 驱动中断——夹具应有档)")
else:
@@ -235,51 +235,51 @@ func _ready() -> void:
await get_tree().process_frame
if main.save_manager.slots.active_slot() != 2:
_problems.append("[savesel] sl6 进档链失守(active=%d 应=2——激活该槽)" % main.save_manager.slots.active_slot())
if not main.camp_overlay.visible or main.save_select_overlay.visible:
if not main.ui.modals.camp_overlay.visible or main.ui.modals.save_select_overlay.visible:
_problems.append("[savesel] sl6 落点未开营地层或选档未隐藏")
main._on_camp_closed()
await get_tree().process_frame
# sl7 删档二次确认双路径:一击只开窗(文件仍在)→取消只关窗(文件仍在)→再开窗确认才删(槽回空)
main._open_save_select()
await get_tree().process_frame
var del_btn: Button = main.save_select_overlay._delete_btns.get(2)
var del_btn: Button = main.ui.modals.save_select_overlay._delete_btns.get(2)
if del_btn == null:
_problems.append("[savesel] 槽2 删档钮缺失(sl7 驱动中断)")
else:
del_btn.pressed.emit()
await get_tree().process_frame
if not main.confirm_dialog.is_open():
if not main.ui.modals.confirm_dialog.is_open():
_problems.append("[savesel] sl7 一击未只开确认窗(直删形态)")
elif not FileAccess.file_exists(csel_tpl % 2):
_problems.append("[savesel] sl7 确认前文件已被删(一次点击即删违契)")
else:
if not main._modal_flow.stack._occluded.has(main.confirm_dialog):
if not main.ui.modals.flow.stack._occluded.has(main.ui.modals.confirm_dialog):
_problems.append("[savesel] sl7 确认窗未经模态栈开(直开旁路——wr2 单口失守)")
elif not main._input_locked():
elif not main.ui.modals.flow.locks_input():
_problems.append("[savesel] sl7 确认窗开时输入锁未生效(锁域失守——wr2)")
main.confirm_dialog.cancelled.emit() # 取消路径(真实信号——cancelled 钮同源)
main.ui.modals.confirm_dialog.cancelled.emit() # 取消路径(真实信号——cancelled 钮同源)
await get_tree().process_frame
if main.confirm_dialog.is_open() or not FileAccess.file_exists(csel_tpl % 2):
if main.ui.modals.confirm_dialog.is_open() or not FileAccess.file_exists(csel_tpl % 2):
_problems.append("[savesel] sl7 取消路径失守(应只关窗、文件仍在)")
elif main._confirm_consumer != "":
_problems.append("[savesel] sl8 取消后消费方状态量未清(分派面残留)")
if main._modal_flow.stack._occluded.has(main.confirm_dialog):
if main.ui.modals.flow.stack._occluded.has(main.ui.modals.confirm_dialog):
_problems.append("[savesel] sl7 取消后确认窗遮挡账未清(关侧未走模态栈——wr2)")
del_btn.pressed.emit() # 再开窗走确认路径
await get_tree().process_frame
main.confirm_dialog.confirmed.emit() # 确认路径(真实信号——confirmed 钮同源)
main.ui.modals.confirm_dialog.confirmed.emit() # 确认路径(真实信号——confirmed 钮同源)
await get_tree().process_frame
if FileAccess.file_exists(csel_tpl % 2) or main.confirm_dialog.is_open():
if FileAccess.file_exists(csel_tpl % 2) or main.ui.modals.confirm_dialog.is_open():
_problems.append("[savesel] sl7 确认路径失守(应删档+关窗)")
elif main.run_scope.is_adventure or main._start_overlay.visible:
elif main.run_scope.is_adventure or main.ui.screens.start_overlay.visible:
_problems.append("[savesel] sl8 删档确认误触发开局(双常驻接线形态——分派单口失守)")
elif bool(main.save_select_overlay._occupied_flags.get(2, true)):
elif bool(main.ui.modals.save_select_overlay._occupied_flags.get(2, true)):
_problems.append("[savesel] sl7 删后选档面未就地刷新(槽2 应回空态)")
elif main._confirm_consumer != "":
_problems.append("[savesel] sl8 确认后消费方状态量未清(分派面残留)")
main._on_save_select_closed()
await get_tree().process_frame
if main.save_select_overlay.visible or not main._start_overlay.visible:
if main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("[savesel] 关闭后层未隐藏或开始层未恢复(模态栈单口失守)")
# ---- 设置面段(隔离 store 换注禁碰真实 user://settings.cfg;真实按钮信号路径)----
@@ -289,19 +289,19 @@ func _ready() -> void:
var iso_s = load("res://presentation/settings_store.gd").new("user://smoke_click_probe_settings.cfg")
iso_s.load_settings()
main.settings_store = iso_s
main.settings_overlay.retarget(iso_s)
var settings_btn: Button = ProbeUiUtil.find_button(main._start_overlay, "设置", "prefix")
main.ui.modals.settings_overlay.retarget(iso_s)
var settings_btn: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "设置", "prefix")
if settings_btn == null:
_problems.append("设置入口按钮缺失(开始层 settingsbtn)")
else:
settings_btn.pressed.emit() # 真实按钮信号路径 → _open_settings
await get_tree().process_frame
if not main.settings_overlay.visible or main._start_overlay.visible:
if not main.ui.modals.settings_overlay.visible or main.ui.screens.start_overlay.visible:
_problems.append("设置面未打开(点击路径)")
else:
# 减动效转正双态(卡④ AC③)——M4 适配:拨杆开关(_motion_btn)语义翻转 on=完整/off=快速,
# 状态字随行 _motion_st;查找由文字改成员引用(拨杆无文字)
var motion_btn: Button = main.settings_overlay._motion_btn
var motion_btn: Button = main.ui.modals.settings_overlay._motion_btn
if motion_btn == null:
_problems.append("设置面减动效开关缺失")
else:
@@ -318,7 +318,7 @@ func _ready() -> void:
if UiKit.motion_scale != 1.0:
_problems.append("减动效关未恢复(motion_scale != 1.0)")
# 音量实时生效(卡④ AC①):滑杆 30% → master bus db 与 store 双读
var vol: HSlider = main.settings_overlay._volume
var vol: HSlider = main.ui.modals.settings_overlay._volume
vol.value = 30
await get_tree().process_frame
var master_idx := AudioServer.get_bus_index("Master")
@@ -329,24 +329,24 @@ func _ready() -> void:
_problems.append("音量未入 store(%f ≠ 0.3)" % iso_s.volume_linear)
# 调试入口断言(卡⑦ c4 发布态换代——角标恒隐;原「最顶层浮层不被遮挡」
# 回归语义随调试浮层发布态退役,申报见迭代 38 实现报告 §8.2)
if main.debug_badge == null or main.debug_badge.visible:
if main.ui.screens.debug_badge == null or main.ui.screens.debug_badge.visible:
_problems.append("设置面开中调试角标可见(发布态恒隐失守)")
var settings_close: Button = ProbeUiUtil.find_button(main.settings_overlay, "返回", "prefix")
var settings_close: Button = ProbeUiUtil.find_button(main.ui.modals.settings_overlay, "返回", "prefix")
if settings_close == null:
_problems.append("设置面返回按钮缺失")
else:
settings_close.pressed.emit()
await get_tree().process_frame
if main.settings_overlay.visible or not main._start_overlay.visible:
if main.ui.modals.settings_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("设置面关闭后未恢复开始层")
# 旧入口零残留断言(卡④ AC③——测试调整板不含减动效开关;14.5 起载体=调试浮窗)
var legacy_motion = ProbeUiUtil.find_check_button(main.debug_overlay, "减动效")
var legacy_motion = ProbeUiUtil.find_check_button(main.ui.modals.debug_overlay, "减动效")
if legacy_motion != null:
_problems.append("调整板减动效旧入口未退役(设置面唯一入口失守——卡④ AC③)")
# D1 断言(二轮):退出游戏只在一级菜单——开始菜单有、设置面无
if ProbeUiUtil.find_button(main._start_overlay, "退出游戏", "prefix") == null:
if ProbeUiUtil.find_button(main.ui.screens.start_overlay, "退出游戏", "prefix") == null:
_problems.append("开始菜单缺退出游戏按钮(二轮 D1)")
if ProbeUiUtil.find_button(main.settings_overlay, "退出游戏", "prefix") != null:
if ProbeUiUtil.find_button(main.ui.modals.settings_overlay, "退出游戏", "prefix") != null:
_problems.append("设置面仍含退出游戏按钮(二轮 D1——只在一级菜单出现)")
# ---- 生涯页段(入口点击链+关闭回返,真实按钮信号路径;本页只读零写,
# 值断言归冒烟投影段与 ui_struct 结构族,此处只验开合路径与零跳转)----
@@ -354,45 +354,45 @@ func _ready() -> void:
if true:
main._open_career()
await get_tree().process_frame
if not main.career_overlay.visible or main._start_overlay.visible:
if not main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible:
_problems.append("生涯页未打开(点击路径)")
else:
# 零跳转契约(提纲 P4 甲):页内除「返回」外零导航钮
for nav in ["营地", "商店", "设置", "退出", "冒险"]:
if ProbeUiUtil.find_button(main.career_overlay, nav, "contains") != null:
if ProbeUiUtil.find_button(main.ui.modals.career_overlay, nav, "contains") != null:
_problems.append("生涯页内出现导航入口「%s」(零跳转违约——生涯纯只读)" % nav)
# 局内入口断言(提纲 P4 甲):生涯入口只在开始层——战斗屏/地图屏零生涯角标
if main.board_zone.find_child("career_badge", true, false) != null \
or main.level_screen.find_child("career_badge", true, false) != null:
if main.ui.battle.board_zone.find_child("career_badge", true, false) != null \
or main.ui.modals.level_screen.find_child("career_badge", true, false) != null:
_problems.append("局内屏出现生涯入口件(局内不放生涯入口违约)")
var career_close: Button = ProbeUiUtil.find_button(main.career_overlay, "返回", "prefix")
var career_close: Button = ProbeUiUtil.find_button(main.ui.modals.career_overlay, "返回", "prefix")
if career_close == null:
_problems.append("生涯页返回按钮缺失")
else:
career_close.pressed.emit()
await get_tree().process_frame
if main.career_overlay.visible or not main._start_overlay.visible:
if main.ui.modals.career_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("生涯页关闭后未恢复开始层")
# 换回全新零档(营地购买态不泄入后续冒险段——确定性;档案残留下次段首清理兜底)
ProbeUiUtil.isolate_save(main, "smoke_click_probe_reset", true, false, true) # 零档换注(继续上局钮已随三入口化撤出——卡② mn8)
main.debug_overlay.set_sliders(400, 200) # 高加成保 L1 可完成——L2 岔路选路段只能经冒险推进到达(单关直入口退役)
main.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,行为走查确定性禁依赖默认分支)
main.ui.modals.debug_overlay.set_sliders(400, 200) # 高加成保 L1 可完成——L2 岔路选路段只能经冒险推进到达(单关直入口退役)
main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,行为走查确定性禁依赖默认分支)
var t1 := Time.get_ticks_msec()
main._start_adventure_from_menu()
await get_tree().process_frame
await get_tree().process_frame
# 既有口径回归(迭代36 wc3——调试滑条 400/200=局外加成 pct>0,开场行应带「(含面板加成)」;
# 本局为本探针首个冒险,日志无先前行,全文本判即可)
var wc3_txt: String = main.log_view._log.get_parsed_text()
var wc3_txt: String = main.ui.battle.log_view._log.get_parsed_text()
if not wc3_txt.contains("种子 42") or not wc3_txt.contains("(含面板加成)"):
_problems.append("开场行回归失守:局外加成(pct>0)局的开场行应带「(含面板加成)」(wc3)")
# 关卡开始 = 入口选路停点——press_candidate 图上命中路径选入口候选进首战
#(首格可点 AC 口径随之含入口选路一步:菜单点击 → 停点呈现 → pick → 棋盘安装)
if main.level_screen.visible:
if main.ui.modals.level_screen.visible:
# 独立性断言:独立屏激活期战斗屏宿主整体隐藏
if main._battle_root.visible:
if main.ui.screens.battle_root.visible:
_problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)")
main.level_screen.press_candidate(0)
main.ui.modals.level_screen.press_candidate(0)
await get_tree().process_frame
await get_tree().process_frame
var t_entry := Time.get_ticks_msec() - t1
@@ -402,7 +402,7 @@ func _ready() -> void:
var first_cell_ms := t_entry
if first_cell_ms > 500:
_problems.append("进入战斗屏首格可点超时 %dms > 500ms" % first_cell_ms)
if main.board_view.cells_probe().is_empty():
if main.ui.battle.board_zone.board_view.cells_probe().is_empty():
_problems.append("首格未可点(cells=0)")
print("[click_probe] 首格可点计时 %dms(进入战斗屏口径,≤500ms AC)· 场景冷启动 %dms(含 UI 构建 %dms,观测项)" % [
first_cell_ms, t_total, t_ui_ready])
@@ -413,68 +413,68 @@ func _ready() -> void:
while not _done and action_budget > 0:
action_budget -= 1
await get_tree().process_frame
if main.node_overlay.visible:
main.node_overlay.press_continue()
if main.ui.modals.node_overlay.visible:
main.ui.modals.node_overlay.press_continue()
_seals_placed.clear()
continue
if main.curio_overlay.visible:
if main.ui.modals.curio_overlay.visible:
# 三选一停点(CurioPickOverlay——奖励节点/关卡完成兑付共用面,须与硬锁态适配,
# 未适配则走到即卡死)。两态分派:result 态(兑付反馈——点继续推进)/
# 待选态(点首件+确认)。真实信号路径全线。〔无替换子态——装配不可变契约。〕
# 三选一停点输入锁定断言(停点态断言面:待选/兑付两态共用此层)。
if not _tested_curio_head and main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty() and String(main.curio_overlay._header).contains("关卡奖励点"):
if not _tested_curio_head and main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty() and String(main.ui.modals.curio_overlay._header).contains("关卡奖励点"):
# 停点态判据 = _node_curio_pending(奖励节点三选一停点住此——原条件查
# _curio_pending〔关卡完成三选一态〕与奖励头部互斥、恒不可命中=拦截能力未证病根,
# 卡⑤ d4 转正随修;头部校验串本体零触碰)
_tested_curio_head = true
var ch := String(main.curio_overlay._header)
var ch := String(main.ui.modals.curio_overlay._header)
if not ch.contains("冒险牌库") or not ch.contains("属性值提升与补给") or ch.contains("同权重") or ch.contains("同上限") or ch.contains("提升池"):
_problems.append("三选一头部旧话残留或新话缺失(wa4——%s)" % ch.replace("
", "|"))
# 奖励节点战报文案换代断言(卡⑤ wr2——reward_line 同屏打印于停点拉起前,
# 同一驱动路径承载;旧「内容待定」残留或新话缺失即红)
var logtext: String = main.log_view.parsed_text()
var logtext: String = main.ui.battle.log_view.parsed_text()
if not logtext.contains("关卡奖励点(三选一——选毕推进)") or logtext.contains("内容待定"):
_problems.append("奖励节点战报文案未换代(卡⑤ wr2——旧话残留或新话缺失)")
if not main._input_locked():
if not main.ui.modals.flow.locks_input():
_problems.append("三选一停点打开时输入未锁定")
if main.curio_overlay._continue_btn.visible:
main.curio_overlay.press_continue()
if main.ui.modals.curio_overlay._continue_btn.visible:
main.ui.modals.curio_overlay.press_continue()
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty():
main.curio_overlay.press_card(0) # 奖励节点待选态:点首件+确认(真实按钮信号路径——与关卡完成停点同形)
main.curio_overlay.press_confirm()
main.ui.modals.curio_overlay.press_card(0) # 奖励节点待选态:点首件+确认(真实按钮信号路径——与关卡完成停点同形)
main.ui.modals.curio_overlay.press_confirm()
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
main.curio_overlay.press_card(0)
main.curio_overlay.press_confirm()
main.ui.modals.curio_overlay.press_card(0)
main.ui.modals.curio_overlay.press_confirm()
else:
main.curio_overlay.press_continue() # 停点已消费的残面——继续关屏
main.ui.modals.curio_overlay.press_continue() # 停点已消费的残面——继续关屏
_seals_placed.clear()
continue
if main.shop_overlay.visible:
if main.ui.modals.shop_overlay.visible:
# 商店房覆盖层(摆放约束:每终点前一位恰一店,真实路径恒必经一店):零购离开(真实 leave 信号路径)放行选路。
# 分派链序:商店层=选路页上的模态覆盖(停点呈现为
# level_screen+shop 双 visible)——必须先于 level_screen 分派,否则下层候选点击
# 空转烧尽预算(实证:lv2 商店停点双 visible 循环 12000 帧 done=false)。
main.shop_overlay.leave_pressed.emit()
main.ui.modals.shop_overlay.leave_pressed.emit()
_seals_placed.clear()
continue
if main.level_screen.visible:
if main.ui.modals.level_screen.visible:
# 节点图选路口(零自动推进停点)——图上命中路径交替选候选(岔路使用行为;布局决策 2 改造)
# 地图屏角标一次性 case:选路前经角标开/关状态面板(层在地图屏之上开合,
# 关闭后地图屏选路态零扰动——P2 甲层级语义)
if not _tested_map_status and not main.status_panel.visible:
if not _tested_map_status and not main.ui.modals.status_panel.visible:
_tested_map_status = true
var mpb: Button = main.level_screen.status_badge
var mpb: Button = main.ui.modals.level_screen.status_badge
if mpb == null or not mpb.visible or mpb.disabled:
_problems.append("地图屏状态角标缺失/未点亮/被置灰(P1 丁)")
else:
mpb.pressed.emit()
if not main.status_panel.visible or not main.level_screen.visible:
if not main.ui.modals.status_panel.visible or not main.ui.modals.level_screen.visible:
_problems.append("地图屏角标开状态面板失败或地图屏被误藏(P2 甲层级失守)")
var rbtn2: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix")
var rbtn2: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix")
if rbtn2 != null:
rbtn2.pressed.emit()
if main.status_panel.visible or not main.level_screen.visible:
if main.ui.modals.status_panel.visible or not main.ui.modals.level_screen.visible:
_problems.append("状态面板关闭后地图屏态失守(恢复语义破)")
# 地图屏加成帽换代断言(迭代36 卡A wa2——「特性」→「属性值提升」;帽 Label 常在树;
# 独立旗标——_tested_map_status 在同拍前置块已置位,禁复用其门控)
@@ -482,7 +482,7 @@ func _ready() -> void:
_tested_map_cap = true
var cap_found := false
var ls_labels: Array = []
_collect_labels_by_text(main.level_screen, ls_labels)
_collect_labels_by_text(main.ui.modals.level_screen, ls_labels)
for l in ls_labels:
if l.text == "属性值提升":
cap_found = true
@@ -494,20 +494,20 @@ func _ready() -> void:
if main.run_scope.runner != null:
for nd in main.run_scope.runner.nodes():
var nd_id: int = main.run_scope.runner.node_id_of(nd)
if String(nd.get("type", "")) == "reward" and main.level_screen._cand_ids.has(nd_id):
pick = main.level_screen._cand_ids.find(nd_id)
if String(nd.get("type", "")) == "reward" and main.ui.modals.level_screen._cand_ids.has(nd_id):
pick = main.ui.modals.level_screen._cand_ids.find(nd_id)
break
main.level_screen.press_candidate(pick)
main.ui.modals.level_screen.press_candidate(pick)
_route_clicks += 1
_seals_placed.clear()
continue
if main._inter_overlay.visible:
if main.ui.screens.inter_overlay.visible:
# 大奖励屏(冒险级停点——非末关的关卡完成恰一次):首见确认推进 L2(真实按钮信号路径);再见 = L2 已关卡完成,收口
if not main._input_locked():
if not main.ui.modals.flow.locks_input():
_problems.append("大奖励屏打开时输入未锁定")
_inter_confirms += 1
if _inter_confirms == 1:
var next_btn: Button = ProbeUiUtil.find_button(main._inter_overlay, "进入下一层", "prefix")
var next_btn: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix")
if next_btn == null:
_problems.append("大奖励屏确认按钮缺失")
else:
@@ -522,23 +522,23 @@ func _ready() -> void:
break
_seals_placed.clear()
continue
if main._end_overlay.is_open() and main._settlement_stop_pending:
if main.ui.modals.end_overlay.is_open() and main.run_scope.settlement_stop_timer.is_armed():
# 战斗胜利总结结算框(真实信号路径——每场普通胜利到达):断言已拍板面后点「下一节点」
if not main._input_locked():
if not main.ui.modals.flow.locks_input():
_problems.append("胜利结算框打开时输入未锁定")
var gp_line: String = "%s %s" % [main._end_overlay._gp_name.text, main._end_overlay._gp_val.text] # 两列账行(审计差距④)——名/值列拼回整串断言
var gp_line: String = "%s %s" % [main.ui.modals.end_overlay._gp_name.text, main.ui.modals.end_overlay._gp_val.text] # 两列账行(审计差距④)——名/值列拼回整串断言
if not gp_line.contains("GP +%d" % main.run_scope.last_gain_amount) or not gp_line.contains("合计 %d" % int(main.run_scope.runner.gp)):
_problems.append("胜利结算框 GP 增量/总量与钱包实态不符(%s|实态 +%d/%d)" % [gp_line, main.run_scope.last_gain_amount, int(main.run_scope.runner.gp)])
if main._end_overlay.verdict_text() == "":
if main.ui.modals.end_overlay.verdict_text() == "":
_problems.append("胜利结算框判定行缺失")
_settlement_confirms += 1
main._end_overlay._btn.pressed.emit() # 「下一节点」真实信号路径
main.ui.modals.end_overlay._btn.pressed.emit() # 「下一节点」真实信号路径
await get_tree().process_frame
await get_tree().process_frame
if main._settlement_stop_pending or not main.level_screen.visible:
if main.run_scope.settlement_stop_timer.is_armed() or not main.ui.modals.level_screen.visible:
_problems.append("胜利结算框「下一节点」未进选路页(卡⑥ AC②)")
continue
if main._end_overlay.is_open():
if main.ui.modals.end_overlay.is_open():
# 冒险终局(失败/放弃/打穿)——无单关直入口,见终局即收口(模态断言照旧)
# 三选一链适配:_adventure_mode 仍真且非选路挂起=中途结算面(Boss 胜利
# 总结「继续」后挂三选一→真终局未到)——真实信号路径点继续推进。
@@ -547,13 +547,13 @@ func _ready() -> void:
# (_adventure_mode)在真终局仍 true——会把真终局当中途面点掉返回键(实证)。
var _adv_running: bool = main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running"
if main.run_scope.is_adventure and _adv_running:
main._end_overlay._btn.pressed.emit()
main.ui.modals.end_overlay._btn.pressed.emit()
continue
if not main._input_locked():
if not main.ui.modals.flow.locks_input():
_problems.append("终局 overlay 打开时输入未锁定")
_done = true
print("[click_probe] 冒险终局:%s · 选路口点击 %d 次" % [
main._end_overlay.verdict_text().replace("\n", " "), _route_clicks])
main.ui.modals.end_overlay.verdict_text().replace("\n", " "), _route_clicks])
break
if main.run_scope.settlement == null or main.run_scope.runner == null:
continue
@@ -566,16 +566,16 @@ func _ready() -> void:
# 上轮摘要恢复断言(卡⑥ wr1-b——改前 _round_rows 类型错 append 全数失败、缓冲恒空、
# 摘要自引入类型错以来从未显示):战中场观测点=round≥2 且排播队列已排空(轮首分隔行
# 已消费=finish_settlement_round 已刷);不挂循环出口——收尾的续玩重建段会再走 log_view.clear()
if not _tested_round_summary and int(st["round"]) >= 2 and main._row_queue.is_empty():
if not _tested_round_summary and int(st["round"]) >= 2 and main.ui.screens.row_pacer.pending_rows().is_empty():
_tested_round_summary = true
if main.log_view._summary.text == "":
if main.ui.battle.log_view._summary.text == "":
_problems.append("上轮摘要恒空(卡⑥ wr1-b——结算行缓冲仍失真)")
if JSON.stringify(st) == JSON.stringify(_last_state): # 排播演播期豁免(硬锁 v3——合法停顿态):演播期棋盘锁生效、probe 点击被吞、
# 逻辑态恒定属预期。判据=纯队列(与 main 锁同源)。Waiting 收尾等待期**不豁免**——
# 锁若把收尾链卡死,此处 stall 照报(探测面)。
# T1 D1 处置=保留豁免+注记:冻结/缓存/重放语义不再靠豁免绕行,由 _locktest_tick
# 脚本化断言正面承载(每 run 一次;逻辑态恒定本豁免继续作 stall 判据)。
if main._row_queue.is_empty():
if main.ui.screens.row_pacer.pending_rows().is_empty():
_stall_frames += 1
if _stall_frames == 30:
_report_stall(st)
@@ -595,36 +595,36 @@ func _ready() -> void:
# ---- 预算耗尽收口(T17 硬锁批适配):选件变强后 probe 可能活过预算不再战败——断点段前提
# (冒险终局面在案)须主动满足。收口驱动:任何停点层逐个推进(复用主循环分支语义)至
# level_screen 选路停点→真实放弃二击→终局面。战斗中则继续 autoplay 打完。
if not _done and not main._end_overlay.is_open():
if not _done and not main.ui.modals.end_overlay.is_open():
var close_guard := 8000
var dbg_frame := 0 # T17 演播后排窗:每场胜利 +~400 帧演播等待(旧同帧 flush 零等待)——守卫按多场战斗放大
while close_guard > 0:
close_guard -= 1
await get_tree().process_frame
if main._end_overlay.is_open():
if main.ui.modals.end_overlay.is_open():
# 中途结算面(state=running)=点继续推进;真终局(settling)=断点段前提满足,收口。
if main.run_scope.is_adventure and main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running":
main._end_overlay._btn.pressed.emit()
main.ui.modals.end_overlay._btn.pressed.emit()
else:
break
elif main.curio_overlay.visible:
if main.curio_overlay._continue_btn.visible:
main.curio_overlay.press_continue()
elif main.ui.modals.curio_overlay.visible:
if main.ui.modals.curio_overlay._continue_btn.visible:
main.ui.modals.curio_overlay.press_continue()
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
main.curio_overlay.press_card(0)
main.curio_overlay.press_confirm()
main.ui.modals.curio_overlay.press_card(0)
main.ui.modals.curio_overlay.press_confirm()
else:
main.curio_overlay.press_continue()
elif main.node_overlay.visible:
main.node_overlay.press_continue()
elif main.shop_overlay.visible:
main.shop_overlay.leave_pressed.emit()
elif main._inter_overlay.visible:
var next_btn2: Button = ProbeUiUtil.find_button(main._inter_overlay, "进入下一层", "prefix")
main.ui.modals.curio_overlay.press_continue()
elif main.ui.modals.node_overlay.visible:
main.ui.modals.node_overlay.press_continue()
elif main.ui.modals.shop_overlay.visible:
main.ui.modals.shop_overlay.leave_pressed.emit()
elif main.ui.screens.inter_overlay.visible:
var next_btn2: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix")
if next_btn2 != null:
next_btn2.pressed.emit()
elif main.level_screen.visible:
var abandon_btn: Button = ProbeUiUtil.find_button(main.level_screen, "放弃", "prefix")
elif main.ui.modals.level_screen.visible:
var abandon_btn: Button = ProbeUiUtil.find_button(main.ui.modals.level_screen, "放弃", "prefix")
if abandon_btn != null:
_closed_via_abandon = true
abandon_btn.pressed.emit()
@@ -652,14 +652,14 @@ func _ready() -> void:
str(((bpf as Dictionary).get("bonus_deck", []) as Array).size()),
str((bpf as Dictionary).get("bonus_pending"))])
_problems.append("断点段:战败结算后断点未清(T3 三结局清断点失守)")
if not main._end_overlay.is_open():
if not main.ui.modals.end_overlay.is_open():
_problems.append("断点段:战败后未出冒险终局面")
if main._end_overlay._btn == null:
if main.ui.modals.end_overlay._btn == null:
_problems.append("断点段:终局面返回按钮缺失")
else:
main._end_overlay._btn.pressed.emit() # 真实按钮信号路径(_btn=继续/返回共用句柄)
main.ui.modals.end_overlay._btn.pressed.emit() # 真实按钮信号路径(_btn=继续/返回共用句柄)
await get_tree().process_frame
if not main._start_overlay.visible:
if not main.ui.screens.start_overlay.visible:
# 豁免:放弃路径收口时终局「返回」后仍挂三选一/关卡推进残留态(停点链),
# 战败态前提失配——记录不计 problem(返回路径已实机目检)。
if not _closed_via_abandon:
@@ -683,16 +683,16 @@ func _ready() -> void:
if FileAccess.file_exists(p_ol):
DirAccess.remove_absolute(p_ol)
ProbeUiUtil.isolate_save(main, "smoke_click_probe_openline", true, false, true)
main.debug_overlay.reset_sliders() # ① 白板反向前提(_start_adventure 只关窗不清值——残值跨段存活防坑)
main.debug_overlay.seed_edit.text = "42"
main.ui.modals.debug_overlay.reset_sliders() # ① 白板反向前提(_start_adventure 只关窗不清值——残值跨段存活防坑)
main.ui.modals.debug_overlay.seed_edit.text = "42"
main._start_adventure_from_menu()
var ol_l1_wait := 0
while ol_l1_wait < 24 and String(main.log_view._log.get_parsed_text()).count("· 种子 ") < 1:
while ol_l1_wait < 24 and String(main.ui.battle.log_view._log.get_parsed_text()).count("· 种子 ") < 1:
ol_l1_wait += 1
await get_tree().process_frame
if ol_l1_wait >= 24:
_problems.append("开场行路径①:L1 开场行未达日志(开局清日志后 24 帧内未现种子行)")
elif String(main.log_view._log.get_parsed_text()).count("(含面板加成)") > 0:
elif String(main.ui.battle.log_view._log.get_parsed_text()).count("(含面板加成)") > 0:
_problems.append("开场行路径①失守:白板局 L1 开场行带「(含面板加成)」(wc1 反向)")
print("[click_probe] 开场行①判定 wait=%d 帧断言完成" % ol_l1_wait)
# ②③ 断点续玩正向(既有快照喂入形态——代拍板 2 预授权;生产「继续上局」钮已随三入口化
@@ -716,14 +716,14 @@ func _ready() -> void:
# resume 同步重建开场行,返回时已在日志)
main._resume_from_menu()
var ol_r_wait := 0
while ol_r_wait < 24 and String(main.log_view._log.get_parsed_text()).count("· 种子 ") < 1:
while ol_r_wait < 24 and String(main.ui.battle.log_view._log.get_parsed_text()).count("· 种子 ") < 1:
ol_r_wait += 1
await get_tree().process_frame
if main.run_scope.runner == null or main.run_scope.runner.journal().bonus_total("atk") != 5:
_problems.append("开场行路径②③前提失守:续玩重建后加算区 atk!=5(断点回填链断)")
if ol_r_wait >= 24:
_problems.append("开场行路径②③失守:续玩重建未发开场行(选路停点形应重发——形态/驱动复核)")
elif String(main.log_view._log.get_parsed_text()).count("(含面板加成)") < 1:
elif String(main.ui.battle.log_view._log.get_parsed_text()).count("(含面板加成)") < 1:
_problems.append("开场行路径②③失守:bonus 已入账(pct=0/ov=0)的续玩重建开场行未带「(含面板加成)」(wc1 正向/wc2)")
else:
print("[click_probe] 开场行②③判定:续玩重建开场行带后缀(bonus.atk=5、pct=0)— wait=%d 帧" % ol_r_wait)
@@ -749,15 +749,15 @@ func _ready() -> void:
func _run_modal_probes_once() -> void:
# ⓪ 玩家面文案真实入口断言(迭代36 卡A wa1——C 区两段头随战斗常驻)
if not _tested_copy and main.info_panel != null and main.info_panel.visible:
if not _tested_copy and main.ui.battle.info_panel != null and main.ui.battle.info_panel.visible:
_tested_copy = true
var feat_head: Label = null
var curio_head: Label = null
for child in main.info_panel._feat_section.get_children():
for child in main.ui.battle.info_panel._feat_section.get_children():
if child is Label:
feat_head = child
break
for child in main.info_panel._curio_section.get_children():
for child in main.ui.battle.info_panel._curio_section.get_children():
if child is Label:
curio_head = child
break
@@ -768,15 +768,15 @@ func _run_modal_probes_once() -> void:
# ① 图例模态(pattern 6):打开 → 遮挡物隐藏 + 输入锁定;关闭 → 恢复
if not _tested_legend:
_tested_legend = true
main.ap_bar._btn_legend.pressed.emit()
if not main.legend_overlay.is_open():
main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit()
if not main.ui.modals.legend_overlay.is_open():
_problems.append("图例打开失败")
if main._end_overlay.is_open() or main._start_overlay.visible:
if main.ui.modals.end_overlay.is_open() or main.ui.screens.start_overlay.visible:
_problems.append("图例打开时上方遮挡层未隐藏(缺陷⑤复发)")
if not main._input_locked():
if not main.ui.modals.flow.locks_input():
_problems.append("图例打开时棋盘输入未锁定")
var legend_has_seal := false
for e in main.legend_overlay.ENTRIES:
for e in main.ui.modals.legend_overlay.ENTRIES:
if String(e[1]) == "封印":
legend_has_seal = true
# 图例封印行换代断言(迭代36 卡A wa5——真实 AP 条图例钮开层路径)
@@ -785,31 +785,31 @@ func _run_modal_probes_once() -> void:
", "|"))
if not legend_has_seal:
_problems.append("图例缺『封印』格态条目(格态图例基线)")
main.legend_overlay.close()
if main.legend_overlay.is_open() or main._input_locked():
main.ui.modals.legend_overlay.close()
if main.ui.modals.legend_overlay.is_open() or main.ui.modals.flow.locks_input():
_problems.append("图例关闭后未恢复输入/可见性")
# ②/②b 明细回看模态无探针(M9/P-C7——冒险中回看归「生涯」,06 登记)
# ⑤ 状态面板(P1 丁角标真实信号路径 + P2 甲模态语义 + P1 守卫 + R3 锁恢复):
# 角标打开 → 只读层可见+输入锁定+余额徽非空 → 「继续」关闭恢复;图例开着 → 角标置灰且点击被守卫兜住
if not _tested_status and not main.legend_overlay.is_open() and not main.status_panel.visible:
if not _tested_status and not main.ui.modals.legend_overlay.is_open() and not main.ui.modals.status_panel.visible:
_tested_status = true
var pbtn: Button = main.board_zone.status_badge
var pbtn: Button = main.ui.battle.board_zone.status_badge
if pbtn == null or not pbtn.visible or pbtn.disabled:
_problems.append("战斗屏状态角标缺失/未点亮/被置灰(P1 丁入口失守 visible=%s disabled=%s)"
% [str(pbtn != null and pbtn.visible), str(pbtn != null and pbtn.disabled)])
else:
pbtn.pressed.emit() # 真实按钮信号路径 → 组合根 _on_status_badge_pressed
if not main.status_panel.visible:
if not main.ui.modals.status_panel.visible:
_problems.append("状态面板未随角标打开(组合根接线失守)")
if not main._input_locked():
if not main.ui.modals.flow.locks_input():
_problems.append("状态面板打开时输入未锁定(R3)")
var bal_txt: String = main.status_panel._balance_badge.value_label.text
var bal_txt: String = main.ui.modals.status_panel._balance_badge.value_label.text
if not bal_txt.begins_with("钥石 "):
_problems.append("状态面板余额徽投影缺失(%s)" % bal_txt)
# ---- 本局信息面板换代断言(迭代36 卡③——真实入口三断言)----
# ① 面板子树零旧段文案(跨关概览退役零残留)
var sv_labels: Array = []
_collect_labels_by_text(main.status_panel, sv_labels)
_collect_labels_by_text(main.ui.modals.status_panel, sv_labels)
for l in sv_labels:
if String(l.text).contains("道具工坊") or String(l.text).contains("天赋树"):
_problems.append("状态面板子树残留旧段文案「%s」(sv6①——零旧段失守)" % String(l.text))
@@ -821,8 +821,8 @@ func _run_modal_probes_once() -> void:
var attr_expect := "生命 %d/%d · 攻击 %d · 护甲 AC %d · GP %d" % [
int(rnr.hero_hp), rnr.effective_max_hp(), rnr.effective_atk(),
rnr.effective_ac(), int(rnr.gp)]
if String(main.status_panel._attrs_label.text) != attr_expect:
_problems.append("状态面板属性行投影失真(sv6②——期望 %s 实测 %s)" % [attr_expect, main.status_panel._attrs_label.text])
if String(main.ui.modals.status_panel._attrs_label.text) != attr_expect:
_problems.append("状态面板属性行投影失真(sv6②——期望 %s 实测 %s)" % [attr_expect, main.ui.modals.status_panel._attrs_label.text])
# ③ _status_data() 直调对拍(sv7——键集恰八键+curios/feats 独立 oracle)
var sd: Dictionary = main._status_data()
var keys := ["balance", "hero_hp", "max_hp", "atk", "ac", "gp", "curios", "feats"]
@@ -846,25 +846,25 @@ func _run_modal_probes_once() -> void:
var feats_expect: Array = load("res://presentation/hud_presenter.gd").feat_holdings_lines(main.db, rnr.feats_held())
if str((sd as Dictionary).get("feats", [])) != str(feats_expect):
_problems.append("_status_data().feats 对拍 feat_holdings_lines 失守(sv7)")
var resume_btn: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix")
var resume_btn: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix")
if resume_btn == null:
_problems.append("状态面板关闭钮缺失(「继续」)")
else:
resume_btn.pressed.emit()
if main.status_panel.visible or main._input_locked():
if main.ui.modals.status_panel.visible or main.ui.modals.flow.locks_input():
_problems.append("状态面板关闭后未恢复(visible=%s locked=%s——R3 残留)"
% [str(main.status_panel.visible), str(main._input_locked())])
% [str(main.ui.modals.status_panel.visible), str(main.ui.modals.flow.locks_input())])
# P1 守卫:图例开着 → 两处入口角标置灰;置灰态点击被处理器兜住(探针 emit 绕过 disabled)
main.ap_bar._btn_legend.pressed.emit()
if not main.board_zone.status_badge.disabled:
main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit()
if not main.ui.battle.board_zone.status_badge.disabled:
_problems.append("图例开着时战斗屏状态角标未置灰(P1 守卫失守)")
main.board_zone.status_badge.pressed.emit()
if main.status_panel.visible:
main.ui.battle.board_zone.status_badge.pressed.emit()
if main.ui.modals.status_panel.visible:
_problems.append("置灰态仍可开状态面板(守卫兜底失守)")
main.legend_overlay.close()
main.ui.modals.legend_overlay.close()
# ③ tooltip(状态效果槽悬停——来源+语义+剩余):有槽可见时触发
if not _tested_tooltip:
var row = main.stage.hero_standee.slot_row # 状态 tooltip 载体(B 区站桩)
var row = main.ui.battle.stage.hero_standee.slot_row # 状态 tooltip 载体(B 区站桩)
for child in row.get_children():
if child is PanelContainer and child.visible and child.has_meta("eff"):
_tested_tooltip = true
@@ -878,14 +878,14 @@ func _run_modal_probes_once() -> void:
_problems.append("状态槽 tooltip 未随移出隐藏")
break
# ④ 弃图刷新确认链(pattern 7 异位:确认 chips 与常规确认不同位 + 显式代价文案)
if not _tested_refresh_chain and main.ap_bar.is_refresh_ready():
if not _tested_refresh_chain and main.ui.battle.board_zone.ap_bar.is_refresh_ready():
_tested_refresh_chain = true
var rap_before: int = int(main.run_scope.settlement.action_economy.rap)
var board_before = main.run_scope.settlement.minesweeper.current_board()
main.ap_bar.press_refresh_for_probe()
if not main.ap_bar.confirm_visible():
main.ui.battle.board_zone.ap_bar.press_refresh_for_probe()
if not main.ui.battle.board_zone.ap_bar.confirm_visible():
_problems.append("弃图刷新确认 chips 未出现(异位确认缺失)")
main.ap_bar.press_confirm_for_probe()
main.ui.battle.board_zone.ap_bar.press_confirm_for_probe()
if int(main.run_scope.settlement.action_economy.rap) != rap_before - 8:
_problems.append("弃图刷新未扣 RAP8(%d→%d)" % [rap_before, int(main.run_scope.settlement.action_economy.rap)])
if main.run_scope.settlement.action_economy.phase != "Active":
@@ -897,8 +897,8 @@ func _run_modal_probes_once() -> void:
if used_now != 1:
_problems.append("刷新轮内计数错误(%d)" % used_now)
var remain_expect: int = maxi(0, int(main.db.run.action_economy.refresh_cap_per_round) - used_now)
if not main.ap_bar.refresh_remaining_text().contains(str(remain_expect)):
_problems.append("HUD 刷新余量与快照推导失符(HUD='%s' 应含 %d)" % [main.ap_bar.refresh_remaining_text(), remain_expect])
if not main.ui.battle.board_zone.ap_bar.refresh_remaining_text().contains(str(remain_expect)):
_problems.append("HUD 刷新余量与快照推导失符(HUD='%s' 应含 %d)" % [main.ui.battle.board_zone.ap_bar.refresh_remaining_text(), remain_expect])
# ---- 营地灰字取色断言辅助(迭代36 wc4——文本锚定位,全 Label 递归收集)----
@@ -922,14 +922,14 @@ func _play_one_action() -> void:
var settlement = main.run_scope.settlement
if settlement == null:
return # 结算竞态防抖:battle_finished 与本轮动作调度同帧到达时 settlement 可能已被置空(基线既有噪声——加固)
if not main._row_queue.is_empty():
if not main.ui.screens.row_pacer.pending_rows().is_empty():
return # T1 D1——演播期不出手:此时点击只会进锁期缓存被「最后一次」覆盖(原逐帧连点语义无行为覆盖价值,且毁锁期断言注入的独占前提)
var minesweeper = settlement.minesweeper
# 行为①:随机封印(右键——未开相邻格),约 20% 轮次
if _ap_spent_this_round == 0 and _rand.randf() < 0.2:
var target := _random_unknown_adjacent(minesweeper)
if target.x >= 0:
main.board_view.click_cell(target, MOUSE_BUTTON_RIGHT)
main.ui.battle.board_zone.board_view.click_cell(target, MOUSE_BUTTON_RIGHT)
_seals_placed.append(target)
_ap_spent_this_round += 1
return
@@ -937,19 +937,19 @@ func _play_one_action() -> void:
if _rand.randf() < 0.1 and not _seals_placed.is_empty():
var fc: Vector2i = _seals_placed.pop_back()
if minesweeper.has_seal(fc) and not minesweeper.is_revealed(fc):
main.board_view.click_cell(fc, MOUSE_BUTTON_RIGHT)
main.ui.battle.board_zone.board_view.click_cell(fc, MOUSE_BUTTON_RIGHT)
_ap_spent_this_round += 1
return
# 行为③:随机无效点击(非相邻格/已开格),约 8% 轮次
if _rand.randf() < 0.08:
main.board_view.click_cell(Vector2i(_rand.randi_range(0, minesweeper.current_board().width - 1), _rand.randi_range(0, minesweeper.current_board().height - 1)), MOUSE_BUTTON_LEFT)
main.ui.battle.board_zone.board_view.click_cell(Vector2i(_rand.randi_range(0, minesweeper.current_board().width - 1), _rand.randi_range(0, minesweeper.current_board().height - 1)), MOUSE_BUTTON_LEFT)
return
# 挖掘主策略(最近可挖格)
var plan = AutoPlayer.choose_dig(minesweeper) # O4 收口:策略单一实现
if not plan.is_empty():
if plan.hero_cell != minesweeper.hero_pos():
main.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT)
main.board_view.click_cell(plan.dig_cell, MOUSE_BUTTON_LEFT)
main.ui.battle.board_zone.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT)
main.ui.battle.board_zone.board_view.click_cell(plan.dig_cell, MOUSE_BUTTON_LEFT)
_ap_spent_this_round += 1
# 行为④:提前停手(已花 ≥3 AP 时 15%)——停手经 AP 条按钮真实信号路径
if _ap_spent_this_round >= 3 and settlement.action_economy.phase == "Active" and _rand.randf() < 0.15:
@@ -975,7 +975,7 @@ func _random_unknown_adjacent(minesweeper) -> Vector2i:
func _click_stop() -> void:
# 停手走真实按钮信号路径(AP 条 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体)
main.ap_bar._btn_stop.pressed.emit()
main.ui.battle.board_zone.ap_bar._btn_stop.pressed.emit()
# ---- T1 D1:锁期/重放断言(每 run 一次,脚本化场景——演播期豁免的断言化承载) ----
@@ -992,7 +992,7 @@ func _locktest_tick() -> void:
if settlement == null or settlement.minesweeper.current_board() == null:
return
if _locktest_state == 0:
if main._row_queue.is_empty() or main._input_locked():
if main.ui.screens.row_pacer.pending_rows().is_empty() or main.ui.modals.flow.locks_input():
return
if String(settlement.battle_result) != "" or String(settlement.action_economy.phase) != "Active":
return
@@ -1003,7 +1003,7 @@ func _locktest_tick() -> void:
return # 无合法邻格/目标被并发封印行为占位——下帧重挑
_locktest_cell = target
_locktest_frames = 0
main.board_view.click_cell(target, MOUSE_BUTTON_RIGHT)
main.ui.battle.board_zone.board_view.click_cell(target, MOUSE_BUTTON_RIGHT)
if settlement.minesweeper.has_seal(target):
_problems.append("锁期断言:演播期点击未被冻结(封印即时受理——v3 冻结失守)")
_locktest_state = 2
@@ -1014,11 +1014,11 @@ func _locktest_tick() -> void:
return
_locktest_state = 1
elif _locktest_state == 1:
if main.curio_overlay.visible or main._inter_overlay.visible or main._end_overlay.is_open() \
if main.ui.modals.curio_overlay.visible or main.ui.screens.inter_overlay.visible or main.ui.modals.end_overlay.is_open() \
or String(settlement.battle_result) != "":
_locktest_state = 2 # 前提消失守卫:终结演播竞态——静默弃测
return
if not main._row_queue.is_empty():
if not main.ui.screens.row_pacer.pending_rows().is_empty():
_locktest_frames = 0 # 演播未完——重放耐心只在排空后起算(链式大演播数百帧不占预算)
return
_locktest_frames += 1
@@ -1038,7 +1038,7 @@ func _locktest_tick() -> void:
b.width, b.height, str(settlement.minesweeper.hero_pos())])
_problems.append("重放断言:锁期缓存点击未受理恰一次(封印未置位——零发或双发)")
_locktest_state = 2
main.board_view.click_cell(_locktest_cell, MOUSE_BUTTON_RIGHT) # 撤封印恢复现场(尽力而为不判定)
main.ui.battle.board_zone.board_view.click_cell(_locktest_cell, MOUSE_BUTTON_RIGHT) # 撤封印恢复现场(尽力而为不判定)
func _locktest_pick_cell(minesweeper) -> Vector2i:
+17 -17
View File
@@ -31,15 +31,15 @@ func _ready() -> void:
# 同 ui_click_probe 隔离纪律。首次冒险布尔位置位=对齐真实档游玩史口径,模板抽签路径不变;
# isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退)
ProbeUiUtil.isolate_save(main, "smoke_input_probe_save", true, false, true)
main.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,探针确定性禁依赖默认分支)
main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,探针确定性禁依赖默认分支)
main._start_adventure_from_menu()
await get_tree().process_frame
# ---- ⑫ 图上命中真实输入(回归闸):入口停点候选节点中心
# push_input 真实鼠标路径 motion+click → 悬停预承诺置位 → route_chosen 到达组合根。
# 由来=真机 bug:深层节点(L1 Boss 内容 x=802)在固定画布命中区外,press_candidate 直调
# 命中函数探针全绿、真鼠标点不中——本 case 封住输入管线,⑬封住几何拟合。
if main.level_screen.visible:
var ro = main.level_screen
if main.ui.modals.level_screen.visible:
var ro = main.ui.modals.level_screen
var cands: Array = main.run_scope.runner.next_candidate_ids()
# ---- ⑱ 信息迷雾呈现接线(回归闸):真机 display_probe 读实际绘制记录(雾下节点类型未画、
# Boss 描边画了、候选类型画了)+点击雾下非候选节点零提交(雾不扩大可点域——候选集即合法集)----
@@ -90,9 +90,9 @@ func _ready() -> void:
await get_tree().process_frame
await get_tree().process_frame
ro.route_chosen.disconnect(on_route)
if not submitted.is_empty() or not main.level_screen.visible or not main.run_scope.runner.awaiting_choice():
if not submitted.is_empty() or not main.ui.modals.level_screen.visible or not main.run_scope.runner.awaiting_choice():
_pre_fail("case⑱ 雾下节点点击产生提交(提交=%s visible=%s 选路态=%s——雾不扩大可点域)〔fv9〕"
% [str(submitted), str(main.level_screen.visible), str(main.run_scope.runner.awaiting_choice())])
% [str(submitted), str(main.ui.modals.level_screen.visible), str(main.run_scope.runner.awaiting_choice())])
return
var nds: Array = main.run_scope.runner.nodes()
var tgt := {}
@@ -116,13 +116,13 @@ func _ready() -> void:
main.get_viewport().push_input(evc, true)
await get_tree().process_frame
await get_tree().process_frame
if main.level_screen.visible or main.run_scope.runner.awaiting_choice():
_pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.level_screen.visible), str(main.run_scope.runner.awaiting_choice())])
if main.ui.modals.level_screen.visible or main.run_scope.runner.awaiting_choice():
_pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.ui.modals.level_screen.visible), str(main.run_scope.runner.awaiting_choice())])
for i in range(3):
await get_tree().process_frame
# ---- ⑬ 画布随图拟合(同 bug 的几何面回归):复刻分层公式独立推导最深节点内容 x,
# 断言画布 min 尺寸覆盖之(open_choices 每 stop 拟合——L1 单链 Boss x=70+6*122=802 曾越界)
var ro2 = main.level_screen
var ro2 = main.ui.modals.level_screen
var nds2: Array = main.run_scope.runner.nodes()
var layer2 := {}
var inbound2 := {}
@@ -155,7 +155,7 @@ func _ready() -> void:
var cvmin: Dictionary = ro2.canvas_probe()
if float(cvmin["min_size"].x) < need_x2 - 0.5:
_pre_fail("画布拟合不足(min.x=%.0f < 最深内容 %.0f——深层节点不可点回归)" % [cvmin["min_size"].x, need_x2])
var bv = main.board_view
var bv = main.ui.battle.board_zone.board_view
var ms = main.run_scope.settlement.minesweeper if main.run_scope.settlement != null else null
if ms == null or bv.cells_probe().is_empty():
_pre_fail("战斗未就绪(cells=%d settlement=%s)" % [bv.cells_probe().size(), str(main.run_scope.settlement)])
@@ -479,13 +479,13 @@ func _ready() -> void:
var p_lantern: Dictionary = main.build_adventure_params(main.save_manager, main.meta)
if not (p_lantern.get("attunement", []) as Array).has("wide_scout_band"):
_problems.append("case⑮ 装配未进开局参数面(attunement=%s 应含 wide_scout_band)" % str(p_lantern.get("attunement", [])))
main.run_scope.settlement.attunement = ["wide_scout_band"]
main.run_scope.settlement._facts.attunement = ["wide_scout_band"]
main._sync_treasure_glow()
if int(bv.fog_params_probe()["lantern"]) != 4:
_problems.append("case⑮ 装配后视野未扩(lantern 应=4——呈现桥未生效)")
if int(bv.cells_probe()[ring_cell].fog_state) != 0:
_problems.append("case⑮ 装配后 dist==R+1 格仍被遮(应转可见)")
main.run_scope.settlement.attunement = []
main.run_scope.settlement._facts.attunement = []
main._sync_treasure_glow()
if int(bv.fog_params_probe()["lantern"]) != 0:
_problems.append("case⑮ 卸装后视野未复原(lantern 应=0——契约复原语义显式重算)")
@@ -517,29 +517,29 @@ func _ready() -> void:
if status_dig.x == -9:
_problems.append("case⑯ 无可挖无封印格(板面异常——锁断言中断)")
else:
var pbtn16: Button = main.board_zone.status_badge
var pbtn16: Button = main.ui.battle.board_zone.status_badge
var bpos: Vector2 = pbtn16.get_global_rect().get_center()
_push_click(bpos, MOUSE_BUTTON_LEFT)
_push_release(bpos, MOUSE_BUTTON_LEFT) # Button pressed 信号走 release 沿——按压+释放成对
await get_tree().process_frame
if not main.status_panel.visible or not main._input_locked():
if not main.ui.modals.status_panel.visible or not main.ui.modals.flow.locks_input():
_problems.append("case⑯ 角标真点击未开状态面板(visible=%s locked=%s——入口可达性失守)"
% [str(main.status_panel.visible), str(main._input_locked())])
% [str(main.ui.modals.status_panel.visible), str(main.ui.modals.flow.locks_input())])
else:
_push_click(_pos(bv, status_dig), MOUSE_BUTTON_LEFT)
await get_tree().process_frame
await get_tree().process_frame
if _has_action(status_dig, "dig") or ms.is_revealed(status_dig):
_problems.append("case⑯ 状态面板开启时挖掘穿透(R3 输入锁破——目标=%s)" % status_dig)
var resume_btn: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix")
var resume_btn: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix")
if resume_btn == null:
_problems.append("case⑯ 状态面板关闭钮缺失")
else:
resume_btn.pressed.emit()
await get_tree().process_frame
if main.status_panel.visible or main._input_locked():
if main.ui.modals.status_panel.visible or main.ui.modals.flow.locks_input():
_problems.append("case⑯ 关闭后未恢复(visible=%s locked=%s——R3 残留)"
% [str(main.status_panel.visible), str(main._input_locked())])
% [str(main.ui.modals.status_panel.visible), str(main.ui.modals.flow.locks_input())])
_push_click(_pos(bv, status_dig), MOUSE_BUTTON_LEFT)
await get_tree().process_frame
await get_tree().process_frame
+51 -51
View File
@@ -16,7 +16,7 @@ func _ready() -> void:
await get_tree().process_frame
var problems: Array = []
# ---- 放弃入口(台账 119):默认隐藏(非冒险模式无冒险可放弃——开始层/调试两层)----
if main.level_screen.abandon_btn == null or main.level_screen.abandon_btn.visible:
if main.ui.modals.level_screen.abandon_btn == null or main.ui.modals.level_screen.abandon_btn.visible:
print("[ui_probe] FAIL — 放弃按钮默认应隐藏(非冒险模式关)")
get_tree().quit(1)
return
@@ -28,99 +28,99 @@ func _ready() -> void:
problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)")
else:
await get_tree().process_frame
if not main.camp_overlay.visible:
if not main.ui.modals.camp_overlay.visible:
problems.append("营地层未打开(进档开层新路径)")
elif main._start_overlay.visible:
elif main.ui.screens.start_overlay.visible:
problems.append("营地层打开时开始层未隐藏(规范 2.3 遮挡纪律)")
else:
if not String(main.camp_overlay._balance_label.text).begins_with("钥石"):
problems.append("营地余额文案缺失(%s)" % main.camp_overlay._balance_label.text)
if main.camp_overlay._tree_buy_btns.is_empty():
if not String(main.ui.modals.camp_overlay._balance_label.text).begins_with("钥石"):
problems.append("营地余额文案缺失(%s)" % main.ui.modals.camp_overlay._balance_label.text)
if main.ui.modals.camp_overlay._tree_buy_btns.is_empty():
problems.append("树淬炼柜台购买钮缺失(四柜台重排后树=主界面)")
else:
var btn_by_id := {}
for card2 in main.camp_overlay._tree_buy_btns:
for card2 in main.ui.modals.camp_overlay._tree_buy_btns:
btn_by_id[String(card2.node_id)] = card2.btn
if not btn_by_id.has("sp_hp") or bool(btn_by_id["sp_hp"].disabled):
problems.append("脊柱生命 L1 白板可购钮未启用(无门槛——can_buy 不含余额面语义)")
if not btn_by_id.has("att_socket2") or not bool(btn_by_id["att_socket2"].disabled):
problems.append("同调位 2 前置未亮应禁用(点亮前置语义)")
if main.camp_overlay._workshop_rows.size() != 12:
problems.append("秘宝匣柜台行数错误(%d ≠ 12——迭代 22 秘宝批换代)" % main.camp_overlay._workshop_rows.size())
if main.ui.modals.camp_overlay._workshop_rows.size() != 12:
problems.append("秘宝匣柜台行数错误(%d ≠ 12——迭代 22 秘宝批换代)" % main.ui.modals.camp_overlay._workshop_rows.size())
main._on_camp_closed()
await get_tree().process_frame
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点)
await get_tree().process_frame
if main.camp_overlay.visible or not main._start_overlay.visible:
if main.ui.modals.camp_overlay.visible or not main.ui.screens.start_overlay.visible:
problems.append("营地关闭后未恢复开始层(开合往返失守)")
# 换回全新零档(营地段购买态不泄入后续冒险段——确定性;先删后建防陈旧档泄入)
ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", true, false, true)
main.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机;本探针 start_single 第五路同吃此框,确定性禁依赖默认分支)
main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机;本探针 start_single 第五路同吃此框,确定性禁依赖默认分支)
main._start_adventure_from_menu()
await get_tree().process_frame
await get_tree().process_frame
# ---- 入口选路停点(关卡开始即出节点图——level_started 后、首战前弹选路层;
# press_candidate 图上命中路径 pick 入口单候选进首战)----
# ---- 独立性断言(独立屏激活期战斗屏宿主整体隐藏,零透出)----
if not main.level_screen.visible:
if not main.ui.modals.level_screen.visible:
problems.append("关卡开始未弹入口选路停点(level_screen 应可见)")
elif main._battle_root.visible:
elif main.ui.screens.battle_root.visible:
problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)")
elif main.run_scope.runner == null or not main.run_scope.runner.awaiting_choice() \
or main.run_scope.runner.next_candidate_ids().size() != 1:
problems.append("入口停点状态失守(选路态 + 入口单候选)")
else:
main.level_screen.press_candidate(0)
main.ui.modals.level_screen.press_candidate(0)
await get_tree().process_frame
await get_tree().process_frame
if not main._battle_root.visible or main.level_screen.visible:
if not main.ui.screens.battle_root.visible or main.ui.modals.level_screen.visible:
problems.append("选路提交战斗节点后未切回战斗屏(屏级互斥恢复失守)")
# ---- 放弃入口·冒险模式可见 + 战斗中按下被拒(真实按钮路径——入口挂选路停点,战斗中拒绝为防御分支)----
if main.level_screen.abandon_btn == null or not main.level_screen.abandon_btn.visible:
if main.ui.modals.level_screen.abandon_btn == null or not main.ui.modals.level_screen.abandon_btn.visible:
problems.append("冒险模式放弃按钮未开放(选路停点入口在案)")
else:
main.level_screen.abandon_btn.pressed.emit()
main.ui.modals.level_screen.abandon_btn.pressed.emit()
await get_tree().process_frame
if main.run_scope.adventure == null or String(main.run_scope.adventure.state) != "running":
problems.append("首战中放弃未被拒(战斗域单事务在飞——state 应保持 running)")
# ---- 既有断言(回归基线;A 区无进度条——节点链进度由关卡画面独立屏承载)----
if main.board_view.cells_probe().is_empty():
if main.ui.battle.board_zone.board_view.cells_probe().is_empty():
problems.append("棋盘格未生成(cells=0)")
if main.board_view.board() == null:
if main.ui.battle.board_zone.board_view.board() == null:
problems.append("board 未装载")
if main.info_panel.attack_text().contains("—") or main.info_panel.armor_text().contains("—"):
problems.append("C 区攻击/护甲未显示(%s | %s)" % [main.info_panel.attack_text(), main.info_panel.armor_text()])
if main.ui.battle.info_panel.attack_text().contains("—") or main.ui.battle.info_panel.armor_text().contains("—"):
problems.append("C 区攻击/护甲未显示(%s | %s)" % [main.ui.battle.info_panel.attack_text(), main.ui.battle.info_panel.armor_text()])
if main.run_scope.settlement == null or main.run_scope.settlement.battle_result != "":
problems.append("首战未处于进行中")
if not main.log_view.parsed_text().contains("节点 1/"):
if not main.ui.battle.log_view.parsed_text().contains("节点 1/"):
problems.append("节点头日志缺失")
# ---- 四区布局断言(battle.md §Layout)----
if not main.stage.round_label.text.contains("第 1 轮"):
problems.append("B 区轮次·阶段指示缺失(%s)" % main.stage.round_label.text)
if main.stage.enemy_box.get_child_count() < 1:
problems.append("B 区敌方站桩未构建(%d 个)" % main.stage.enemy_box.get_child_count())
if main.ap_bar._dots.get_child_count() != int(main.db.run.action_economy.ap_per_round):
problems.append("D 区 AP 点阵数量错误(%d)" % main.ap_bar._dots.get_child_count())
if main.ap_bar.refresh_remaining_text() == "":
if not main.ui.battle.stage.round_label.text.contains("第 1 轮"):
problems.append("B 区轮次·阶段指示缺失(%s)" % main.ui.battle.stage.round_label.text)
if main.ui.battle.stage.enemy_box.get_child_count() < 1:
problems.append("B 区敌方站桩未构建(%d 个)" % main.ui.battle.stage.enemy_box.get_child_count())
if main.ui.battle.board_zone.ap_bar._dots.get_child_count() != int(main.db.run.action_economy.ap_per_round):
problems.append("D 区 AP 点阵数量错误(%d)" % main.ui.battle.board_zone.ap_bar._dots.get_child_count())
if main.ui.battle.board_zone.ap_bar.refresh_remaining_text() == "":
problems.append("D 区弃图刷新按钮文案缺失")
if main.board_zone.toast == null:
if main.ui.battle.board_zone.toast == null:
problems.append("D 区 toast 槽缺失")
if main.info_panel.item_slots == null or main.info_panel.item_slots.get_child_count() != 4:
if main.ui.battle.info_panel.item_slots == null or main.ui.battle.info_panel.item_slots.get_child_count() != 4:
problems.append("C 区秘宝槽(预留)缺失")
if main.stage.hero_standee.slot_row == null:
if main.ui.battle.stage.hero_standee.slot_row == null:
problems.append("B 区站桩状态行缺失(状态效果唯一展示面——2026-09-26 C 区精简批)")
# B 区明细带挖掘期收起断言(T17 视觉扩展批适配):结算演播期 settling 视觉延伸=mode=settle
# 属新语义(演播放完回 dig——main._is_settling 排播项,判据=纯队列),本断言仅在排播空闲态判定。
if main._row_queue.is_empty() \
and (main.log_view.mode() != "dig" or main.log_view.custom_minimum_size.y != 24.0):
problems.append("B 区明细带挖掘期应收起 24px(mode=%s h=%s)" % [main.log_view.mode(), str(main.log_view.custom_minimum_size.y)])
if main.ui.screens.row_pacer.pending_rows().is_empty() \
and (main.ui.battle.log_view.mode() != "dig" or main.ui.battle.log_view.custom_minimum_size.y != 24.0):
problems.append("B 区明细带挖掘期应收起 24px(mode=%s h=%s)" % [main.ui.battle.log_view.mode(), str(main.ui.battle.log_view.custom_minimum_size.y)])
# E 区无道具面板——四区构建由其余断言覆盖
# ---- 敌方站桩投影(顺位徽标/意图三档动作动画占位——敌人系统×4,全站桩)----
# B 区站桩子件伴随件化(壳层 tooltip 载体)——断言口径见台账 171:
# 伴随件化(壳层 tooltip 载体),穿透明(子件 _name_label/_armor_icon/_intent_badge)改本体
# 探针出口 stage.enemy_body_at(i).name_line_probe()/intent_probe()——呈现口径逐字保持
var standee = main.stage.enemy_body_at(0)
var standee = main.ui.battle.stage.enemy_body_at(0)
if standee == null:
problems.append("敌立绘本体缺失(enemy_body_at(0) 无世界站桩)")
else:
@@ -138,12 +138,12 @@ func _ready() -> void:
# 站桩纹理非空断言(八敌真图落位;缺位回退链保持的对照面)
if not bool(standee.portrait_probe().get("has_tex", false)):
problems.append("敌立绘纹理未落位(素材管线 48px 档——迭代 6 T2;缺位回退链不应在此态触发)")
if main.stage.hero_standee == null or not main.stage.hero_standee.has_world_hp_bar:
if main.ui.battle.stage.hero_standee == null or not main.ui.battle.stage.hero_standee.has_world_hp_bar:
problems.append("B 区主角站桩缺失(世界血条面缺失)")
elif not bool(main.stage.hero_standee.portrait_probe().get("has_tex", false)):
elif not bool(main.ui.battle.stage.hero_standee.portrait_probe().get("has_tex", false)):
problems.append("主角立绘纹理未落位(48px 档——迭代 6 T2/T3)")
# ---- BUG-0001 回归(tooltip 入树后 set_effects 槽位不错位——结算软锁修复沉淀)----
var slots_row = main.stage.hero_standee.slot_row # BUG-0001 回归随组件迁入站桩(tooltip/槽位错位同源)
var slots_row = main.ui.battle.stage.hero_standee.slot_row # BUG-0001 回归随组件迁入站桩(tooltip/槽位错位同源)
var probe_eff: Array = [{"effect_id": "attack_up", "display_name": "增伤", "layers": 1,
"remaining_settlements": 1, "source_class": "safe_cell", "round_index": 1, "effective_value": 50}]
slots_row.set_effects(probe_eff)
@@ -196,12 +196,12 @@ func _ready() -> void:
main._debug.start_single(2)
await get_tree().process_frame
await get_tree().process_frame
if main.board_view.board() == null:
if main.ui.battle.board_zone.board_view.board() == null:
problems.append("L2 单场未启动(board=null)")
else:
# 地形变异:L2 板允许带变异(岩浆/烬覆)——布置域断言 =
# 变异格全在初始展开区外(closed_only 结构保障)
var b0 = main.board_view.board()
var b0 = main.ui.battle.board_zone.board_view.board()
var r0p := {}
for c in b0.compute_r0():
r0p[c] = true
@@ -209,8 +209,8 @@ func _ready() -> void:
if r0p.has(c):
problems.append("L2 板变异格落在初始展开区(地形变异 布置域违例)")
break
main.board_view.set_desaturated(true)
main.board_view.set_desaturated(false)
main.ui.battle.board_zone.board_view.set_desaturated(true)
main.ui.battle.board_zone.board_view.set_desaturated(false)
# ---- GP 终局行:summary gp_final 键取数+胜利面板 GP 行呈现(C5:GP 行只上冒险终局面板;
# 断言面 = summary 键,无影子账清零转存)----
# (置于战斗态断言之后——合成收束会灰化站桩并清 settlement 引用,具污染性)
@@ -223,17 +223,17 @@ func _ready() -> void:
main._on_adventure_finished({"result": "completed", "cleared": true, "levels_cleared": 3,
"stopped_at": 0, "keystone_total": 30, "master_seed": 42, "gp_final": 57})
main.meta.save = meta_saved
if not main._end_overlay.is_open():
if not main.ui.modals.end_overlay.is_open():
problems.append("冒险终局面板未开(合成摘要路径)")
elif not main._end_overlay._gp_row.visible or not main._end_overlay._gp_val.text.contains("57"):
problems.append("终局面板 GP 行缺失或取数不对(%s)" % main._end_overlay._gp_val.text)
main._end_overlay.close()
main._close_modal(main._end_overlay)
elif not main.ui.modals.end_overlay._gp_row.visible or not main.ui.modals.end_overlay._gp_val.text.contains("57"):
problems.append("终局面板 GP 行缺失或取数不对(%s)" % main.ui.modals.end_overlay._gp_val.text)
main.ui.modals.end_overlay.close()
main.ui.modals.flow.close(main.ui.modals.end_overlay)
if problems.is_empty():
print("[ui_probe] PASS — cells=%d board=%dx%d hp='%s' enemies=%d ap_dots=%d" % [
main.board_view.cells_probe().size(), main.board_view.board().width, main.board_view.board().height,
main.info_panel.attack_text(),
main.stage.enemy_box.get_child_count(), main.ap_bar._dots.get_child_count()])
main.ui.battle.board_zone.board_view.cells_probe().size(), main.ui.battle.board_zone.board_view.board().width, main.ui.battle.board_zone.board_view.board().height,
main.ui.battle.info_panel.attack_text(),
main.ui.battle.stage.enemy_box.get_child_count(), main.ui.battle.board_zone.ap_bar._dots.get_child_count()])
get_tree().quit(0)
else:
for p in problems:
+68 -68
View File
@@ -28,13 +28,13 @@ func _ready() -> void:
# ---- 菜单簇(开始层可见态;逐屏开→采样→关,模态恢复走组合根真实路径)----
ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_menu", true, false, true)
_settle()
await _audit("start", [main._start_overlay])
await _audit("start", [main.ui.screens.start_overlay])
_start_columns_contract(main)
_modal_registry_contract(main)
main._open_settings()
_settle()
await _audit("settings", [main.settings_overlay])
await _audit("settings", [main.ui.modals.settings_overlay])
main._on_settings_closed()
_settle()
@@ -42,7 +42,7 @@ func _ready() -> void:
problems.append("[camp] 营地入口缺失(选档层新档钮——mn9 新路径驱动中断)")
else:
_settle()
await _audit("camp", [main.camp_overlay])
await _audit("camp", [main.ui.modals.camp_overlay])
# 营地灰字源 token 扫描(迭代36 wc4 静态半——源内零裸 UiKit.STONE;判别式=
# STONE 出现次数 == STONE_DEEP 出现次数,即每处 STONE 都带 _DEEP 后缀)
var camp_src := FileAccess.get_file_as_string("res://presentation/views/camp_overlay_view.gd")
@@ -59,15 +59,15 @@ func _ready() -> void:
# 生涯页驱动=函数直调中间态(卡② mn9/mn10——careerbtn 已撤,卡④ 营地笔记本改挂前无 UI 路径)
main._open_career()
_settle()
if not main.career_overlay.visible or main._start_overlay.visible:
if not main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible:
problems.append("[career] 生涯页未打开或开始层未隐藏(函数直调中间态断裂)")
await _audit("career", [main.career_overlay])
await _audit("career", [main.ui.modals.career_overlay])
_career_readonly_contract(main)
_career_zero_motion_contract(main)
_career_nojump_contract(main)
main._on_career_closed()
_settle()
if main.career_overlay.visible or not main._start_overlay.visible:
if main.ui.modals.career_overlay.visible or not main.ui.screens.start_overlay.visible:
problems.append("[career] 关闭后页未隐藏或开始层未恢复(模态栈单口失守)")
# ---- 选档界面(迭代 33 卡③):函数直调开层(「进入存档」钮归卡② mn3——点击链在卡② 承接),
@@ -93,39 +93,39 @@ func _ready() -> void:
problems.append("[savesel] 开层前激活态非零(sl9 重启链——boot/注入面后应无激活)")
main._open_save_select()
_settle()
if not main.save_select_overlay.visible or main._start_overlay.visible:
if not main.ui.modals.save_select_overlay.visible or main.ui.screens.start_overlay.visible:
problems.append("[savesel] 选档层未打开或开始层未隐藏(开层函数断裂)")
await _audit("savesel", [main.save_select_overlay])
await _audit("savesel", [main.ui.modals.save_select_overlay])
_save_select_contract(main, ssel_tpl)
main._on_save_select_closed()
_settle()
if main.save_select_overlay.visible or not main._start_overlay.visible:
if main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
problems.append("[savesel] 关闭后层未隐藏或开始层未恢复(模态栈单口失守)")
main.confirm_dialog.open("结构探针采样", "确认")
main.ui.modals.confirm_dialog.open("结构探针采样", "确认")
_settle()
await _audit("confirm", [main.confirm_dialog])
main.confirm_dialog.close()
await _audit("confirm", [main.ui.modals.confirm_dialog])
main.ui.modals.confirm_dialog.close()
_settle()
main._show_debug_overlay()
_settle()
await _audit("debug", [main.debug_overlay])
await _audit("debug", [main.ui.modals.debug_overlay])
main._close_debug_overlay()
_settle()
# ---- 冒险簇(真流推进:开冒险→地图选路态→选战斗节点→战斗 HUD)----
ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_adv", true, false, true)
main.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,结构采样确定性禁依赖默认分支)
main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,结构采样确定性禁依赖默认分支)
main._start_adventure_from_menu()
_settle(4)
await _audit("level_map", [main.level_screen])
await _audit("level_map", [main.ui.modals.level_screen])
var battle_id := _pick_battle_node(main)
if battle_id >= 0:
main.level_screen.route_chosen.emit(battle_id)
main.ui.modals.level_screen.route_chosen.emit(battle_id)
_settle(6)
_audit("battle_hud", [main.board_zone, main.info_panel, main.stage])
_audit("battle_hud", [main.ui.battle.board_zone, main.ui.battle.info_panel, main.ui.battle.stage])
_standeefit_contract(main)
# ---- 状态面板:组合根真实路径开层→结构断言→关闭。附只读性契约
#(R4——子树信号连接白名单巡检)与零动效契约(R5/P4 甲——零 AnimationPlayer/零 tween)。
@@ -133,74 +133,74 @@ func _ready() -> void:
# 一律红,须先入册审查);白名单={视图自身 _emit_closed, 组合根 _on_status_closed}。
main._open_status_panel()
_settle()
await _audit("status", [main.status_panel])
await _audit("status", [main.ui.modals.status_panel])
_status_readonly_contract(main)
_status_zero_motion_contract(main)
main._on_status_closed()
_settle()
if main.status_panel.visible:
if main.ui.modals.status_panel.visible:
problems.append("[status] 关闭后层未隐藏(模态栈单口失守)")
# ---- 图例层(旧模态、首过结构族):ap_bar legend_toggled
# 信号真路径开(board_zone 信号→组合根 _on_legend_toggled→_open_modal)→结构断言族→
# closed 信号真路径关(close()→组合根 _on_modal_closed)。
main.board_zone.legend_toggled.emit()
main.ui.battle.board_zone.legend_toggled.emit()
_settle()
if not main.legend_overlay.visible:
if not main.ui.modals.legend_overlay.visible:
problems.append("[legend] 图例层未打开(legend_toggled 路径中断)")
await _audit("legend", [main.legend_overlay])
main.legend_overlay.close()
await _audit("legend", [main.ui.modals.legend_overlay])
main.ui.modals.legend_overlay.close()
_settle()
if main.legend_overlay.visible:
if main.ui.modals.legend_overlay.visible:
problems.append("[legend] 关闭后层未隐藏(模态栈单口失守)")
else:
problems.append("[battle_hud] 候选中无战斗节点(驱动中断——候选=%s)" % str(main.level_screen._cand_ids))
problems.append("[battle_hud] 候选中无战斗节点(驱动中断——候选=%s)" % str(main.ui.modals.level_screen._cand_ids))
# ---- 兑付屏(真实 open 面 + 合成最小数据——结构断言只看矩形/宽度/可见性)----
main._end_overlay.open_victory("结构探针·胜利演算", "返回", Rect2(200, 200, 400, 300), {"gp_summary": "GP 0"})
main.ui.modals.end_overlay.open_victory("结构探针·胜利演算", "返回", Rect2(200, 200, 400, 300), {"gp_summary": "GP 0"})
_settle()
await _audit("battle_end", [main._end_overlay])
main._end_overlay.close()
await _audit("battle_end", [main.ui.modals.end_overlay])
main.ui.modals.end_overlay.close()
_settle()
# ---- 节点结算层(旧模态、首过结构族):rest 兑付真路径开
#(组合根 _open_modal 登记册+show_rest 合成喂入)→结构断言族→continue_pressed 信号真路径关
#(press_continue→组合根 _on_node_continue→_close_modal)。
main._open_modal(main.node_overlay)
main.node_overlay.show_rest(3, 15, 20)
main.ui.modals.flow.open(main.ui.modals.node_overlay)
main.ui.modals.node_overlay.show_rest(3, 15, 20)
_settle()
if not main.node_overlay.visible:
if not main.ui.modals.node_overlay.visible:
problems.append("[node] 节点结算层未打开(rest 兑付路径中断)")
await _audit("node_overlay", [main.node_overlay])
main.node_overlay.press_continue()
await _audit("node_overlay", [main.ui.modals.node_overlay])
main.ui.modals.node_overlay.press_continue()
_settle()
if main.node_overlay.visible:
if main.ui.modals.node_overlay.visible:
problems.append("[node] 关闭后层未隐藏(continue_pressed 路径中断)")
main.curio_overlay.open_pick("结构探针·三选一", [
main.ui.modals.curio_overlay.open_pick("结构探针·三选一", [
{"id": "probe_heal", "text": "回血\n恢复 3 点"},
{"id": "probe_magic", "text": "爆裂轰击\n下一击 +5"},
{"id": "probe_pierce", "text": "穿甲\n无视护甲"},
])
_settle()
await _audit("curio_pick", [main.curio_overlay])
main.curio_overlay.visible = false
await _audit("curio_pick", [main.ui.modals.curio_overlay])
main.ui.modals.curio_overlay.visible = false
_settle()
main.shop_overlay.open_shop([
main.ui.modals.shop_overlay.open_shop([
{"product_id": "probe_p1", "display_name": "探针商品甲", "attr": "atk", "flat_value": 1, "price": 10, "purchased": 0},
{"product_id": "probe_p2", "display_name": "探针商品乙", "attr": "hp", "flat_value": 2, "price": 20, "purchased": 0},
], 100)
_settle()
await _audit("shop", [main.shop_overlay])
await _audit("shop", [main.ui.modals.shop_overlay])
# ---- 术语传导:货架卡与三选一卡片的玩家面文案(物品名=专长获取、分类=专长凭证)
main.shop_overlay.open_shelf([
main.ui.modals.shop_overlay.open_shelf([
{"kind_id": "feat_ticket", "display_name": "专长获取", "value": 0, "price": 5},
{"kind_id": "heal", "display_name": "应急回复", "value": 6, "price": 5},
], 20)
_settle()
var ticket_row: Dictionary = main.shop_overlay._shelf_rows[0]
var ticket_row: Dictionary = main.ui.modals.shop_overlay._shelf_rows[0]
if String(main.db.run.shop.pool_row("feat_ticket").get("display_name", "")) != "专长获取":
problems.append("[shop_term] shop.tres 专长凭证件名未换代(现=%s)" % String(main.db.run.shop.pool_row("feat_ticket").get("display_name", "")))
if ticket_row.card.name_l.text != "专长获取":
@@ -211,7 +211,7 @@ func _ready() -> void:
problems.append("[shop_term] 专长凭证件描述未用规范流程句(现=%s)" % ticket_row.card.desc_l.text)
if ticket_row.card.foot_l.text != "专长凭证":
problems.append("[shop_term] 专长凭证件分类标注未用「专长凭证」(现=%s)" % ticket_row.card.foot_l.text)
var heal_row: Dictionary = main.shop_overlay._shelf_rows[1]
var heal_row: Dictionary = main.ui.modals.shop_overlay._shelf_rows[1]
if heal_row.card.desc_l.text != "回复 +6(本冒险)":
problems.append("[shop_term] 回复件描述漏了 heal 词表(现=%s)" % heal_row.card.desc_l.text)
if heal_row.card.foot_l.text != "回复":
@@ -223,7 +223,7 @@ func _ready() -> void:
problems.append("[curio_term] 专长凭证三选一卡未用物品名/规范流程句(现=%s)" % ticket_text)
if ticket_text.contains("门票") or ticket_text.contains("T5") or ticket_text.contains("持有集") or ticket_text.contains("奇物三选一"):
problems.append("[curio_term] 专长凭证三选一卡残留退役词/内部任务号(现=%s)" % ticket_text)
main.shop_overlay.visible = false
main.ui.modals.shop_overlay.visible = false
_settle()
# ---- ⑦resume 专项:续玩停点恢复(main.gd:717 报错回归闸)----
@@ -234,11 +234,11 @@ func _ready() -> void:
main.run_scope.runner = null # 全新启动等价态(真机=菜单直进续玩,无前置冒险——runner 从未赋值;探针此前推流会残留旧 runner)
main._resume_from_menu()
_settle(8)
if not main._end_overlay.visible:
if not main.ui.modals.end_overlay.visible:
problems.append("[resume] 续玩恢复后胜利总结面未开(三选一停点分支中断)")
else:
var found := {"ok": false}
_find_label_text(main._end_overlay, "合计 24", found)
_find_label_text(main.ui.modals.end_overlay, "合计 24", found)
if not found.ok:
problems.append("[resume] 胜利总结面 GP 行未见快照合计「合计 24」(runner 缺位守卫缺位——717 回归)")
var s: String = main._gp_summary_text({"gp_dropped": 0})
@@ -280,7 +280,7 @@ func _find_label_text(node: Node, needle: String, found: Dictionary) -> void:
func _pick_battle_node(main: Node) -> int:
# 入口/兑付停点走 level_screen.open_choices 直传(不写 _pending_candidates)——
# 候选从视图态取:_cand_ids × runner.nodes() 的 type(编号解析=node_id_of 单一出口)
var ls: Node = main.level_screen
var ls: Node = main.ui.modals.level_screen
if ls._map._runner == null:
return -1
for nd in ls._map._runner.nodes():
@@ -306,7 +306,7 @@ const _PAUSE_READONLY_SIGNALS: Array[String] = [
func _status_readonly_contract(main: Node) -> void:
var overlay: Control = main.status_panel
var overlay: Control = main.ui.modals.status_panel
var stack: Array = [overlay]
var conn_count := 0
while not stack.is_empty():
@@ -332,7 +332,7 @@ func _status_readonly_contract(main: Node) -> void:
# ⑨status 零动效契约(R5/P4 甲——零动效直开直关路径存在断言):
# 子树零 AnimationPlayer + 视图源码零 tween/动效包装调用(源码 token 扫描=ui_contrast 静态段同手法)
func _status_zero_motion_contract(main: Node) -> void:
var overlay: Control = main.status_panel
var overlay: Control = main.ui.modals.status_panel
var stack: Array = [overlay]
while not stack.is_empty():
var n: Node = stack.pop_back()
@@ -352,7 +352,7 @@ func _status_zero_motion_contract(main: Node) -> void:
# 清单外零连接。白名单={视图自身 _emit_closed〔返回钮〕, 组合根 _on_career_closed};页内任何
# 购买/导航/写入方向的新连接一律红,须先入册审查。
func _career_readonly_contract(main: Node) -> void:
var overlay: Control = main.career_overlay
var overlay: Control = main.ui.modals.career_overlay
var stack: Array = [overlay]
var conn_count := 0
while not stack.is_empty():
@@ -377,7 +377,7 @@ func _career_readonly_contract(main: Node) -> void:
# ⑪career 零动效契约(状态面板 ⑨ 同形制——P4 甲直开直关)
func _career_zero_motion_contract(main: Node) -> void:
var overlay: Control = main.career_overlay
var overlay: Control = main.ui.modals.career_overlay
var stack: Array = [overlay]
while not stack.is_empty():
var n: Node = stack.pop_back()
@@ -392,13 +392,13 @@ func _career_zero_motion_contract(main: Node) -> void:
# ⑫模态登记/树序全量合同(V1——career 专属登记序契约推广到全部模态+屏壳;
# 判据=main.LAYER_CONTRACT 单一事实源:登记册序==合同模态序、直接子层树序==合同序逐对相等。
# 判据=main.ui.LAYER_CONTRACT 单一事实源:登记册序==合同模态序、直接子层树序==合同序逐对相等。
# 两特例按合同数据表达:level_screen 宿住冒险屏壳(树序随宿主=全模态之下,遮挡语义归模态栈
# screen_state 登记)、camp_overlay 成员与 camp_screen 屏壳同槽(屏壳树序代言成员登记位)。
# _career_registry_contract 的断言(career=状态面板后/确认弹窗前、树序同步)由本全量合同覆盖)
# ⑭选档界面契约(卡③ sl1/sl2/sl3/sl4/sl9——呈现面全部来自喂入数据,视图零自判)
func _save_select_contract(main: Node, tpl: String) -> void:
var sso: Node = main.save_select_overlay
var sso: Node = main.ui.modals.save_select_overlay
# sl1 三槽呈现:槽卡计数==3+零滚动容器(禁滚动长列表形态——契约 1)
var rows: VBoxContainer = sso._rows_box
var live_rows := 0
@@ -477,13 +477,13 @@ func _collect_labels(node: Node, out: Array[String]) -> void:
func _modal_registry_contract(main: Node) -> void:
var list: Array = main._modals._list
var list: Array = main.ui.modals.flow.stack._list
# ① 登记册序==合同模态序(逐位点名——合同是登记序唯一事实源)
var contract_modals: Array = []
for entry: Dictionary in main.LAYER_CONTRACT:
for entry: Dictionary in main.ui.LAYER_CONTRACT:
if String(entry["kind"]) != "modal":
continue
var n: Node = main._layer_node(String(entry["key"]))
var n: Node = main.ui.node(String(entry["key"]))
if n == null:
problems.append("[registry] 合同层缺失: %s〔装配生命批 V1〕" % String(entry["key"]))
continue
@@ -498,10 +498,10 @@ func _modal_registry_contract(main: Node) -> void:
i, list[i].name, contract_modals[i].name])
# ② 直接子层树序==合同序逐对相等(屏壳/模态/全屏态/非视觉节点全链;宿住层随宿主不参与)
var prev: Node = null
for entry: Dictionary in main.LAYER_CONTRACT:
for entry: Dictionary in main.ui.LAYER_CONTRACT:
if entry.has("host"):
continue
var node: Node = main._layer_node(String(entry["shell"]) if entry.has("shell") else String(entry["key"]))
var node: Node = main.ui.node(String(entry["shell"]) if entry.has("shell") else String(entry["key"]))
if node == null:
problems.append("[registry] 合同层缺失: %s〔装配生命批 V1〕" % String(entry["key"]))
continue
@@ -510,23 +510,23 @@ func _modal_registry_contract(main: Node) -> void:
prev.name, node.name, node.get_index(), prev.get_index()])
prev = node
# ③ 宿住层特例:level_screen 宿主=冒险屏壳且在全直接子模态之下(遮挡语义归模态栈 screen_state)
var lv: Node = main.level_screen
if lv.get_parent() != main._adventure_screen:
var lv: Node = main.ui.modals.level_screen
if lv.get_parent() != main.ui.screens.adventure_screen:
problems.append("[registry] level_screen 宿主应=冒险屏壳(实测 %s)〔装配生命批 V1〕" % lv.get_parent().name)
elif lv.get_parent().get_index() >= main.legend_overlay.get_index():
elif lv.get_parent().get_index() >= main.ui.modals.legend_overlay.get_index():
problems.append("[registry] 冒险屏壳应在全模态之下(宿主 idx=%d legend idx=%d)〔装配生命批 V1〕" % [
lv.get_parent().get_index(), main.legend_overlay.get_index()])
lv.get_parent().get_index(), main.ui.modals.legend_overlay.get_index()])
# ④ 营地同槽特例:camp_screen 屏壳树序夹商店层与设置层之间(成员 camp_overlay 登记位代言)
var cs: Node = main._camp_screen
if cs.get_index() <= main.shop_overlay.get_index() or cs.get_index() >= main.settings_overlay.get_index():
var cs: Node = main.ui.screens.camp_screen
if cs.get_index() <= main.ui.modals.shop_overlay.get_index() or cs.get_index() >= main.ui.modals.settings_overlay.get_index():
problems.append("[registry] camp_screen 屏壳树序应夹商店与设置层之间(idx=%d shop=%d settings=%d)〔装配生命批 V1〕" % [
cs.get_index(), main.shop_overlay.get_index(), main.settings_overlay.get_index()])
cs.get_index(), main.ui.modals.shop_overlay.get_index(), main.ui.modals.settings_overlay.get_index()])
# ⑬career 零跳转契约(提纲 P4 甲——页内零「前往他处」入口:除「返回」外任何导航钮=红)
func _career_nojump_contract(main: Node) -> void:
for nav in ["营地", "商店", "设置", "冒险", "继续上局", "退出", "前往"]:
if ProbeUiUtil.find_button(main.career_overlay, nav, "contains") != null:
if ProbeUiUtil.find_button(main.ui.modals.career_overlay, nav, "contains") != null:
problems.append("[career][nojump] 页内出现导航入口「%s」(零跳转违约——生涯纯只读)" % nav)
@@ -534,7 +534,7 @@ func _career_nojump_contract(main: Node) -> void:
# (名行基线离盒底 1px 同型高危写法返修回归闸;
# 内容矩形=StandeeBody.content_rect(),最低绘制元素=底座下缘)
func _standeefit_contract(main: Node) -> void:
var st: Node = main.stage
var st: Node = main.ui.battle.stage
if st == null or st.hero_standee == null:
problems.append("[standee] 舞台/主角站桩缺失(断言中断)")
return
@@ -711,8 +711,8 @@ func _dump(c: Control, depth: int) -> void:
# ⑤start 专项:左列/右列矩形不相交(M2 双列契约——装配件命名 start_box/start_right 为探针锚点)
func _start_columns_contract(main: Node) -> void:
var box: Control = main._start_overlay.find_child("start_box", true, false)
var right: Control = main._start_overlay.find_child("start_right", true, false)
var box: Control = main.ui.screens.start_overlay.find_child("start_box", true, false)
var right: Control = main.ui.screens.start_overlay.find_child("start_right", true, false)
if box == null or right == null:
problems.append("[start][columns] 左列/右列节点缺失(start_box/start_right——装配命名失守)")
return
+41 -39
View File
@@ -43,35 +43,35 @@ func _ready() -> void:
# ① 全局浮层角标开窗(调试入口=UI 最顶层浮层,任何 UI 不遮挡——
# main 直接子层、LAYER_CONTRACT 末位 float 类;boot 菜单优先不开浮窗,角标真实点击路径开窗;
# 悬浮于菜单之上——开始层不隐藏;树序新基准=登记册末位(确认弹窗之后=绘制序最上))
main.debug_badge.pressed.emit()
main.ui.screens.debug_badge.pressed.emit()
await get_tree().process_frame
if not main.debug_overlay.visible:
if not main.ui.modals.debug_overlay.visible:
problems.append("调试浮层角标点击后浮窗未开(全局浮层入口失守)")
if not main._start_overlay.visible:
if not main.ui.screens.start_overlay.visible:
problems.append("开始覆盖层被隐藏——浮窗应悬浮于菜单之上(14.5 形态失守)")
if main.debug_overlay.get_index() < main.confirm_dialog.get_index():
if main.ui.modals.debug_overlay.get_index() < main.ui.modals.confirm_dialog.get_index():
problems.append("调试浮窗树序不在确认弹窗之后——登记册末位失守")
# ② 滑条 → 生效预览联动
main.debug_overlay.set_sliders(100, 50)
if str(boosted_atk) not in main.debug_overlay.effect_text() or str(boosted_hp) not in main.debug_overlay.effect_text():
problems.append("生效预览未联动(%s)" % main.debug_overlay.effect_text())
main.ui.modals.debug_overlay.set_sliders(100, 50)
if str(boosted_atk) not in main.ui.modals.debug_overlay.effect_text() or str(boosted_hp) not in main.ui.modals.debug_overlay.effect_text():
problems.append("生效预览未联动(%s)" % main.ui.modals.debug_overlay.effect_text())
# ③ 应用(真实信号路径:apply_requested → 组合根)——E4 后加成零表写,生效断言移至 ⑧ 战斗快照
main.debug_overlay.press_apply_for_probe()
main.ui.modals.debug_overlay.press_apply_for_probe()
# ④ 重置(重开浮窗 → 重置按钮真实路径;面板百分比归零——下一场战斗生效值回到基线)
main._show_debug_overlay()
main.debug_overlay.press_reset_for_probe()
main.ui.modals.debug_overlay.press_reset_for_probe()
# ⑤ 批量模拟全链路(N=10——走 level_autoplay 与 headless 扫描同口径;表基线由组合根复位;
# 菜单态上下文未锁定=按钮可点,is_running 语义=模拟在跑〔18卡⑦ 与对局禁用态拆分〕)
main.debug_overlay.set_sliders(100, 0)
main.debug_overlay.set_sim_count(10)
main.debug_overlay.press_simulate_for_probe()
main.ui.modals.debug_overlay.set_sliders(100, 0)
main.ui.modals.debug_overlay.set_sim_count(10)
main.ui.modals.debug_overlay.press_simulate_for_probe()
var guard := SIM_GUARD_FRAMES
while main.debug_overlay.is_running() and guard > 0:
while main.ui.modals.debug_overlay.is_running() and guard > 0:
guard -= 1
await get_tree().process_frame
if main.debug_overlay.is_running():
if main.ui.modals.debug_overlay.is_running():
problems.append("模拟未在预算帧内完成(疑似挂死)")
var out: String = main.debug_overlay.output_text()
var out: String = main.ui.modals.debug_overlay.output_text()
if not out.contains("关卡完成率"):
problems.append("模拟输出缺关卡完成率")
# 口径换代 V7:胜局统计行措辞钉子(atk+100% 加成档 10 局必有胜局——hp_left_win 非空路径恒出)
@@ -83,7 +83,7 @@ func _ready() -> void:
problems.append("模拟输出 N 标注错误(%s)" % out.substr(0, 40))
# ⑥ 关闭 → 浮窗收起、开始层全程在屏
main._close_debug_overlay()
if main.debug_overlay.visible or not main._start_overlay.visible:
if main.ui.modals.debug_overlay.visible or not main.ui.screens.start_overlay.visible:
problems.append("关闭后层级状态错误")
# ⑦ hero 表运行期不可变(E4 收口回归断言:任何代码 reintroduce 表写在此被抓获——原"表值残留"断言语义升级)
if int(main.db.combat.hero.atk) != base_atk or int(main.db.combat.hero.max_hp) != base_hp:
@@ -93,20 +93,20 @@ func _ready() -> void:
# 存档隔离(停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律;
# isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退)
ProbeUiUtil.isolate_save(main, "smoke_tune_probe_save", true, false, true) # 末参=swap_save(卡②签名换代——断点写盘经 autoload 落隔离档)
main.debug_overlay.set_sliders(100, 50)
main.debug_overlay.press_apply_for_probe()
main.debug_overlay.seed_edit.text = "42" # 种子钉死(18卡⑦审计🔴——空框=非首局随机,显式优先经真实路径断言 master_seed)
main.ui.modals.debug_overlay.set_sliders(100, 50)
main.ui.modals.debug_overlay.press_apply_for_probe()
main.ui.modals.debug_overlay.seed_edit.text = "42" # 种子钉死(18卡⑦审计🔴——空框=非首局随机,显式优先经真实路径断言 master_seed)
main._start_adventure_from_menu() # 完整冒险路径(唯一试玩入口;_start_single 按语义清零滑条 = 恒基线,不承载加成断言)
await get_tree().process_frame
await get_tree().process_frame
# ⑧b 开冒险路径显式关浮窗(18卡⑦ T2 防护——调试会话默认开态下开局不带窗,对局中经角标再开)
if main.debug_overlay.visible:
if main.ui.modals.debug_overlay.visible:
problems.append("开冒险路径未显式关浮窗(T2 防护失守)")
if int(main.run_scope.adventure.master_seed) != 42:
problems.append("显式种子未达冒险(master_seed=%s ≠ 42——真实路径显式优先失守)" % str(main.run_scope.adventure.master_seed))
# 关卡开始 = 入口选路停点——真实按钮路径选入口候选进首战再读呈现快照
if main.level_screen.visible:
main.level_screen.press_candidate(0)
if main.ui.modals.level_screen.visible:
main.ui.modals.level_screen.press_candidate(0)
await get_tree().process_frame
await get_tree().process_frame
var hero: Dictionary = main.run_scope.settlement.presentation_snapshot().hero
@@ -116,23 +116,23 @@ func _ready() -> void:
# ⑨ 对局中可开(18卡⑦ P2 甲/T2 防护——S1+ 停机点回归闸):战斗屏角标真实信号路径开窗;
# 棋盘输入不锁、状态角标不置灰(「默认开×遮挡复活」下必锁死的破约回归面);
# 对局中批量模拟/两层调试两钮禁用;当局数据六项与只读访问器逐项一致;关闭钮真实路径收窗恢复
var dbg_badge: Button = main.debug_badge
var dbg_badge: Button = main.ui.screens.debug_badge
if dbg_badge == null or dbg_badge.visible:
problems.append("对局中调试浮层角标非恒隐(发布态语义失守——卡⑦ 删写死行批)")
else:
main._show_debug_overlay() # 对局中开浮窗回归经直调承载(:61 既有直调先例——发布态角标恒隐,按钮信号路径退役)
if not main.debug_overlay.visible:
if not main.ui.modals.debug_overlay.visible:
problems.append("对局中角标开浮窗失败(P2 甲对局中可达失守)")
if main._input_locked():
if main.ui.modals.flow.locks_input():
problems.append("对局中开浮窗后棋盘输入锁死(T2 防护失守——S1+ 停机点)")
if main.board_zone.status_badge.disabled:
if main.ui.battle.board_zone.status_badge.disabled:
problems.append("对局中开浮窗后状态角标置灰(T2 防护失守——S1+ 停机点)")
if main.debug_overlay.is_running():
if main.ui.modals.debug_overlay.is_running():
problems.append("对局中上下文锁定误判为模拟在跑(is_running 语义互扰)")
if not main.debug_overlay._sim_btn.disabled or not main.debug_overlay._dbg_btn.disabled:
if not main.ui.modals.debug_overlay._sim_btn.disabled or not main.ui.modals.debug_overlay._dbg_btn.disabled:
problems.append("对局中批量模拟/两层调试按钮未禁用(T2 并存态不开放失守)")
# 当局数据六项值对(T3——数值与只读访问器逐项一致;种子=adventure.master_seed 现值非 _seed_value 重算)
var rd: Dictionary = main.debug_overlay.run_data_probe()
var rd: Dictionary = main.ui.modals.debug_overlay.run_data_probe()
var r = main.run_scope.adventure.runner()
if String(rd.get("seed", "")) != "42":
problems.append("当局数据种子 ≠ master_seed 现值(%s)" % str(rd.get("seed")))
@@ -146,27 +146,27 @@ func _ready() -> void:
problems.append("当局数据 GP 失对(%s ≠ %s)" % [str(rd.get("gp")), str(r.gp)])
if int(rd.get("keystone", -1)) != main.run_scope.adventure.journal().keystone_gained():
problems.append("当局数据钥石失对(%s ≠ %s)" % [str(rd.get("keystone")), str(main.run_scope.adventure.journal().keystone_gained())])
var close_btn: Button = ProbeUiUtil.find_button(main.debug_overlay, "关闭", "prefix")
var close_btn: Button = ProbeUiUtil.find_button(main.ui.modals.debug_overlay, "关闭", "prefix")
if close_btn == null:
problems.append("浮窗关闭钮缺失(真实收窗路径中断)")
else:
close_btn.pressed.emit()
if main.debug_overlay.visible or main._input_locked():
if main.ui.modals.debug_overlay.visible or main.ui.modals.flow.locks_input():
problems.append("对局中关闭浮窗后态失守(visible=%s locked=%s)"
% [str(main.debug_overlay.visible), str(main._input_locked())])
% [str(main.ui.modals.debug_overlay.visible), str(main.ui.modals.flow.locks_input())])
# ⑨b 只读巡检(17卡③形制)+零动效契约(17卡③ R5——零 AnimationPlayer+源码零 tween)
_overlay_readonly_contract(main, problems)
_overlay_zero_motion_contract(main, problems)
# ⑩ 种子三分单点断言(18卡⑦ C1——判定只落 _seed_value() 单点;对局态不影响本判定面):
# 显式合法整数全场景优先(含首局)→ 首局(存档布尔 false)恒 42 → 非首局空框引擎熵连抽不全等 42
main.debug_overlay.seed_edit.text = "777"
main.ui.modals.debug_overlay.seed_edit.text = "777"
if main._seed_value() != 777:
problems.append("显式种子输入未优先(_seed_value != 777——C1 ①失守)")
# SaveManager 是 autoload 单例——换注走原地 attach(首局态挂载+恢复隔离档);
# --script 模式无裸 autoload 标识符——经 main 节点路径解析
var sm = main.get_node("/root/SaveManager")
sm.slots.attach_path("user://smoke_tune_probe_seed.json") # 全新空档——first_adventure_done=false(首局态;新档缺省面)
main.debug_overlay.seed_edit.text = ""
main.ui.modals.debug_overlay.seed_edit.text = ""
if main._seed_value() != 42:
problems.append("首局空框种子 != 42(P5 甲失守——教学态可复现破约)")
sm.slots.attach_path("user://smoke_tune_probe_save.json") # 恢复隔离档(非首局态——isolate_save 已建档置位)
@@ -181,7 +181,7 @@ func _ready() -> void:
all42 = false
if all42:
problems.append("非首局空框连抽 4 次全等 42(随机化失守——C1 ③)")
main.debug_overlay.seed_edit.text = "42" # 钉回显式值(现场整洁)
main.ui.modals.debug_overlay.seed_edit.text = "42" # 钉回显式值(现场整洁)
for f0 in ["user://smoke_tune_probe_seed.json", "user://smoke_tune_probe_seed.json.tmp"]:
if FileAccess.file_exists(f0):
DirAccess.remove_absolute(f0)
@@ -196,10 +196,11 @@ func _ready() -> void:
# 只读巡检(18卡⑦ T3——17卡③「白名单外零连接」形制沿用):浮窗子树全控件 × 游戏语义信号名清单
# 全连接巡检——白名单={视图自身八方法 + 组合根 _on_modal_closed(closed 外发消费)};
# 全连接巡检——白名单={视图自身八方法 + 注册面模态编排口 ui.modals.flow.close(closed 外发消费——
# 注册面批把该连接目标从组合根转发壳改为 flow 真方法,机检白名单随批登记同一连接)};
# 当局数据区块=纯标签零连接在此被一并约束(新增任何连接=红须入册审查)。
func _overlay_readonly_contract(main: Node, problems: Array) -> void:
var overlay: Control = main.debug_overlay
var overlay: Control = main.ui.modals.debug_overlay
var stack: Array = [overlay]
var conn_count := 0
while not stack.is_empty():
@@ -210,8 +211,9 @@ func _overlay_readonly_contract(main: Node, problems: Array) -> void:
var cb: Callable = conn["callable"]
var host: Object = cb.get_object()
var m := String(cb.get_method())
var flow_host: Object = main.ui.modals.flow # 注册面模态编排口(closed 外发消费白名单宿主)
var ok := (host == overlay and m in _OVERLAY_VIEW_METHODS) \
or (host == main and m == "_on_modal_closed") \
or (host == flow_host and m == "close") \
or (host is VScrollBar or host is HScrollBar) # 引擎滚动机构内部 drag 接线(RichTextLabel 自动滚动条——17卡③布局生命线同类豁免,零游戏写通路)
if not ok:
problems.append("[readonly] 非法信号连接 %s.%s → %s.%s(白名单外零连接巡检——18卡⑦ T3)"
@@ -224,7 +226,7 @@ func _overlay_readonly_contract(main: Node, problems: Array) -> void:
# 零动效契约(17卡③ R5 形制——子树零 AnimationPlayer + 视图源码零 tween/动效包装调用)
func _overlay_zero_motion_contract(main: Node, problems: Array) -> void:
var overlay: Control = main.debug_overlay
var overlay: Control = main.ui.modals.debug_overlay
var stack: Array = [overlay]
while not stack.is_empty():
var n: Node = stack.pop_back()
+8 -8
View File
@@ -332,13 +332,13 @@ static func _on_adventure_curio_resolved(result: Dictionary, st: Dictionary, adr
# 商店贪心买齐:GP 局内死钱——买得起就买( affordable 集里挑最便宜的
# 反复买到达不上任何商品为止,guard 防死循环)。确定性(零随机流);购买出账/加算区入账全走
# runner.buy_product 核心编排(与呈现层购买同一出账路径,wallet_changed 信号照常采集)。
# runner.shop.buy_product 核心编排(与呈现层购买同一出账路径,wallet_changed 信号照常采集)。
static func _autoplay_shop(r, st: Dictionary) -> void:
var guard := 30
while guard > 0:
guard -= 1
var affordable: Array = []
for p in r.shop_products():
for p in r.shop.shop_products():
if r.gp >= int(p.get("price", 1 << 30)):
affordable.append(p)
if affordable.is_empty():
@@ -347,20 +347,20 @@ static func _autoplay_shop(r, st: Dictionary) -> void:
for p in affordable:
if int(p.get("price", 1 << 30)) < int(pick.get("price", 1 << 30)):
pick = p
if not r.buy_product(String(pick.get("product_id", ""))):
if not r.shop.buy_product(String(pick.get("product_id", ""))):
return
st.shop_buys = int(st.get("shop_buys", 0)) + 1
# 商店共享池贪心:货架三选一同构面贪心买齐——affordable 集挑最便宜反复买
# (每次买后全重抽、货架随买随换,枯竭/不可负担即止,guard 防死循环)。购买走 runner.buy_pool_item
# (每次买后全重抽、货架随买随换,枯竭/不可负担即止,guard 防死循环)。购买走 runner.shop.buy_pool_item
# 核心编排(与呈现层购买同一出账+池消费路径,wallet_changed 信号照常采集)。确定性(零随机流)。
static func _autoplay_shop_pool(r, st: Dictionary) -> void:
var guard := 30
while guard > 0:
guard -= 1
var affordable: Array = []
for c in r.shop_shelf():
for c in r.shop.shop_shelf():
if r.gp >= int(c.get("price", 1 << 30)):
affordable.append(c)
if affordable.is_empty():
@@ -369,7 +369,7 @@ static func _autoplay_shop_pool(r, st: Dictionary) -> void:
for c in affordable:
if int(c.get("price", 1 << 30)) < int(pick.get("price", 1 << 30)):
pick = c
if not r.buy_pool_item(String(pick.get("kind_id", ""))):
if not r.shop.buy_pool_item(String(pick.get("kind_id", ""))):
return
st.shop_buys = int(st.get("shop_buys", 0)) + 1
@@ -415,10 +415,10 @@ static func _route_pick(runner, route: String, st: Dictionary) -> int:
s = 10.0
var cheapest := 1 << 30
if String(st.get("shop_mode", "legacy")) == "pool":
for c in runner.shop_shelf(): # pool 面:货架现价
for c in runner.shop.shop_shelf(): # pool 面:货架现价
cheapest = mini(cheapest, int(c.get("price", 1 << 30)))
else:
for p in runner.shop_products():
for p in runner.shop.shop_products():
cheapest = mini(cheapest, int(p.get("price", 1 << 30)))
if cheapest < (1 << 30) and runner.gp >= cheapest:
s = 80.0
+4 -12
View File
@@ -1,5 +1,5 @@
{
"note": "存量基线·首次自动生成;闸况变化时本脚本会判红,需人工确认后重生成;2026-10-17 人工确认重生成 06 条目:af94ce97 把 gdd_lint 记为存量红(01 篇 R5),该红已由 fcb8c3f8 补流程图消除,实测 --docs 域 06 exit=0,故基线改回绿。2026-10-18 新增闸件 08c 流水线闸(tools/pipeline_gate.py 注册上电),手工加入绿记录(roadmap 压缩批实测 --docs 域 exit=0),其余条目原样保留。2026-10-22 测试术语与层级重构批 A 改键:01 冒烟全量→01 系统测试(同一断言面 smoke_all.gd,运行器换 tools/run_system_test.py,过渡期判定口径不变)、02 单元套件→02 单元与集成测试(同一 run_all.gd 入口,名从实)、新增 00 冒烟测试(tools/run_smoke.py+game/tests/smoke_boot.tscn,重写后单跑实测 17 项 0 失败 PASS);09 静态扫描基线 exit=1(fp=4354b2aa72a7)系滞后红,本批单跑实测 exit=0(与 2026-10-22 16:20 回执一致),基线改回绿;其余条目原样保留。;2026-10-23 主会话记 07 doc_link 为存量红(用户令忽略:锻造工坊.md 死链的修复在另一会话未合入,合回闸豁免;外部修复合入后应重生成基线归绿);2026-10-26 用户特批带红合并(迭代 38 收口):09 静态扫描记存量红——gdstyle 恶化 2 条(battle_orchestrator.gd max-file-length 1314>1302=卡④头注必载内容、level_map_screen.gd max-class-variables 22>21=卡①缓存成员,均为卡片义务的记账漂移非缺陷,压缩还债经计算不可行),用户裁「不管引入的恶化直接提交、特许免测试重跑」;后续批次若触及两文件可顺带还债或经批准重生成基线归绿;2026-10-27 用户裁「标准是尺、不以现状为尺」:09 存量红照实记(exit=1+现行红项指纹,两条恶化各自点名可核),gdstyle 基线登记值回退 1302/21(波 0 的 18 条陈旧键清理保留),gdstyle_gate 逐条违规行加 FAIL 前缀并加「refresh 禁抬高登记值」守卫——存量红期间任何新增违规或进一步恶化都会改指纹被拦;阈值口径不变=gdstyle 默认值(文件 1000 行/类变量 15 等),还债归迭代 40 卡①(battle_orchestrator 1314→≤1000)与迭代 41 卡③(level_map_screen 22→≤15),还清后重新生成基线归绿;2026-10-27 用户特批带红合并(迭代39 拆件批):02 记存量红(fp f74b9a48ef4a=round_flow EV 锚 goblin 0.9148∉[1.00,1.20],触发器批有意行为改动,旧带宽锚触发器前设计,重锚归后续数值批待用户发起);08d 记存量红(red-nokeys=生涯.md:55 引 adventure_runner.gd:814 行号漂移,B1 拆分后 660 行,另 :50/:60 与 元进度.md:70 建议级,修复归合并后文档批,反哺点在册);07 doc_link 基线红→现绿改善随下次基线重生成记账;09 指纹经 S9 注解修复(f96632de)回归登记值 91d45583cf6d;2026-10-28 迭代40 收口记账(用户裁「刷」):卡① 拆件还债——battle_orchestrator 行数红项消失(实测 1036≤登记 1302;契约2 残余 36 行系合并适配带入 master 迭代39 内容,用户裁登记残余债归待办清单),09 红项由两条收敛为一条(level_map_screen 类变量 22>21=迭代38 存量,还债归迭代41 卡③),指纹 91d45583cf6d → 8467b4588a20(收口会话按闸壳同款算法对用户探跑日志实算,全量闸复验);02 存量红已随 master 侧 EV 重锚批(ba5105aa)归绿并入本合并;2026-10-28 EV 锚重锚批(工程改动·worktree ev-anchor-remeasure-20261028,用户特许会话内执行):02 基线红→现绿归绿——round_flow EV 锚 goblin 带宽按无情境口径(模拟器不过滤 trigger 行)重锚 [0.82,1.02](迭代39触发器批孤注守势入池稀释,实测 0.9148,重锚注记 enemy.tres 与测试注释;旧带 [1.00,1.20] 随池构成换代作废),撤销证明=带宽回退 [1.00,1.20] 断言复红;08d 存量红不变(生涯.md:55 行号漂移,修复归文档批);2026-10-29 文档清理批(批 5)实测 --docs 域 07/08d 皆绿,两条目归绿;2026-10-29 能力审计闸上电(tools/capability_audit.py 注册文档域 08e,查 GDD 纯度+能力清单与覆盖域表结构):手工加入绿记录(批 5 纯度红项清零后孤立实测 exit=0),其余条目原样保留;2026-10-29 合回闸关联档批实测重生成 02/09 两键(人工确认凭据=待办清单「迭代 41 测试债修复」条 2026-10-29 用户裁带红合并、代码域现存红全部由该条目承接):02 记存量红 fp f9865f1a5a44=9 套件失败(config_db 解析错 cattr/config_db_fatal_oob 退出码 2≠1/table_structure_guard 缺 meta_state/treasure_items 缺 crossrun/standee_fx_sweep set_trans 空引用/forge_workbench 120s 超时挂死/tree_requires_level 空基址/save_manager_domain 缺 _Line truncated
"note": "存量基线·闸况变化时本脚本会判红,需人工确认后重生成;2026-10-29 二次记账=03/04/05 三闸按 80b49c0f 先例经用户裁决记入现行红项指纹(03=8a3cae0d0446/04=591b27250b36/05=de89589e49ed)——master 同值复现非迭代42新增(stash 对照法取证,承「迭代 41 测试债修复」条目扩展);原 2026-10-17 06 条目人工重生成记录见 git 提交史",
"gates": {
"00 冒烟测试": {
"exit": "0",
@@ -13,17 +13,13 @@
"exit": "1",
"fp": "f9865f1a5a44"
},
"03 探针 --click": {
"exit": "0",
"fp": "green"
},
"04 探针 --full": {
"03 场景探针": {
"exit": "0",
"fp": "green"
},
"05 渲染闸": {
"exit": "0",
"fp": "green"
"exit": "1",
"fp": "de89589e49ed"
},
"06 gdd_lint": {
"exit": "0",
@@ -53,10 +49,6 @@
"exit": "0",
"fp": "green"
},
"11 回放确定性对拍": {
"exit": "SKIP",
"fp": "skip"
},
"08d 引用可核": {
"exit": "0",
"fp": "green"
+3 -11
View File
@@ -33,11 +33,10 @@ tests/
| **单元测试** | 领域类的原子行为对不对(公式/状态机/纯逻辑)——从对外方法进、断言钉对外可观测面(写法判据见 test-conventions §8.2) | `tests/unit/` | `run_all.gd` manifest(闸 02) |
| **集成测试** | 跨系统的闭环对不对 | `tests/integration/` | 同上(与单元测试同一入口、同一协议) |
| **系统测试** | 启动真实工程走真实玩法路径 | 过渡期 `game/probe/smoke_all.gd`(断言迁移批后=`tests/system_test.gd`) | `run_system_test.py`(闸 01) |
| **UI 探针+渲染 golden** | 跑起来之后 UI 与场景切换对不对;画面基线 | `game/probe/*.tscn` 探针 | `probe_gate.sh`(闸 03/04)、`render_gate.sh`(闸 05) |
| **UI 探针+渲染 golden** | 跑起来之后 UI 与场景切换对不对;画面基线 | `game/probe/*.tscn` 探针 | `probe_gate.sh`(闸 03)、`render_gate.sh`(闸 05) |
> **integration/ 已建承载**(迭代 29 T3——此前只有章程句无实体,空壳曾于迭代 19 卡① T4 删过一次)。
> 首个集成用例 = `save_rng_roundtrip_test.gd`(存档⇄RNG 快照往返)。章程登记的四类集成用例里,
> 回放确定性已由 `tools/parity_anchor.py` 承担并收编为全量域闸 11 号条件闸件;
> Core-Core 与 A1×A2 端到端仍随属主批增量登记;各 GDD 登记的 deferral 集成用例以对侧登记为生效条件。
>
> **分层判据**:能不能在 `--script` 模式下不加载 autoload 就验完?能 = 单元测试;
@@ -148,12 +147,6 @@ tests/
| 系统间契约词表与受理口径:五配方互异/掷点标签成员性/增益布尔受理 | 战斗结算×数据配置×增益状态 | `DamageController.open_ticket()`/`StatusContainer.apply_effect()` | `system_contracts_test.gd:_test_damage_controller_vocabularies`;`system_contracts_test.gd:_test_rng_tag_closure`;`system_contracts_test.gd:_test_status_boolean_acceptance` |
| 敌人技能池跨表链接校验 | 数据配置×敌人系统 | `ConfigDbCrossValidator.validate_skill_pool_links()` | `config_db_cross_validator_test.gd:_test_all_links_valid`;`config_db_cross_validator_test.gd:_test_dangling_skill_id`;`config_db_cross_validator_test.gd:_test_cast_side_mismatch` |
**跨域条件闸**
| 能力 | 系统 | 真实入口 | 代表用例 |
|---|---|---|---|
| 回放确定性(同种子三变体逐位) | 全玩法链 | `tools/parity_anchor.py`(闸 11 条件闸,按批发起) | `tools/parity_anchor.py`(整件为条件闸) |
**覆盖域表**(`design/gdd/systems/` 逐篇对照——状态三档:已覆盖/部分覆盖/未覆盖;部分与未覆盖行的缺口指待办清单条目,覆盖面提升时同批改本表):
| 域 | 系统文档 | 能力行 | 状态 | 缺口 |
@@ -201,9 +194,8 @@ tests/
| `tools/run_system_test.py`(系统测试) | 真实玩法路径的行为断言面(推理→战斗→结算→进度推进) | 退出码+脚本错误扫描 | 玩法链路改动的调试期确认 |
| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(65 套件 manifest) | 退出码+三硬闸(见下);每套件子进程带截止时间守护——超时先 taskkill /T 整树回收再判该套件 FAIL、批继续(默认 120s,环境变量 `RUN_ALL_SUITE_TIMEOUT_SEC` 可覆盖);连续 3 个套件超时=红树整批挂死,第 3 个即中止整轮、汇总已见失败与超时清单后非 0 退出(阈值=可调常量,不连续的超时照旧逐个回收) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;此形态仅供开发期最窄确认,不产闸回执、不作验收凭据);闸机关联档选跑——同一过滤机制加 `-- --only-source=gates`,由 `tools/run_all_gates.sh` 在 02 闸传入(回执 scope=related,是验收跑批本体) |
| `tools/probe_gate.sh`(场景探针,`--click`/`--full` 两模式) | UI 可达性、可见性、状态——真实实例化场景+真实输入路径 | 探针自检+退出码 | 新增 UI 区域的配套探针直跑与断言复核 |
| `tools/render_gate.sh`(渲染 golden) | 画面基线——截图与基线图逐像素容差比对 | golden 比对脚本(依赖 PIL) | 主题/素材/排版类改动;窗口化运行,需开显示器 |
| `tools/render_gate.sh`(渲染 golden) | 画面基线——截图与基线图逐像素容差比对 | golden 比对脚本(依赖 PIL)+退出码/取证双检/三段探针日志脚本错误扫描(`error_scan.py`) | 主题/素材/排版类改动;窗口化运行,需开显示器 |
| `game/tests/static_scan.sh`(S 域) | 项目纪律+第三方质量规则:RNG 单点/core 纯度/类型标注/文件头/注释禁词/旧符号残留;S13=gdstyle 质量规则(重复 load、循环内 await、循环内分配、不可达代码等真缺陷类;基线键=文件|规则|函数名(类/文件级=文件|规则,无数字条目带消息),数字漂移不假红,同键指标恶化超基线登记值即红) | 退出码(逐规则 FAIL 行) | 改产品码前后的快速自检;新增纪律先补扫描规则再修码 |
| `tools/parity_anchor.py`(回放对拍) | 同种子下三变体逐位确定性 | 工具脚本逐位比对 | 本批触及数值/存档/RNG/结算面时;须用户批准发起 |
| 测量件(`game/sim/`,逐件见下「游离脚本台账」) | 期望/分布读数——只给读数,不判对错 | 人工读 stdout 或配套工具脚本 | 调参与归因;MC 类一律用户发起(档位表见 `sim-data/README.md`「MC 采集档位表」) |
> 单件直跑是调试期最窄确认手段,**不构成验收**——验收只有 `tools/run_all_gates.sh` 一个入口(判定三硬闸见下「本地闸」节)。
@@ -267,7 +259,7 @@ godot --headless --path . res://probe/<probe>.tscn
| 脚本 | 定性 | 用途与触发方式 | 结果由谁判 |
|---|---|---|---|
| `adventure_monte_carlo.gd` | **在役**(`AGENTS.md` 在册入口) | 全冒险期望 MC;`tools/parity_anchor.py` 的比对载体 | 工具脚本逐位比对 |
| `adventure_monte_carlo.gd` | **在役**(`AGENTS.md` 在册入口) | 全冒险期望 MC | 人工读 stdout |
| `verifier_batch.gd` | **在役**(`AGENTS.md` 在册入口) | `tools/balance_verifier.py` 的采集端(数值验证) | 报告与原始数据落 `sim-data/` |
| `reward_expectation.gd` | **在役**(`AGENTS.md` 在册入口) | 奖励期望测量(非封锁格分布/资源类型频次) | 人工读 stdout |
| `monte_carlo.gd` | 在役 | 战斗期望 MC(胜轮数/承伤/清图/开格分布) | 人工读 stdout |
-166
View File
@@ -1,166 +0,0 @@
{
"variants": {
"base": {
"runsum": [
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":8,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71000\",\"1#71000\",\"1#71000\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":4,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":5,\"route\":\"min\",\"seal_hit\":22,\"seal_hit_gain\":79,\"seal_miss\":3,\"seal_miss_penalty\":8,\"seal_placed\":25,\"seed\":71000,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":24,\"visited_count\":3,\"wallet_count\":4}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":10,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71001\",\"1#71001\",\"1#71001\"],\"levels_cleared\":0,\"mine_dmg\":0,\"mine_hits\":6,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":6,\"route\":\"min\",\"seal_hit\":21,\"seal_hit_gain\":-296,\"seal_miss\":7,\"seal_miss_penalty\":16,\"seal_placed\":31,\"seed\":71001,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":10,\"visited_count\":3,\"wallet_count\":4}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":2,\"choices\":1,\"cleared\":false,\"encounter_max_hp_seq\":[26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":5,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71002\",\"1#71002\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":2,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":5,\"route\":\"min\",\"seal_hit\":17,\"seal_hit_gain\":391,\"seal_miss\":1,\"seal_miss_penalty\":8,\"seal_placed\":19,\"seed\":71002,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":21,\"visited_count\":2,\"wallet_count\":3}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":15,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71003\",\"1#71003\",\"1#71003\",\"1#71003\",\"1#71003\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":6,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":13,\"route\":\"min\",\"seal_hit\":28,\"seal_hit_gain\":317,\"seal_miss\":9,\"seal_miss_penalty\":24,\"seal_placed\":44,\"seed\":71003,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":23,\"visited_count\":5,\"wallet_count\":6}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":6,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,32],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":39,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71004\",\"1#71004\",\"1#71004\",\"1#71004\",\"1#71004\",\"1#71004\",\"1#71004\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":17,\"policy\":\"normal\",\"refreshes\":1,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":28,\"route\":\"min\",\"seal_hit\":70,\"seal_hit_gain\":23,\"seal_miss\":17,\"seal_miss_penalty\":8,\"seal_placed\":93,\"seed\":71004,\"shop_gp_seq\":[19],\"stopped_at\":1,\"taken\":47,\"visited_count\":7,\"wallet_count\":11}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":26,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71005\",\"1#71005\",\"1#71005\",\"1#71005\",\"1#71005\"],\"levels_cleared\":0,\"mine_dmg\":22,\"mine_hits\":8,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":15,\"route\":\"min\",\"seal_hit\":39,\"seal_hit_gain\":284,\"seal_miss\":11,\"seal_miss_penalty\":0,\"seal_placed\":56,\"seed\":71005,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":48,\"visited_count\":5,\"wallet_count\":6}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":17,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71006\",\"1#71006\",\"1#71006\",\"1#71006\",\"1#71006\"],\"levels_cleared\":0,\"mine_dmg\":6,\"mine_hits\":6,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":13,\"route\":\"min\",\"seal_hit\":39,\"seal_hit_gain\":291,\"seal_miss\":5,\"seal_miss_penalty\":6,\"seal_placed\":50,\"seed\":71006,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":23,\"visited_count\":5,\"wallet_count\":6}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":6,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,35],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":28,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71007\",\"1#71007\",\"1#71007\",\"1#71007\",\"1#71007\",\"1#71007\",\"1#71007\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":8,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":24,\"route\":\"min\",\"seal_hit\":59,\"seal_hit_gain\":-70,\"seal_miss\":11,\"seal_miss_penalty\":12,\"seal_placed\":74,\"seed\":71007,\"shop_gp_seq\":[39],\"stopped_at\":1,\"taken\":44,\"visited_count\":7,\"wallet_count\":14}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":7,\"choices\":7,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,32,32,32],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":38,\"hp_pct\":0,\"keystone_total\":3,\"level_maps\":[\"1#71008\",\"1#71008\",\"1#71008\",\"1#71008\",\"1#71008\",\"1#71008\",\"1#71008\",\"2#71008\",\"2#71008\"],\"levels_cleared\":1,\"mine_dmg\":16,\"mine_hits\":18,\"policy\":\"normal\",\"refreshes\":1,\"relics\":[\"hard_hat\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":22,\"heal_gained\":6,\"hp_after\":11,\"hp_before\":5,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"hard_hat\",\"feat_name\":\"安全帽\",\"hp_after\":11,\"hp_before\":11,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":33,\"route\":\"min\",\"seal_hit\":83,\"seal_hit_gain\":483,\"seal_miss\":14,\"seal_miss_penalty\":8,\"seal_placed\":102,\"seed\":71008,\"shop_gp_seq\":[19],\"stopped_at\":2,\"taken\":54,\"visited_count\":9,\"wallet_count\":12}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":18,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71009\",\"1#71009\",\"1#71009\",\"1#71009\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":3,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":9,\"route\":\"min\",\"seal_hit\":30,\"seal_hit_gain\":-28,\"seal_miss\":4,\"seal_miss_penalty\":8,\"seal_placed\":35,\"seed\":71009,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":26,\"visited_count\":4,\"wallet_count\":5}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":9,\"choices\":8,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":60,\"hp_pct\":0,\"keystone_total\":3,\"level_maps\":[\"1#71010\",\"1#71010\",\"1#71010\",\"1#71010\",\"1#71010\",\"1#71010\",\"1#71010\",\"2#71010\",\"2#71010\",\"2#71010\"],\"levels_cleared\":1,\"mine_dmg\":24,\"mine_hits\":13,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[\"hard_hat\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":13,\"heal_gained\":7,\"hp_after\":18,\"hp_before\":11,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"hard_hat\",\"feat_name\":\"安全帽\",\"hp_after\":18,\"hp_before\":18,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":47,\"route\":\"min\",\"seal_hit\":109,\"seal_hit_gain\":309,\"seal_miss\":14,\"seal_miss_penalty\":8,\"seal_placed\":129,\"seed\":71010,\"shop_gp_seq\":[27],\"stopped_at\":2,\"taken\":84,\"visited_count\":10,\"wallet_count\":16}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":18,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71011\",\"1#71011\",\"1#71011\",\"1#71011\",\"1#71011\"],\"levels_cleared\":0,\"mine_dmg\":0,\"mine_hits\":7,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":15,\"route\":\"min\",\"seal_hit\":47,\"seal_hit_gain\":-119,\"seal_miss\":9,\"seal_miss_penalty\":24,\"seal_placed\":61,\"seed\":71011,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":18,\"visited_count\":5,\"wallet_count\":6}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":15,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71012\",\"1#71012\",\"1#71012\",\"1#71012\"],\"levels_cleared\":0,\"mine_dmg\":0,\"mine_hits\":8,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":11,\"route\":\"min\",\"seal_hit\":34,\"seal_hit_gain\":89,\"seal_miss\":10,\"seal_miss_penalty\":14,\"seal_placed\":47,\"seed\":71012,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":15,\"visited_count\":4,\"wallet_count\":5}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":2,\"choices\":1,\"cleared\":false,\"encounter_max_hp_seq\":[26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":6,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71013\",\"1#71013\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":4,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":3,\"route\":\"min\",\"seal_hit\":10,\"seal_hit_gain\":-21,\"seal_miss\":2,\"seal_miss_penalty\":8,\"seal_placed\":15,\"seed\":71013,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":22,\"visited_count\":2,\"wallet_count\":3}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":24,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71014\",\"1#71014\",\"1#71014\",\"1#71014\",\"1#71014\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":6,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":11,\"route\":\"min\",\"seal_hit\":32,\"seal_hit_gain\":-62,\"seal_miss\":2,\"seal_miss_penalty\":0,\"seal_placed\":40,\"seed\":71014,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":32,\"visited_count\":5,\"wallet_count\":6}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,32],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":24,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71015\",\"1#71015\",\"1#71015\",\"1#71015\",\"1#71015\",\"1#71015\",\"1#71015\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":8,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":17,\"route\":\"min\",\"seal_hit\":51,\"seal_hit_gain\":8,\"seal_miss\":9,\"seal_miss_penalty\":14,\"seal_placed\":68,\"seed\":71015,\"shop_gp_seq\":[25],\"stopped_at\":1,\"taken\":40,\"visited_count\":7,\"wallet_count\":11}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":25,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71016\",\"1#71016\",\"1#71016\",\"1#71016\",\"1#71016\"],\"levels_cleared\":0,\"mine_dmg\":6,\"mine_hits\":4,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":14,\"route\":\"min\",\"seal_hit\":50,\"seal_hit_gain\":421,\"seal_miss\":6,\"seal_miss_penalty\":4,\"seal_placed\":61,\"seed\":71016,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":31,\"visited_count\":5,\"wallet_count\":6}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":1,\"choices\":0,\"cleared\":false,\"encounter_max_hp_seq\":[26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":1,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71017\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":2,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":2,\"route\":\"min\",\"seal_hit\":7,\"seal_hit_gain\":7,\"seal_miss\":4,\"seal_miss_penalty\":16,\"seal_placed\":11,\"seed\":71017,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":17,\"visited_count\":1,\"wallet_count\":2}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":33,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71018\",\"1#71018\",\"1#71018\",\"1#71018\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":3,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":11,\"route\":\"min\",\"seal_hit\":40,\"seal_hit_gain\":416,\"seal_miss\":2,\"seal_miss_penalty\":-2,\"seal_placed\":45,\"seed\":71018,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":41,\"visited_count\":4,\"wallet_count\":5}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":2,\"choices\":1,\"cleared\":false,\"encounter_max_hp_seq\":[26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":7,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71019\",\"1#71019\"],\"levels_cleared\":0,\"mine_dmg\":0,\"mine_hits\":10,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":5,\"route\":\"min\",\"seal_hit\":11,\"seal_hit_gain\":-85,\"seal_miss\":10,\"seal_miss_penalty\":24,\"seal_placed\":22,\"seed\":71019,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":7,\"visited_count\":2,\"wallet_count\":3}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":20,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,41,41,41,41,41,41,41,41,44],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":154,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#71020\",\"1#71020\",\"1#71020\",\"1#71020\",\"1#71020\",\"1#71020\",\"1#71020\",\"2#71020\",\"2#71020\",\"2#71020\",\"2#71020\",\"2#71020\",\"2#71020\",\"2#71020\",\"2#71020\",\"2#71020\",\"2#71020\",\"3#71020\",\"3#71020\",\"3#71020\",\"3#71020\",\"3#71020\",\"3#71020\",\"3#71020\",\"3#71020\",\"3#71020\",\"3#71020\",\"3#71020\",\"3#71020\",\"3#71020\",\"3#71020\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":2,\"relics\":[\"pain_memory\",\"map_exchange\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"pain_memory\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":20,\"heal_gained\":7,\"hp_after\":25,\"hp_before\":18,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"pain_memory\",\"feat_name\":\"疼痛记忆\",\"hp_after\":25,\"hp_before\":25,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":27,\"heal_gained\":6,\"hp_after\":41,\"hp_before\":35,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"map_exchange\",\"feat_name\":\"弃图兑换\",\"hp_after\":41,\"hp_before\":41,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":102,\"route\":\"min\",\"seal_hit\":345,\"seal_hit_gain\":1539,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":362,\"seed\":71020,\"shop_gp_seq\":[27,62,86],\"stopped_at\":0,\"taken\":154,\"visited_count\":31,\"wallet_count\":38}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":19,\"choices\":23,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,38,38,38,38,38,38,38,38,38],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":139,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#71021\",\"1#71021\",\"1#71021\",\"1#71021\",\"1#71021\",\"1#71021\",\"1#71021\",\"2#71021\",\"2#71021\",\"2#71021\",\"2#71021\",\"2#71021\",\"2#71021\",\"2#71021\",\"2#71021\",\"2#71021\",\"2#71021\",\"3#71021\",\"3#71021\",\"3#71021\",\"3#71021\",\"3#71021\",\"3#71021\",\"3#71021\",\"3#71021\",\"3#71021\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":2,\"relics\":[\"executioner\",\"seal_echo\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":16,\"heal_gained\":3,\"hp_after\":35,\"hp_before\":32,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"hp_after\":35,\"hp_before\":35,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_echo\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":16,\"heal_gained\":0,\"hp_after\":38,\"hp_before\":38,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_echo\",\"feat_name\":\"封印回响\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":85,\"route\":\"min\",\"seal_hit\":282,\"seal_hit_gain\":1672,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":307,\"seed\":71021,\"shop_gp_seq\":[34,47],\"stopped_at\":3,\"taken\":139,\"visited_count\":26,\"wallet_count\":31}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":17,\"choices\":19,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,41,41,41,41,41,41],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":117,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#71022\",\"1#71022\",\"1#71022\",\"1#71022\",\"1#71022\",\"1#71022\",\"1#71022\",\"2#71022\",\"2#71022\",\"2#71022\",\"2#71022\",\"2#71022\",\"2#71022\",\"2#71022\",\"2#71022\",\"2#71022\",\"2#71022\",\"3#71022\",\"3#71022\",\"3#71022\",\"3#71022\",\"3#71022\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"breaker_strike\",\"trap_burst\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":25,\"heal_gained\":3,\"hp_after\":35,\"hp_before\":32,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"breaker_strike\",\"feat_name\":\"破封一击\",\"hp_after\":35,\"hp_before\":35,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"trap_burst\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":23,\"heal_gained\":2,\"hp_after\":41,\"hp_before\":39,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"trap_burst\",\"feat_name\":\"陷阱爆发\",\"hp_after\":41,\"hp_before\":41,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":78,\"route\":\"min\",\"seal_hit\":269,\"seal_hit_gain\":1054,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":288,\"seed\":71022,\"shop_gp_seq\":[39,61],\"stopped_at\":3,\"taken\":117,\"visited_count\":22,\"wallet_count\":30}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":16,\"choices\":19,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,35,35,35,35,35,35,38,38,38,38,38,38],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":131,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#71023\",\"1#71023\",\"1#71023\",\"1#71023\",\"1#71023\",\"1#71023\",\"1#71023\",\"2#71023\",\"2#71023\",\"2#71023\",\"2#71023\",\"2#71023\",\"2#71023\",\"2#71023\",\"2#71023\",\"2#71023\",\"2#71023\",\"3#71023\",\"3#71023\",\"3#71023\",\"3#71023\",\"3#71023\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"blood_honey\",\"skip_mining\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":10,\"heal_gained\":5,\"hp_after\":35,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blood_honey\",\"feat_name\":\"血蜜\",\"hp_after\":35,\"hp_before\":35,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":16,\"heal_gained\":0,\"hp_after\":38,\"hp_before\":38,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"skip_mining\",\"feat_name\":\"跳格采掘\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":80,\"route\":\"min\",\"seal_hit\":255,\"seal_hit_gain\":650,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":274,\"seed\":71023,\"shop_gp_seq\":[26,37],\"stopped_at\":3,\"taken\":131,\"visited_count\":22,\"wallet_count\":26}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":19,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,32,32,32,32,32,32,38,38,38,38,38,38,38,38,44],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":142,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#71024\",\"1#71024\",\"1#71024\",\"1#71024\",\"1#71024\",\"1#71024\",\"1#71024\",\"2#71024\",\"2#71024\",\"2#71024\",\"2#71024\",\"2#71024\",\"2#71024\",\"2#71024\",\"2#71024\",\"2#71024\",\"2#71024\",\"3#71024\",\"3#71024\",\"3#71024\",\"3#71024\",\"3#71024\",\"3#71024\",\"3#71024\",\"3#71024\",\"3#71024\",\"3#71024\",\"3#71024\",\"3#71024\",\"3#71024\",\"3#71024\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":2,\"relics\":[\"chain_traps\",\"map_exchange\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"chain_traps\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":21,\"heal_gained\":1,\"hp_after\":32,\"hp_before\":31,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"chain_traps\",\"feat_name\":\"连环陷阱\",\"hp_after\":32,\"hp_before\":32,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"seal_echo\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":16,\"heal_gained\":5,\"hp_after\":38,\"hp_before\":33,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"map_exchange\",\"feat_name\":\"弃图兑换\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":102,\"route\":\"min\",\"seal_hit\":338,\"seal_hit_gain\":1807,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":354,\"seed\":71024,\"shop_gp_seq\":[16,68,68],\"stopped_at\":0,\"taken\":142,\"visited_count\":31,\"wallet_count\":35}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":19,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,41,41,41,41,41,41,41,47],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":139,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#71025\",\"1#71025\",\"1#71025\",\"1#71025\",\"1#71025\",\"1#71025\",\"1#71025\",\"2#71025\",\"2#71025\",\"2#71025\",\"2#71025\",\"2#71025\",\"2#71025\",\"2#71025\",\"2#71025\",\"2#71025\",\"2#71025\",\"3#71025\",\"3#71025\",\"3#71025\",\"3#71025\",\"3#71025\",\"3#71025\",\"3#71025\",\"3#71025\",\"3#71025\",\"3#71025\",\"3#71025\",\"3#71025\",\"3#71025\",\"3#71025\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"market_close\",\"warning_beacon\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":16,\"heal_gained\":5,\"hp_after\":35,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"market_close\",\"feat_name\":\"收盘\",\"hp_after\":35,\"hp_before\":35,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":42,\"heal_gained\":8,\"hp_after\":25,\"hp_before\":17,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"warning_beacon\",\"feat_name\":\"示警烟火\",\"hp_after\":25,\"hp_before\":25,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":96,\"route\":\"min\",\"seal_hit\":329,\"seal_hit_gain\":1701,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":350,\"seed\":71025,\"shop_gp_seq\":[33,79,101],\"stopped_at\":0,\"taken\":139,\"visited_count\":31,\"wallet_count\":38}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":20,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,32,32,32,32,32,32,41,41,41,41,41,41,41,41,47],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":131,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#71026\",\"1#71026\",\"1#71026\",\"1#71026\",\"1#71026\",\"1#71026\",\"1#71026\",\"2#71026\",\"2#71026\",\"2#71026\",\"2#71026\",\"2#71026\",\"2#71026\",\"2#71026\",\"2#71026\",\"2#71026\",\"2#71026\",\"3#71026\",\"3#71026\",\"3#71026\",\"3#71026\",\"3#71026\",\"3#71026\",\"3#71026\",\"3#71026\",\"3#71026\",\"3#71026\",\"3#71026\",\"3#71026\",\"3#71026\",\"3#71026\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"market_close\",\"execution_scholar\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":19,\"heal_gained\":4,\"hp_after\":32,\"hp_before\":28,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"market_close\",\"feat_name\":\"收盘\",\"hp_after\":32,\"hp_before\":32,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":19,\"heal_gained\":7,\"hp_after\":41,\"hp_before\":34,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"execution_scholar\",\"feat_name\":\"斩首学者\",\"hp_after\":41,\"hp_before\":41,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":92,\"route\":\"min\",\"seal_hit\":299,\"seal_hit_gain\":1397,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":323,\"seed\":71026,\"shop_gp_seq\":[22,72,114],\"stopped_at\":0,\"taken\":131,\"visited_count\":31,\"wallet_count\":39}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":16,\"choices\":18,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,32,32,32,32,32,32,38,38,38,38,38],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":119,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#71027\",\"1#71027\",\"1#71027\",\"1#71027\",\"1#71027\",\"1#71027\",\"1#71027\",\"2#71027\",\"2#71027\",\"2#71027\",\"2#71027\",\"2#71027\",\"2#71027\",\"2#71027\",\"2#71027\",\"2#71027\",\"2#71027\",\"3#71027\",\"3#71027\",\"3#71027\",\"3#71027\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"hard_hat\",\"fuse_wire\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"chain_traps\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":12,\"heal_gained\":6,\"hp_after\":20,\"hp_before\":14,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"hard_hat\",\"feat_name\":\"安全帽\",\"hp_after\":20,\"hp_before\":20,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"fuse_wire\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"venom_edge\",\"kind\":\"relic\",\"tier\":\"rare\"}],\"gp\":15,\"heal_gained\":7,\"hp_after\":35,\"hp_before\":28,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"fuse_wire\",\"feat_name\":\"保险丝\",\"hp_after\":35,\"hp_before\":35,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":75,\"route\":\"min\",\"seal_hit\":255,\"seal_hit_gain\":1770,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":272,\"seed\":71027,\"shop_gp_seq\":[20,50],\"stopped_at\":3,\"taken\":119,\"visited_count\":21,\"wallet_count\":27}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":38,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#71028\",\"1#71028\",\"1#71028\",\"1#71028\",\"1#71028\"],\"levels_cleared\":0,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":16,\"route\":\"min\",\"seal_hit\":60,\"seal_hit_gain\":-8,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":62,\"seed\":71028,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":38,\"visited_count\":5,\"wallet_count\":6}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":15,\"choices\":18,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,32,32,32,32,32,38,38,38,38,38],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":105,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#71029\",\"1#71029\",\"1#71029\",\"1#71029\",\"1#71029\",\"1#71029\",\"1#71029\",\"2#71029\",\"2#71029\",\"2#71029\",\"2#71029\",\"2#71029\",\"2#71029\",\"2#71029\",\"2#71029\",\"2#71029\",\"2#71029\",\"3#71029\",\"3#71029\",\"3#71029\",\"3#71029\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"last_drop\",\"twin_pick\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":24,\"heal_gained\":4,\"hp_after\":32,\"hp_before\":28,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"last_drop\",\"feat_name\":\"一滴不剩\",\"hp_after\":32,\"hp_before\":32,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":29,\"heal_gained\":7,\"hp_after\":37,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"twin_pick\",\"feat_name\":\"双管镐\",\"hp_after\":37,\"hp_before\":37,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":64,\"route\":\"min\",\"seal_hit\":244,\"seal_hit_gain\":1307,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":262,\"seed\":71029,\"shop_gp_seq\":[24,57],\"stopped_at\":3,\"taken\":105,\"visited_count\":21,\"wallet_count\":26}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":15,\"choices\":19,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,32,32,32,32,32,38,38,38,38,38],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":114,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#71030\",\"1#71030\",\"1#71030\",\"1#71030\",\"1#71030\",\"1#71030\",\"1#71030\",\"2#71030\",\"2#71030\",\"2#71030\",\"2#71030\",\"2#71030\",\"2#71030\",\"2#71030\",\"2#71030\",\"2#71030\",\"2#71030\",\"3#71030\",\"3#71030\",\"3#71030\",\"3#71030\",\"3#71030\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"market_close\",\"chain_traps\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":13,\"heal_gained\":2,\"hp_after\":32,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"market_close\",\"feat_name\":\"收盘\",\"hp_after\":32,\"hp_before\":32,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"chain_traps\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":42,\"heal_gained\":7,\"hp_after\":35,\"hp_before\":28,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"chain_traps\",\"feat_name\":\"连环陷阱\",\"hp_after\":35,\"hp_before\":35,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":71,\"route\":\"min\",\"seal_hit\":234,\"seal_hit_gain\":1765,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":255,\"seed\":71030,\"shop_gp_seq\":[23,54],\"stopped_at\":3,\"taken\":114,\"visited_count\":22,\"wallet_count\":26}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":19,\"choices\":23,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,32,32,32,32,32,32,38,38,38,38,38,38,38,38,38],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":142,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#71031\",\"1#71031\",\"1#71031\",\"1#71031\",\"1#71031\",\"1#71031\",\"1#71031\",\"2#71031\",\"2#71031\",\"2#71031\",\"2#71031\",\"2#71031\",\"2#71031\",\"2#71031\",\"2#71031\",\"2#71031\",\"2#71031\",\"3#71031\",\"3#71031\",\"3#71031\",\"3#71031\",\"3#71031\",\"3#71031\",\"3#71031\",\"3#71031\",\"3#71031\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":2,\"relics\":[\"chain_traps\",\"seal_echo\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"chain_traps\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":28,\"heal_gained\":6,\"hp_after\":31,\"hp_before\":25,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"chain_traps\",\"feat_name\":\"连环陷阱\",\"hp_after\":31,\"hp_before\":31,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_echo\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":23,\"heal_gained\":7,\"hp_after\":37,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_echo\",\"feat_name\":\"封印回响\",\"hp_after\":37,\"hp_before\":37,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":89,\"route\":\"min\",\"seal_hit\":303,\"seal_hit_gain\":1540,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":324,\"seed\":71031,\"shop_gp_seq\":[21,69],\"stopped_at\":3,\"taken\":142,\"visited_count\":26,\"wallet_count\":31}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":19,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,41,41,41,41,41,41,41,44],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":144,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#71032\",\"1#71032\",\"1#71032\",\"1#71032\",\"1#71032\",\"1#71032\",\"1#71032\",\"2#71032\",\"2#71032\",\"2#71032\",\"2#71032\",\"2#71032\",\"2#71032\",\"2#71032\",\"2#71032\",\"2#71032\",\"2#71032\",\"3#71032\",\"3#71032\",\"3#71032\",\"3#71032\",\"3#71032\",\"3#71032\",\"3#71032\",\"3#71032\",\"3#71032\",\"3#71032\",\"3#71032\",\"3#71032\",\"3#71032\",\"3#71032\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":3,\"relics\":[\"executioner\",\"market_close\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":19,\"heal_gained\":5,\"hp_after\":35,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"hp_after\":35,\"hp_before\":35,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":16,\"heal_gained\":3,\"hp_after\":41,\"hp_before\":38,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"market_close\",\"feat_name\":\"收盘\",\"hp_after\":41,\"hp_before\":41,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":105,\"route\":\"min\",\"seal_hit\":330,\"seal_hit_gain\":1031,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":362,\"seed\":71032,\"shop_gp_seq\":[27,64,78],\"stopped_at\":0,\"taken\":144,\"visited_count\":31,\"wallet_count\":36}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":16,\"choices\":19,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,32,32,32,32,32,32,38,38,38,38,38,38],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":117,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#71033\",\"1#71033\",\"1#71033\",\"1#71033\",\"1#71033\",\"1#71033\",\"1#71033\",\"2#71033\",\"2#71033\",\"2#71033\",\"2#71033\",\"2#71033\",\"2#71033\",\"2#71033\",\"2#71033\",\"2#71033\",\"2#71033\",\"3#71033\",\"3#71033\",\"3#71033\",\"3#71033\",\"3#71033\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"map_exchange\",\"skip_mining\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":16,\"heal_gained\":5,\"hp_after\":32,\"hp_before\":27,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"map_exchange\",\"feat_name\":\"弃图兑换\",\"hp_after\":32,\"hp_before\":32,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":23,\"heal_gained\":5,\"hp_after\":38,\"hp_before\":33,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"skip_mining\",\"feat_name\":\"跳格采掘\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":78,\"route\":\"min\",\"seal_hit\":240,\"seal_hit_gain\":654,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":255,\"seed\":71033,\"shop_gp_seq\":[20,45],\"stopped_at\":3,\"taken\":117,\"visited_count\":22,\"wallet_count\":26}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":19,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,41,41,41,41,41,41,41,44],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":151,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#71034\",\"1#71034\",\"1#71034\",\"1#71034\",\"1#71034\",\"1#71034\",\"1#71034\",\"2#71034\",\"2#71034\",\"2#71034\",\"2#71034\",\"2#71034\",\"2#71034\",\"2#71034\",\"2#71034\",\"2#71034\",\"2#71034\",\"3#71034\",\"3#71034\",\"3#71034\",\"3#71034\",\"3#71034\",\"3#71034\",\"3#71034\",\"3#71034\",\"3#71034\",\"3#71034\",\"3#71034\",\"3#71034\",\"3#71034\",\"3#71034\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"executioner\",\"blood_insight\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":22,\"heal_gained\":7,\"hp_after\":30,\"hp_before\":23,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"hp_after\":30,\"hp_before\":30,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":13,\"heal_gained\":5,\"hp_after\":41,\"hp_before\":36,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blood_insight\",\"feat_name\":\"见血开窍\",\"hp_after\":41,\"hp_before\":41,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":106,\"route\":\"min\",\"seal_hit\":332,\"seal_hit_gain\":1732,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":351,\"seed\":71034,\"shop_gp_seq\":[29,74,76],\"stopped_at\":0,\"taken\":151,\"visited_count\":31,\"wallet_count\":37}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":19,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,41,41,41,41,41,41,41,47],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":144,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#71035\",\"1#71035\",\"1#71035\",\"1#71035\",\"1#71035\",\"1#71035\",\"1#71035\",\"2#71035\",\"2#71035\",\"2#71035\",\"2#71035\",\"2#71035\",\"2#71035\",\"2#71035\",\"2#71035\",\"2#71035\",\"2#71035\",\"3#71035\",\"3#71035\",\"3#71035\",\"3#71035\",\"3#71035\",\"3#71035\",\"3#71035\",\"3#71035\",\"3#71035\",\"3#71035\",\"3#71035\",\"3#71035\",\"3#71035\",\"3#71035\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"trap_instinct\",\"blast_accounting\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"trap_instinct\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":13,\"heal_gained\":4,\"hp_after\":35,\"hp_before\":31,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"trap_instinct\",\"feat_name\":\"陷阱本能\",\"hp_after\":35,\"hp_before\":35,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":33,\"heal_gained\":6,\"hp_after\":41,\"hp_before\":35,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blast_accounting\",\"feat_name\":\"爆破会计\",\"hp_after\":41,\"hp_before\":41,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":96,\"route\":\"min\",\"seal_hit\":316,\"seal_hit_gain\":1841,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":340,\"seed\":71035,\"shop_gp_seq\":[32,58,100],\"stopped_at\":0,\"taken\":144,\"visited_count\":31,\"wallet_count\":38}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":19,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,32,32,32,32,32,32,38,38,38,38,38,38,38,44],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":148,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#71036\",\"1#71036\",\"1#71036\",\"1#71036\",\"1#71036\",\"1#71036\",\"1#71036\",\"2#71036\",\"2#71036\",\"2#71036\",\"2#71036\",\"2#71036\",\"2#71036\",\"2#71036\",\"2#71036\",\"2#71036\",\"2#71036\",\"3#71036\",\"3#71036\",\"3#71036\",\"3#71036\",\"3#71036\",\"3#71036\",\"3#71036\",\"3#71036\",\"3#71036\",\"3#71036\",\"3#71036\",\"3#71036\",\"3#71036\",\"3#71036\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"skip_mining\",\"frontier_miner\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":26,\"heal_gained\":6,\"hp_after\":23,\"hp_before\":17,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"skip_mining\",\"feat_name\":\"跳格采掘\",\"hp_after\":23,\"hp_before\":23,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":23,\"heal_gained\":7,\"hp_after\":38,\"hp_before\":31,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"frontier_miner\",\"feat_name\":\"边陲矿工\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":100,\"route\":\"min\",\"seal_hit\":331,\"seal_hit_gain\":1293,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":354,\"seed\":71036,\"shop_gp_seq\":[18,68,74],\"stopped_at\":0,\"taken\":148,\"visited_count\":31,\"wallet_count\":35}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":17,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,29,29,29,29,29,35,35,35,35,35,35,35,41],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":155,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#71037\",\"1#71037\",\"1#71037\",\"1#71037\",\"1#71037\",\"1#71037\",\"1#71037\",\"2#71037\",\"2#71037\",\"2#71037\",\"2#71037\",\"2#71037\",\"2#71037\",\"2#71037\",\"2#71037\",\"2#71037\",\"2#71037\",\"3#71037\",\"3#71037\",\"3#71037\",\"3#71037\",\"3#71037\",\"3#71037\",\"3#71037\",\"3#71037\",\"3#71037\",\"3#71037\",\"3#71037\",\"3#71037\",\"3#71037\",\"3#71037\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":2,\"relics\":[\"breaker_strike\",\"field_dressing\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":3,\"candidates\":[{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":15,\"heal_gained\":1,\"hp_after\":29,\"hp_before\":28,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"breaker_strike\",\"feat_name\":\"破封一击\",\"hp_after\":29,\"hp_before\":29,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":35,\"heal_gained\":7,\"hp_after\":35,\"hp_before\":28,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"field_dressing\",\"feat_name\":\"战地包扎\",\"hp_after\":35,\"hp_before\":35,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":95,\"route\":\"min\",\"seal_hit\":290,\"seal_hit_gain\":191,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":310,\"seed\":71037,\"shop_gp_seq\":[9,43,95],\"stopped_at\":0,\"taken\":155,\"visited_count\":31,\"wallet_count\":32}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":19,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,38,38,38,38,38,38,38,44],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":127,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#71038\",\"1#71038\",\"1#71038\",\"1#71038\",\"1#71038\",\"1#71038\",\"1#71038\",\"2#71038\",\"2#71038\",\"2#71038\",\"2#71038\",\"2#71038\",\"2#71038\",\"2#71038\",\"2#71038\",\"2#71038\",\"2#71038\",\"3#71038\",\"3#71038\",\"3#71038\",\"3#71038\",\"3#71038\",\"3#71038\",\"3#71038\",\"3#71038\",\"3#71038\",\"3#71038\",\"3#71038\",\"3#71038\",\"3#71038\",\"3#71038\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"last_stand\",\"blast_accounting\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"last_stand\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":19,\"heal_gained\":6,\"hp_after\":35,\"hp_before\":29,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"last_stand\",\"feat_name\":\"背水\",\"hp_after\":35,\"hp_before\":35,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":20,\"heal_gained\":6,\"hp_after\":38,\"hp_before\":32,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blast_accounting\",\"feat_name\":\"爆破会计\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":97,\"route\":\"min\",\"seal_hit\":316,\"seal_hit_gain\":941,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":335,\"seed\":71038,\"shop_gp_seq\":[30,60,95],\"stopped_at\":0,\"taken\":127,\"visited_count\":31,\"wallet_count\":37}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":19,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,41,41,41,41,41,41,41,47],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":141,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#71039\",\"1#71039\",\"1#71039\",\"1#71039\",\"1#71039\",\"1#71039\",\"1#71039\",\"2#71039\",\"2#71039\",\"2#71039\",\"2#71039\",\"2#71039\",\"2#71039\",\"2#71039\",\"2#71039\",\"2#71039\",\"2#71039\",\"3#71039\",\"3#71039\",\"3#71039\",\"3#71039\",\"3#71039\",\"3#71039\",\"3#71039\",\"3#71039\",\"3#71039\",\"3#71039\",\"3#71039\",\"3#71039\",\"3#71039\",\"3#71039\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"skip_mining\",\"last_drop\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":16,\"heal_gained\":7,\"hp_after\":23,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"skip_mining\",\"feat_name\":\"跳格采掘\",\"hp_after\":23,\"hp_before\":23,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":34,\"heal_gained\":8,\"hp_after\":36,\"hp_before\":28,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"last_drop\",\"feat_name\":\"一滴不剩\",\"hp_after\":36,\"hp_before\":36,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":91,\"route\":\"min\",\"seal_hit\":316,\"seal_hit_gain\":955,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":332,\"seed\":71039,\"shop_gp_seq\":[39,76,84],\"stopped_at\":0,\"taken\":141,\"visited_count\":31,\"wallet_count\":38}"
],
"jsonl": {
"normal": {
"sha256": "19ee7ccdfb3780f5152b30f830c1168852c4fb4df73e2c27463a3a4695ef35ed",
"rows": 9216
},
"perfect": {
"sha256": "962b6f68440bcf95f2b21ac4cc07a8523655f044ae6355aa674d0db27bdecc1d",
"rows": 60774
}
}
},
"attune": {
"runsum": [
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":8,\"choices\":7,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":38,\"hp_pct\":0,\"keystone_total\":3,\"level_maps\":[\"1#72000\",\"1#72000\",\"1#72000\",\"1#72000\",\"1#72000\",\"1#72000\",\"1#72000\",\"2#72000\",\"2#72000\"],\"levels_cleared\":1,\"mine_dmg\":22,\"mine_hits\":11,\"policy\":\"normal\",\"refreshes\":1,\"relics\":[\"seal_stake\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":18,\"heal_gained\":7,\"hp_after\":14,\"hp_before\":7,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_stake\",\"feat_name\":\"封印桩\",\"hp_after\":14,\"hp_before\":14,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":32,\"route\":\"min\",\"seal_hit\":91,\"seal_hit_gain\":738,\"seal_miss\":9,\"seal_miss_penalty\":-2,\"seal_placed\":108,\"seed\":72000,\"shop_gp_seq\":[29],\"stopped_at\":2,\"taken\":60,\"visited_count\":9,\"wallet_count\":15}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,32,32],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":39,\"hp_pct\":0,\"keystone_total\":3,\"level_maps\":[\"1#72001\",\"1#72001\",\"1#72001\",\"1#72001\",\"1#72001\",\"1#72001\",\"1#72001\",\"2#72001\"],\"levels_cleared\":1,\"mine_dmg\":14,\"mine_hits\":10,\"policy\":\"normal\",\"refreshes\":1,\"relics\":[\"skip_mining\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":27,\"heal_gained\":6,\"hp_after\":9,\"hp_before\":3,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"skip_mining\",\"feat_name\":\"跳格采掘\",\"hp_after\":9,\"hp_before\":9,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":23,\"route\":\"min\",\"seal_hit\":63,\"seal_hit_gain\":556,\"seal_miss\":9,\"seal_miss_penalty\":20,\"seal_placed\":78,\"seed\":72001,\"shop_gp_seq\":[17],\"stopped_at\":2,\"taken\":53,\"visited_count\":8,\"wallet_count\":10}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":3,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":9,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#72002\",\"1#72002\",\"1#72002\",\"1#72002\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":4,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":5,\"route\":\"min\",\"seal_hit\":11,\"seal_hit_gain\":-127,\"seal_miss\":5,\"seal_miss_penalty\":8,\"seal_placed\":23,\"seed\":72002,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":25,\"visited_count\":4,\"wallet_count\":4}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":5,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":28,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#72003\",\"1#72003\",\"1#72003\",\"1#72003\",\"1#72003\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":7,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":16,\"route\":\"min\",\"seal_hit\":60,\"seal_hit_gain\":234,\"seal_miss\":6,\"seal_miss_penalty\":8,\"seal_placed\":70,\"seed\":72003,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":36,\"visited_count\":5,\"wallet_count\":6}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":13,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#72004\",\"1#72004\",\"1#72004\",\"1#72004\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":9,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":8,\"route\":\"min\",\"seal_hit\":21,\"seal_hit_gain\":323,\"seal_miss\":5,\"seal_miss_penalty\":6,\"seal_placed\":28,\"seed\":72004,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":29,\"visited_count\":4,\"wallet_count\":5}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":21,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#72005\",\"1#72005\",\"1#72005\",\"1#72005\"],\"levels_cleared\":0,\"mine_dmg\":6,\"mine_hits\":7,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":12,\"route\":\"min\",\"seal_hit\":35,\"seal_hit_gain\":321,\"seal_miss\":3,\"seal_miss_penalty\":4,\"seal_placed\":42,\"seed\":72005,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":27,\"visited_count\":4,\"wallet_count\":5}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":5,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":17,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#72006\",\"1#72006\",\"1#72006\",\"1#72006\",\"1#72006\"],\"levels_cleared\":0,\"mine_dmg\":6,\"mine_hits\":8,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":16,\"route\":\"min\",\"seal_hit\":48,\"seal_hit_gain\":316,\"seal_miss\":7,\"seal_miss_penalty\":14,\"seal_placed\":68,\"seed\":72006,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":23,\"visited_count\":5,\"wallet_count\":6}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":10,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#72007\",\"1#72007\",\"1#72007\"],\"levels_cleared\":0,\"mine_dmg\":0,\"mine_hits\":2,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":7,\"route\":\"min\",\"seal_hit\":26,\"seal_hit_gain\":363,\"seal_miss\":3,\"seal_miss_penalty\":14,\"seal_placed\":34,\"seed\":72007,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":10,\"visited_count\":3,\"wallet_count\":4}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":8,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#72008\",\"1#72008\",\"1#72008\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":3,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":8,\"route\":\"min\",\"seal_hit\":27,\"seal_hit_gain\":-20,\"seal_miss\":3,\"seal_miss_penalty\":14,\"seal_placed\":34,\"seed\":72008,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":16,\"visited_count\":3,\"wallet_count\":4}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":5,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":35,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#72009\",\"1#72009\",\"1#72009\",\"1#72009\",\"1#72009\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":11,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":16,\"route\":\"min\",\"seal_hit\":51,\"seal_hit_gain\":281,\"seal_miss\":8,\"seal_miss_penalty\":0,\"seal_placed\":63,\"seed\":72009,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":43,\"visited_count\":5,\"wallet_count\":6}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":11,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#72010\",\"1#72010\",\"1#72010\",\"1#72010\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":4,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":8,\"route\":\"min\",\"seal_hit\":21,\"seal_hit_gain\":-53,\"seal_miss\":4,\"seal_miss_penalty\":8,\"seal_placed\":30,\"seed\":72010,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":19,\"visited_count\":4,\"wallet_count\":5}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":7,\"choices\":7,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,32,32,32],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":44,\"hp_pct\":0,\"keystone_total\":3,\"level_maps\":[\"1#72011\",\"1#72011\",\"1#72011\",\"1#72011\",\"1#72011\",\"1#72011\",\"1#72011\",\"2#72011\",\"2#72011\"],\"levels_cleared\":1,\"mine_dmg\":30,\"mine_hits\":16,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[\"map_exchange\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":21,\"heal_gained\":6,\"hp_after\":17,\"hp_before\":11,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"map_exchange\",\"feat_name\":\"弃图兑换\",\"hp_after\":17,\"hp_before\":17,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":34,\"route\":\"min\",\"seal_hit\":80,\"seal_hit_gain\":200,\"seal_miss\":15,\"seal_miss_penalty\":24,\"seal_placed\":101,\"seed\":72011,\"shop_gp_seq\":[16],\"stopped_at\":2,\"taken\":74,\"visited_count\":9,\"wallet_count\":12}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":5,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#72012\",\"1#72012\",\"1#72012\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":3,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":6,\"route\":\"min\",\"seal_hit\":18,\"seal_hit_gain\":107,\"seal_miss\":5,\"seal_miss_penalty\":14,\"seal_placed\":25,\"seed\":72012,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":13,\"visited_count\":3,\"wallet_count\":4}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":4,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":14,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#72013\",\"1#72013\",\"1#72013\",\"1#72013\",\"1#72013\"],\"levels_cleared\":0,\"mine_dmg\":14,\"mine_hits\":10,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":11,\"route\":\"min\",\"seal_hit\":38,\"seal_hit_gain\":289,\"seal_miss\":14,\"seal_miss_penalty\":16,\"seal_placed\":54,\"seed\":72013,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":28,\"visited_count\":5,\"wallet_count\":5}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":7,\"choices\":7,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,32,32,32],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":34,\"hp_pct\":0,\"keystone_total\":3,\"level_maps\":[\"1#72014\",\"1#72014\",\"1#72014\",\"1#72014\",\"1#72014\",\"1#72014\",\"1#72014\",\"2#72014\",\"2#72014\"],\"levels_cleared\":1,\"mine_dmg\":16,\"mine_hits\":7,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[\"seal_echo\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"seal_echo\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"trap_instinct\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":18,\"heal_gained\":6,\"hp_after\":22,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_echo\",\"feat_name\":\"封印回响\",\"hp_after\":22,\"hp_before\":22,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":32,\"route\":\"min\",\"seal_hit\":85,\"seal_hit_gain\":373,\"seal_miss\":11,\"seal_miss_penalty\":12,\"seal_placed\":104,\"seed\":72014,\"shop_gp_seq\":[19],\"stopped_at\":2,\"taken\":50,\"visited_count\":9,\"wallet_count\":12}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":7,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#72015\",\"1#72015\",\"1#72015\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":4,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":5,\"route\":\"min\",\"seal_hit\":22,\"seal_hit_gain\":243,\"seal_miss\":3,\"seal_miss_penalty\":8,\"seal_placed\":26,\"seed\":72015,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":23,\"visited_count\":3,\"wallet_count\":4}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":6,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,32,32],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":28,\"hp_pct\":0,\"keystone_total\":3,\"level_maps\":[\"1#72016\",\"1#72016\",\"1#72016\",\"1#72016\",\"1#72016\",\"1#72016\",\"1#72016\",\"2#72016\"],\"levels_cleared\":1,\"mine_dmg\":16,\"mine_hits\":11,\"policy\":\"normal\",\"refreshes\":1,\"relics\":[\"perfect_insurance\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"perfect_insurance\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":29,\"heal_gained\":6,\"hp_after\":11,\"hp_before\":5,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"perfect_insurance\",\"feat_name\":\"完美保险\",\"hp_after\":11,\"hp_before\":11,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":25,\"route\":\"min\",\"seal_hit\":78,\"seal_hit_gain\":740,\"seal_miss\":10,\"seal_miss_penalty\":24,\"seal_placed\":92,\"seed\":72016,\"shop_gp_seq\":[15],\"stopped_at\":2,\"taken\":44,\"visited_count\":8,\"wallet_count\":11}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":18,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#72017\",\"1#72017\",\"1#72017\",\"1#72017\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":9,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":9,\"route\":\"min\",\"seal_hit\":30,\"seal_hit_gain\":-413,\"seal_miss\":9,\"seal_miss_penalty\":8,\"seal_placed\":40,\"seed\":72017,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":26,\"visited_count\":4,\"wallet_count\":5}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":1,\"choices\":0,\"cleared\":false,\"encounter_max_hp_seq\":[26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":2,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#72018\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":2,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":1,\"route\":\"min\",\"seal_hit\":2,\"seal_hit_gain\":64,\"seal_miss\":3,\"seal_miss_penalty\":8,\"seal_placed\":7,\"seed\":72018,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":18,\"visited_count\":1,\"wallet_count\":2}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":3,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":9,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#72019\",\"1#72019\",\"1#72019\",\"1#72019\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":4,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":7,\"route\":\"min\",\"seal_hit\":24,\"seal_hit_gain\":319,\"seal_miss\":5,\"seal_miss_penalty\":22,\"seal_placed\":33,\"seed\":72019,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":17,\"visited_count\":4,\"wallet_count\":4}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":17,\"choices\":20,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,32,32,32,32,32,32,38,38,38,38,38,38],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":128,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#72020\",\"1#72020\",\"1#72020\",\"1#72020\",\"1#72020\",\"1#72020\",\"1#72020\",\"2#72020\",\"2#72020\",\"2#72020\",\"2#72020\",\"2#72020\",\"2#72020\",\"2#72020\",\"2#72020\",\"2#72020\",\"2#72020\",\"3#72020\",\"3#72020\",\"3#72020\",\"3#72020\",\"3#72020\",\"3#72020\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"geologist\",\"skip_mining\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":19,\"heal_gained\":6,\"hp_after\":32,\"hp_before\":26,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"geologist\",\"feat_name\":\"地质学家\",\"hp_after\":32,\"hp_before\":32,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":19,\"heal_gained\":1,\"hp_after\":38,\"hp_before\":37,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"skip_mining\",\"feat_name\":\"跳格采掘\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":79,\"route\":\"min\",\"seal_hit\":267,\"seal_hit_gain\":884,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":285,\"seed\":72020,\"shop_gp_seq\":[14,63],\"stopped_at\":3,\"taken\":128,\"visited_count\":23,\"wallet_count\":28}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":19,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,32,32,32,32,32,32,41,41,41,41,41,41,41,44],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":160,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#72021\",\"1#72021\",\"1#72021\",\"1#72021\",\"1#72021\",\"1#72021\",\"1#72021\",\"2#72021\",\"2#72021\",\"2#72021\",\"2#72021\",\"2#72021\",\"2#72021\",\"2#72021\",\"2#72021\",\"2#72021\",\"2#72021\",\"3#72021\",\"3#72021\",\"3#72021\",\"3#72021\",\"3#72021\",\"3#72021\",\"3#72021\",\"3#72021\",\"3#72021\",\"3#72021\",\"3#72021\",\"3#72021\",\"3#72021\",\"3#72021\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"breaker_strike\",\"map_exchange\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":33,\"heal_gained\":6,\"hp_after\":27,\"hp_before\":21,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"breaker_strike\",\"feat_name\":\"破封一击\",\"hp_after\":27,\"hp_before\":27,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":21,\"heal_gained\":4,\"hp_after\":41,\"hp_before\":37,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"map_exchange\",\"feat_name\":\"弃图兑换\",\"hp_after\":41,\"hp_before\":41,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":97,\"route\":\"min\",\"seal_hit\":352,\"seal_hit_gain\":1330,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":373,\"seed\":72021,\"shop_gp_seq\":[22,77,90],\"stopped_at\":0,\"taken\":160,\"visited_count\":31,\"wallet_count\":37}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":17,\"choices\":19,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,32,32,32,32,32,32,38,38,38,38,38,38],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":124,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#72022\",\"1#72022\",\"1#72022\",\"1#72022\",\"1#72022\",\"1#72022\",\"1#72022\",\"2#72022\",\"2#72022\",\"2#72022\",\"2#72022\",\"2#72022\",\"2#72022\",\"2#72022\",\"2#72022\",\"2#72022\",\"2#72022\",\"3#72022\",\"3#72022\",\"3#72022\",\"3#72022\",\"3#72022\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":2,\"relics\":[\"hard_hat\",\"frontier_miner\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":26,\"heal_gained\":1,\"hp_after\":32,\"hp_before\":31,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"hard_hat\",\"feat_name\":\"安全帽\",\"hp_after\":32,\"hp_before\":32,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":29,\"heal_gained\":7,\"hp_after\":37,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"frontier_miner\",\"feat_name\":\"边陲矿工\",\"hp_after\":37,\"hp_before\":37,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":81,\"route\":\"min\",\"seal_hit\":300,\"seal_hit_gain\":1123,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":329,\"seed\":72022,\"shop_gp_seq\":[17,73],\"stopped_at\":3,\"taken\":124,\"visited_count\":22,\"wallet_count\":29}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":17,\"choices\":19,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,38,38,38,38,38,38],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":115,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#72023\",\"1#72023\",\"1#72023\",\"1#72023\",\"1#72023\",\"1#72023\",\"1#72023\",\"2#72023\",\"2#72023\",\"2#72023\",\"2#72023\",\"2#72023\",\"2#72023\",\"2#72023\",\"2#72023\",\"2#72023\",\"2#72023\",\"3#72023\",\"3#72023\",\"3#72023\",\"3#72023\",\"3#72023\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"blood_honey\",\"executioner\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":11,\"heal_gained\":5,\"hp_after\":35,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blood_honey\",\"feat_name\":\"血蜜\",\"hp_after\":35,\"hp_before\":35,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":31,\"heal_gained\":4,\"hp_after\":38,\"hp_before\":34,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":75,\"route\":\"min\",\"seal_hit\":286,\"seal_hit_gain\":643,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":304,\"seed\":72023,\"shop_gp_seq\":[34,48],\"stopped_at\":3,\"taken\":115,\"visited_count\":22,\"wallet_count\":29}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":20,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,38,38,38,38,38,38,38,38,41],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":75,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#72024\",\"1#72024\",\"1#72024\",\"1#72024\",\"1#72024\",\"1#72024\",\"1#72024\",\"2#72024\",\"2#72024\",\"2#72024\",\"2#72024\",\"2#72024\",\"2#72024\",\"2#72024\",\"2#72024\",\"2#72024\",\"2#72024\",\"3#72024\",\"3#72024\",\"3#72024\",\"3#72024\",\"3#72024\",\"3#72024\",\"3#72024\",\"3#72024\",\"3#72024\",\"3#72024\",\"3#72024\",\"3#72024\",\"3#72024\",\"3#72024\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"seal_burst\",\"perfect_insurance\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"seal_burst\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":11,\"heal_gained\":7,\"hp_after\":32,\"hp_before\":25,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_burst\",\"feat_name\":\"封印爆裂\",\"hp_after\":32,\"hp_before\":32,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"perfect_insurance\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":20,\"heal_gained\":2,\"hp_after\":38,\"hp_before\":36,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"perfect_insurance\",\"feat_name\":\"完美保险\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":55,\"route\":\"min\",\"seal_hit\":229,\"seal_hit_gain\":-27,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":256,\"seed\":72024,\"shop_gp_seq\":[34,42,59],\"stopped_at\":0,\"taken\":75,\"visited_count\":31,\"wallet_count\":35}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":23,\"choices\":26,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,32,32,32,32,32,32,38,38,38,38,38,38,38,38,38,38,38,47],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":150,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#72025\",\"1#72025\",\"1#72025\",\"1#72025\",\"1#72025\",\"1#72025\",\"1#72025\",\"2#72025\",\"2#72025\",\"2#72025\",\"2#72025\",\"2#72025\",\"2#72025\",\"2#72025\",\"2#72025\",\"2#72025\",\"2#72025\",\"3#72025\",\"3#72025\",\"3#72025\",\"3#72025\",\"3#72025\",\"3#72025\",\"3#72025\",\"3#72025\",\"3#72025\",\"3#72025\",\"3#72025\",\"3#72025\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"field_dressing\",\"hard_hat\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":15,\"heal_gained\":6,\"hp_after\":29,\"hp_before\":23,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"field_dressing\",\"feat_name\":\"战地包扎\",\"hp_after\":29,\"hp_before\":29,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":27,\"heal_gained\":0,\"hp_after\":38,\"hp_before\":38,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"hard_hat\",\"feat_name\":\"安全帽\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":103,\"route\":\"min\",\"seal_hit\":408,\"seal_hit_gain\":2142,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":435,\"seed\":72025,\"shop_gp_seq\":[22,64,116],\"stopped_at\":0,\"taken\":150,\"visited_count\":29,\"wallet_count\":42}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":15,\"choices\":17,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,38,38,38,38],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":121,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#72026\",\"1#72026\",\"1#72026\",\"1#72026\",\"1#72026\",\"1#72026\",\"1#72026\",\"2#72026\",\"2#72026\",\"2#72026\",\"2#72026\",\"2#72026\",\"2#72026\",\"2#72026\",\"2#72026\",\"2#72026\",\"2#72026\",\"3#72026\",\"3#72026\",\"3#72026\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"frontier_miner\",\"perfect_insurance\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":15,\"heal_gained\":7,\"hp_after\":23,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"frontier_miner\",\"feat_name\":\"边陲矿工\",\"hp_after\":23,\"hp_before\":23,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"perfect_insurance\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"trap_instinct\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":26,\"heal_gained\":3,\"hp_after\":38,\"hp_before\":35,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"perfect_insurance\",\"feat_name\":\"完美保险\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":64,\"route\":\"min\",\"seal_hit\":227,\"seal_hit_gain\":370,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":245,\"seed\":72026,\"shop_gp_seq\":[35,51],\"stopped_at\":3,\"taken\":121,\"visited_count\":20,\"wallet_count\":27}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":18,\"choices\":21,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,32,32,32,32,32,32,38,38,38,38,38,38,38],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":125,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#72027\",\"1#72027\",\"1#72027\",\"1#72027\",\"1#72027\",\"1#72027\",\"1#72027\",\"2#72027\",\"2#72027\",\"2#72027\",\"2#72027\",\"2#72027\",\"2#72027\",\"2#72027\",\"2#72027\",\"2#72027\",\"2#72027\",\"3#72027\",\"3#72027\",\"3#72027\",\"3#72027\",\"3#72027\",\"3#72027\",\"3#72027\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"geologist\",\"last_drop\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":20,\"heal_gained\":2,\"hp_after\":32,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"geologist\",\"feat_name\":\"地质学家\",\"hp_after\":32,\"hp_before\":32,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":24,\"heal_gained\":6,\"hp_after\":38,\"hp_before\":32,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"last_drop\",\"feat_name\":\"一滴不剩\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":78,\"route\":\"min\",\"seal_hit\":277,\"seal_hit_gain\":1113,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":301,\"seed\":72027,\"shop_gp_seq\":[23,55],\"stopped_at\":3,\"taken\":125,\"visited_count\":24,\"wallet_count\":29}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":15,\"choices\":17,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,38,38,38,38],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":102,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#72028\",\"1#72028\",\"1#72028\",\"1#72028\",\"1#72028\",\"1#72028\",\"1#72028\",\"2#72028\",\"2#72028\",\"2#72028\",\"2#72028\",\"2#72028\",\"2#72028\",\"2#72028\",\"2#72028\",\"2#72028\",\"2#72028\",\"3#72028\",\"3#72028\",\"3#72028\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"executioner\",\"seal_echo\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":14,\"heal_gained\":7,\"hp_after\":28,\"hp_before\":21,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"hp_after\":28,\"hp_before\":28,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"seal_echo\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":27,\"heal_gained\":4,\"hp_after\":38,\"hp_before\":34,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_echo\",\"feat_name\":\"封印回响\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":67,\"route\":\"min\",\"seal_hit\":244,\"seal_hit_gain\":1907,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":262,\"seed\":72028,\"shop_gp_seq\":[26,50],\"stopped_at\":3,\"taken\":102,\"visited_count\":20,\"wallet_count\":27}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":19,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,32,32,32,32,32,32,38,38,38,38,38,38,38,44],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":124,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#72029\",\"1#72029\",\"1#72029\",\"1#72029\",\"1#72029\",\"1#72029\",\"1#72029\",\"2#72029\",\"2#72029\",\"2#72029\",\"2#72029\",\"2#72029\",\"2#72029\",\"2#72029\",\"2#72029\",\"2#72029\",\"2#72029\",\"3#72029\",\"3#72029\",\"3#72029\",\"3#72029\",\"3#72029\",\"3#72029\",\"3#72029\",\"3#72029\",\"3#72029\",\"3#72029\",\"3#72029\",\"3#72029\",\"3#72029\",\"3#72029\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"frontier_miner\",\"warning_beacon\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":22,\"heal_gained\":2,\"hp_after\":32,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"frontier_miner\",\"feat_name\":\"边陲矿工\",\"hp_after\":32,\"hp_before\":32,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":22,\"heal_gained\":7,\"hp_after\":37,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"warning_beacon\",\"feat_name\":\"示警烟火\",\"hp_after\":37,\"hp_before\":37,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":87,\"route\":\"min\",\"seal_hit\":345,\"seal_hit_gain\":1813,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":369,\"seed\":72029,\"shop_gp_seq\":[13,77,90],\"stopped_at\":0,\"taken\":124,\"visited_count\":31,\"wallet_count\":36}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":19,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,41,41,41,41,41,41,41,47],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":125,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#72030\",\"1#72030\",\"1#72030\",\"1#72030\",\"1#72030\",\"1#72030\",\"1#72030\",\"2#72030\",\"2#72030\",\"2#72030\",\"2#72030\",\"2#72030\",\"2#72030\",\"2#72030\",\"2#72030\",\"2#72030\",\"2#72030\",\"3#72030\",\"3#72030\",\"3#72030\",\"3#72030\",\"3#72030\",\"3#72030\",\"3#72030\",\"3#72030\",\"3#72030\",\"3#72030\",\"3#72030\",\"3#72030\",\"3#72030\",\"3#72030\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"executioner\",\"trap_instinct\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":16,\"heal_gained\":7,\"hp_after\":34,\"hp_before\":27,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"hp_after\":34,\"hp_before\":34,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"trap_instinct\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":30,\"heal_gained\":2,\"hp_after\":41,\"hp_before\":39,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"trap_instinct\",\"feat_name\":\"陷阱本能\",\"hp_after\":41,\"hp_before\":41,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":92,\"route\":\"min\",\"seal_hit\":335,\"seal_hit_gain\":1855,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":358,\"seed\":72030,\"shop_gp_seq\":[32,90,103],\"stopped_at\":0,\"taken\":125,\"visited_count\":31,\"wallet_count\":39}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":16,\"choices\":18,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,32,32,32,32,32,32,38,38,38,38,38],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":121,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#72031\",\"1#72031\",\"1#72031\",\"1#72031\",\"1#72031\",\"1#72031\",\"1#72031\",\"2#72031\",\"2#72031\",\"2#72031\",\"2#72031\",\"2#72031\",\"2#72031\",\"2#72031\",\"2#72031\",\"2#72031\",\"2#72031\",\"3#72031\",\"3#72031\",\"3#72031\",\"3#72031\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":2,\"relics\":[\"hard_hat\",\"perfect_insurance\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":17,\"heal_gained\":1,\"hp_after\":32,\"hp_before\":31,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"hard_hat\",\"feat_name\":\"安全帽\",\"hp_after\":32,\"hp_before\":32,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"perfect_insurance\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":23,\"heal_gained\":7,\"hp_after\":35,\"hp_before\":28,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"perfect_insurance\",\"feat_name\":\"完美保险\",\"hp_after\":35,\"hp_before\":35,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":72,\"route\":\"min\",\"seal_hit\":249,\"seal_hit_gain\":1056,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":273,\"seed\":72031,\"shop_gp_seq\":[20,50],\"stopped_at\":3,\"taken\":121,\"visited_count\":21,\"wallet_count\":27}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":19,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,32,32,32,32,32,32,38,38,38,38,38,38,38,44],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":176,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#72032\",\"1#72032\",\"1#72032\",\"1#72032\",\"1#72032\",\"1#72032\",\"1#72032\",\"2#72032\",\"2#72032\",\"2#72032\",\"2#72032\",\"2#72032\",\"2#72032\",\"2#72032\",\"2#72032\",\"2#72032\",\"2#72032\",\"3#72032\",\"3#72032\",\"3#72032\",\"3#72032\",\"3#72032\",\"3#72032\",\"3#72032\",\"3#72032\",\"3#72032\",\"3#72032\",\"3#72032\",\"3#72032\",\"3#72032\",\"3#72032\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":3,\"relics\":[\"seal_echo\",\"executioner\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"seal_echo\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":18,\"heal_gained\":6,\"hp_after\":22,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_echo\",\"feat_name\":\"封印回响\",\"hp_after\":22,\"hp_before\":22,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":27,\"heal_gained\":5,\"hp_after\":38,\"hp_before\":33,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":104,\"route\":\"min\",\"seal_hit\":406,\"seal_hit_gain\":523,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":434,\"seed\":72032,\"shop_gp_seq\":[12,58,112],\"stopped_at\":0,\"taken\":176,\"visited_count\":31,\"wallet_count\":36}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":12,\"choices\":15,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,38],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":105,\"hp_pct\":0,\"keystone_total\":3,\"level_maps\":[\"1#72033\",\"1#72033\",\"1#72033\",\"1#72033\",\"1#72033\",\"1#72033\",\"1#72033\",\"2#72033\",\"2#72033\",\"2#72033\",\"2#72033\",\"2#72033\",\"2#72033\",\"2#72033\",\"2#72033\",\"2#72033\",\"2#72033\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"geologist\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":13,\"heal_gained\":7,\"hp_after\":34,\"hp_before\":27,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"geologist\",\"feat_name\":\"地质学家\",\"hp_after\":34,\"hp_before\":34,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":51,\"route\":\"min\",\"seal_hit\":181,\"seal_hit_gain\":512,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":195,\"seed\":72033,\"shop_gp_seq\":[36,46],\"stopped_at\":2,\"taken\":105,\"visited_count\":17,\"wallet_count\":24}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":23,\"choices\":26,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,32,32,32,32,32,32,41,41,41,41,41,41,41,41,41,41,41,47],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":127,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#72034\",\"1#72034\",\"1#72034\",\"1#72034\",\"1#72034\",\"1#72034\",\"1#72034\",\"2#72034\",\"2#72034\",\"2#72034\",\"2#72034\",\"2#72034\",\"2#72034\",\"2#72034\",\"2#72034\",\"2#72034\",\"2#72034\",\"3#72034\",\"3#72034\",\"3#72034\",\"3#72034\",\"3#72034\",\"3#72034\",\"3#72034\",\"3#72034\",\"3#72034\",\"3#72034\",\"3#72034\",\"3#72034\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"map_exchange\",\"seal_burst\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":11,\"heal_gained\":6,\"hp_after\":32,\"hp_before\":26,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"map_exchange\",\"feat_name\":\"弃图兑换\",\"hp_after\":32,\"hp_before\":32,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"chain_traps\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_burst\",\"kind\":\"relic\",\"tier\":\"rare\"}],\"gp\":23,\"heal_gained\":7,\"hp_after\":41,\"hp_before\":34,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_burst\",\"feat_name\":\"封印爆裂\",\"hp_after\":41,\"hp_before\":41,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":80,\"route\":\"min\",\"seal_hit\":312,\"seal_hit_gain\":2332,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":350,\"seed\":72034,\"shop_gp_seq\":[25,72,101],\"stopped_at\":0,\"taken\":127,\"visited_count\":29,\"wallet_count\":42}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":17,\"choices\":19,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,41,41,41,41,41,41],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":125,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#72035\",\"1#72035\",\"1#72035\",\"1#72035\",\"1#72035\",\"1#72035\",\"1#72035\",\"2#72035\",\"2#72035\",\"2#72035\",\"2#72035\",\"2#72035\",\"2#72035\",\"2#72035\",\"2#72035\",\"2#72035\",\"2#72035\",\"3#72035\",\"3#72035\",\"3#72035\",\"3#72035\",\"3#72035\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"seal_echo\",\"perfect_insurance\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_echo\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":12,\"heal_gained\":7,\"hp_after\":32,\"hp_before\":25,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_echo\",\"feat_name\":\"封印回响\",\"hp_after\":32,\"hp_before\":32,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"perfect_insurance\",\"kind\":\"relic\",\"tier\":\"rare\"}],\"gp\":23,\"heal_gained\":1,\"hp_after\":41,\"hp_before\":40,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"perfect_insurance\",\"feat_name\":\"完美保险\",\"hp_after\":41,\"hp_before\":41,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":73,\"route\":\"min\",\"seal_hit\":281,\"seal_hit_gain\":2343,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":308,\"seed\":72035,\"shop_gp_seq\":[28,71],\"stopped_at\":3,\"taken\":125,\"visited_count\":22,\"wallet_count\":30}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":19,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,38,38,38,38,38,38,38,38,44],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":137,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#72036\",\"1#72036\",\"1#72036\",\"1#72036\",\"1#72036\",\"1#72036\",\"1#72036\",\"2#72036\",\"2#72036\",\"2#72036\",\"2#72036\",\"2#72036\",\"2#72036\",\"2#72036\",\"2#72036\",\"2#72036\",\"2#72036\",\"3#72036\",\"3#72036\",\"3#72036\",\"3#72036\",\"3#72036\",\"3#72036\",\"3#72036\",\"3#72036\",\"3#72036\",\"3#72036\",\"3#72036\",\"3#72036\",\"3#72036\",\"3#72036\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"stone_skin\",\"executioner\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":19,\"heal_gained\":4,\"hp_after\":35,\"hp_before\":31,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"stone_skin\",\"feat_name\":\"石肤\",\"hp_after\":35,\"hp_before\":35,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":21,\"heal_gained\":7,\"hp_after\":36,\"hp_before\":29,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"hp_after\":36,\"hp_before\":36,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":97,\"route\":\"min\",\"seal_hit\":358,\"seal_hit_gain\":948,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":385,\"seed\":72036,\"shop_gp_seq\":[38,48,70],\"stopped_at\":0,\"taken\":137,\"visited_count\":31,\"wallet_count\":35}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":23,\"choices\":26,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,32,32,32,32,32,32,38,38,38,38,38,38,38,38,38,38,38,47],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":108,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#72037\",\"1#72037\",\"1#72037\",\"1#72037\",\"1#72037\",\"1#72037\",\"1#72037\",\"2#72037\",\"2#72037\",\"2#72037\",\"2#72037\",\"2#72037\",\"2#72037\",\"2#72037\",\"2#72037\",\"2#72037\",\"2#72037\",\"3#72037\",\"3#72037\",\"3#72037\",\"3#72037\",\"3#72037\",\"3#72037\",\"3#72037\",\"3#72037\",\"3#72037\",\"3#72037\",\"3#72037\",\"3#72037\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"market_close\",\"blood_tax\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":6,\"candidates\":[{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":18,\"heal_gained\":6,\"hp_after\":22,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"market_close\",\"feat_name\":\"收盘\",\"hp_after\":22,\"hp_before\":22,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":12,\"candidates\":[{\"feat_id\":\"blood_tax\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":17,\"heal_gained\":2,\"hp_after\":38,\"hp_before\":36,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blood_tax\",\"feat_name\":\"血税\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":86,\"route\":\"min\",\"seal_hit\":346,\"seal_hit_gain\":1916,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":387,\"seed\":72037,\"shop_gp_seq\":[17,70,125],\"stopped_at\":0,\"taken\":108,\"visited_count\":29,\"wallet_count\":42}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":20,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,41,41,41,41,41,41,41,41,47],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":159,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#72038\",\"1#72038\",\"1#72038\",\"1#72038\",\"1#72038\",\"1#72038\",\"1#72038\",\"2#72038\",\"2#72038\",\"2#72038\",\"2#72038\",\"2#72038\",\"2#72038\",\"2#72038\",\"2#72038\",\"2#72038\",\"2#72038\",\"3#72038\",\"3#72038\",\"3#72038\",\"3#72038\",\"3#72038\",\"3#72038\",\"3#72038\",\"3#72038\",\"3#72038\",\"3#72038\",\"3#72038\",\"3#72038\",\"3#72038\",\"3#72038\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"map_exchange\",\"skip_mining\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":10,\"heal_gained\":5,\"hp_after\":35,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"map_exchange\",\"feat_name\":\"弃图兑换\",\"hp_after\":35,\"hp_before\":35,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":22,\"heal_gained\":6,\"hp_after\":41,\"hp_before\":35,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"skip_mining\",\"feat_name\":\"跳格采掘\",\"hp_after\":41,\"hp_before\":41,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":97,\"route\":\"min\",\"seal_hit\":378,\"seal_hit_gain\":2246,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":401,\"seed\":72038,\"shop_gp_seq\":[32,55,100],\"stopped_at\":0,\"taken\":159,\"visited_count\":31,\"wallet_count\":39}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":15,\"choices\":17,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,35,35,35,35,35,35,41,41,41,41],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"hero_dmg\":100,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#72039\",\"1#72039\",\"1#72039\",\"1#72039\",\"1#72039\",\"1#72039\",\"1#72039\",\"2#72039\",\"2#72039\",\"2#72039\",\"2#72039\",\"2#72039\",\"2#72039\",\"2#72039\",\"2#72039\",\"2#72039\",\"2#72039\",\"3#72039\",\"3#72039\",\"3#72039\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"geologist\",\"pain_memory\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":9,\"candidates\":[{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":20,\"heal_gained\":6,\"hp_after\":35,\"hp_before\":29,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"geologist\",\"feat_name\":\"地质学家\",\"hp_after\":35,\"hp_before\":35,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"pain_memory\",\"kind\":\"relic\",\"tier\":\"rare\"}],\"gp\":29,\"heal_gained\":6,\"hp_after\":41,\"hp_before\":35,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"pain_memory\",\"feat_name\":\"疼痛记忆\",\"hp_after\":41,\"hp_before\":41,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":62,\"route\":\"min\",\"seal_hit\":224,\"seal_hit_gain\":711,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":244,\"seed\":72039,\"shop_gp_seq\":[32,63],\"stopped_at\":3,\"taken\":100,\"visited_count\":20,\"wallet_count\":28}"
],
"jsonl": {
"normal": {
"sha256": "b732bfeda9521de4896ebc11936670bd015032131bc871036896c1761ded12c8",
"rows": 9670
},
"perfect": {
"sha256": "06cb4dcc26c56343c61bc8d972238be81b88e3132d7e9b274399f4cfd04cb56e",
"rows": 62765
}
}
},
"shop": {
"runsum": [
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":9,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73000\",\"1#73000\",\"1#73000\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":4,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":5,\"route\":\"min\",\"seal_hit\":19,\"seal_hit_gain\":-30,\"seal_miss\":5,\"seal_miss_penalty\":8,\"seal_placed\":27,\"seed\":73000,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":17,\"visited_count\":3,\"wallet_count\":4}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":19,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73001\",\"1#73001\",\"1#73001\",\"1#73001\",\"1#73001\"],\"levels_cleared\":0,\"mine_dmg\":0,\"mine_hits\":6,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":15,\"route\":\"min\",\"seal_hit\":43,\"seal_hit_gain\":437,\"seal_miss\":8,\"seal_miss_penalty\":6,\"seal_placed\":56,\"seed\":73001,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":19,\"visited_count\":5,\"wallet_count\":6}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":15,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73002\",\"1#73002\",\"1#73002\",\"1#73002\",\"1#73002\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":6,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":9,\"route\":\"min\",\"seal_hit\":35,\"seal_hit_gain\":-120,\"seal_miss\":6,\"seal_miss_penalty\":8,\"seal_placed\":46,\"seed\":73002,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":23,\"visited_count\":5,\"wallet_count\":6}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":6,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73003\",\"1#73003\",\"1#73003\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":3,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":5,\"route\":\"min\",\"seal_hit\":16,\"seal_hit_gain\":-158,\"seal_miss\":4,\"seal_miss_penalty\":14,\"seal_placed\":22,\"seed\":73003,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":14,\"visited_count\":3,\"wallet_count\":4}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":13,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73004\",\"1#73004\",\"1#73004\",\"1#73004\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":8,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":10,\"route\":\"min\",\"seal_hit\":30,\"seal_hit_gain\":-37,\"seal_miss\":8,\"seal_miss_penalty\":8,\"seal_placed\":47,\"seed\":73004,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":29,\"visited_count\":4,\"wallet_count\":5}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":20,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73005\",\"1#73005\",\"1#73005\",\"1#73005\",\"1#73005\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":5,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":14,\"route\":\"min\",\"seal_hit\":36,\"seal_hit_gain\":596,\"seal_miss\":3,\"seal_miss_penalty\":6,\"seal_placed\":43,\"seed\":73005,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":28,\"visited_count\":5,\"wallet_count\":6}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":6,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,41],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":19,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73006\",\"1#73006\",\"1#73006\",\"1#73006\",\"1#73006\",\"1#73006\",\"1#73006\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":12,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":19},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":21,\"route\":\"min\",\"seal_hit\":61,\"seal_hit_gain\":393,\"seal_miss\":14,\"seal_miss_penalty\":22,\"seal_placed\":81,\"seed\":73006,\"shop_buys\":3,\"shop_gp_seq\":[36],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":27,\"visited_count\":7,\"wallet_count\":10}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":9,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73007\",\"1#73007\",\"1#73007\"],\"levels_cleared\":0,\"mine_dmg\":0,\"mine_hits\":5,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":6,\"route\":\"min\",\"seal_hit\":20,\"seal_hit_gain\":-92,\"seal_miss\":5,\"seal_miss_penalty\":22,\"seal_placed\":25,\"seed\":73007,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":9,\"visited_count\":3,\"wallet_count\":4}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,31],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":25,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73008\",\"1#73008\",\"1#73008\",\"1#73008\",\"1#73008\",\"1#73008\",\"1#73008\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":7,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":21,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":21},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":21,\"route\":\"min\",\"seal_hit\":63,\"seal_hit_gain\":398,\"seal_miss\":8,\"seal_miss_penalty\":14,\"seal_placed\":73,\"seed\":73008,\"shop_buys\":2,\"shop_gp_seq\":[23],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":41,\"visited_count\":7,\"wallet_count\":8}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":23,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73009\",\"1#73009\",\"1#73009\",\"1#73009\"],\"levels_cleared\":0,\"mine_dmg\":0,\"mine_hits\":2,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":11,\"route\":\"min\",\"seal_hit\":34,\"seal_hit_gain\":4,\"seal_miss\":3,\"seal_miss_penalty\":6,\"seal_placed\":40,\"seed\":73009,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":23,\"visited_count\":4,\"wallet_count\":5}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":11,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73010\",\"1#73010\",\"1#73010\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":7,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":10,\"route\":\"min\",\"seal_hit\":25,\"seal_hit_gain\":469,\"seal_miss\":6,\"seal_miss_penalty\":14,\"seal_placed\":37,\"seed\":73010,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":19,\"visited_count\":3,\"wallet_count\":4}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":7,\"choices\":7,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":39,\"hp_pct\":0,\"keystone_total\":3,\"level_maps\":[\"1#73011\",\"1#73011\",\"1#73011\",\"1#73011\",\"1#73011\",\"1#73011\",\"1#73011\",\"2#73011\",\"2#73011\"],\"levels_cleared\":1,\"mine_dmg\":8,\"mine_hits\":6,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":21,\"attack\":22,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[\"blood_honey\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":0,\"candidates\":[{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":17,\"heal_gained\":5,\"hp_after\":21,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blood_honey\",\"feat_name\":\"血蜜\",\"hp_after\":21,\"hp_before\":21,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":28,\"route\":\"min\",\"seal_hit\":79,\"seal_hit_gain\":0,\"seal_miss\":6,\"seal_miss_penalty\":16,\"seal_placed\":89,\"seed\":73011,\"shop_buys\":2,\"shop_gp_seq\":[17],\"shop_mode\":\"pool\",\"stopped_at\":2,\"taken\":47,\"visited_count\":9,\"wallet_count\":10}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,31],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":23,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73012\",\"1#73012\",\"1#73012\",\"1#73012\",\"1#73012\",\"1#73012\",\"1#73012\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":8,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":21},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":21,\"route\":\"min\",\"seal_hit\":57,\"seal_hit_gain\":-348,\"seal_miss\":10,\"seal_miss_penalty\":4,\"seal_placed\":70,\"seed\":73012,\"shop_buys\":1,\"shop_gp_seq\":[10],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":39,\"visited_count\":7,\"wallet_count\":7}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":1,\"choices\":0,\"cleared\":false,\"encounter_max_hp_seq\":[26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":4,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73013\"],\"levels_cleared\":0,\"mine_dmg\":14,\"mine_hits\":4,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":2,\"route\":\"min\",\"seal_hit\":7,\"seal_hit_gain\":114,\"seal_miss\":2,\"seal_miss_penalty\":14,\"seal_placed\":9,\"seed\":73013,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":18,\"visited_count\":1,\"wallet_count\":2}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":2,\"choices\":1,\"cleared\":false,\"encounter_max_hp_seq\":[26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":4,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73014\",\"1#73014\"],\"levels_cleared\":0,\"mine_dmg\":22,\"mine_hits\":4,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":3,\"route\":\"min\",\"seal_hit\":10,\"seal_hit_gain\":240,\"seal_miss\":3,\"seal_miss_penalty\":0,\"seal_placed\":15,\"seed\":73014,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":26,\"visited_count\":2,\"wallet_count\":3}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":19,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73015\",\"1#73015\",\"1#73015\",\"1#73015\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":8,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":8,\"route\":\"min\",\"seal_hit\":24,\"seal_hit_gain\":127,\"seal_miss\":6,\"seal_miss_penalty\":12,\"seal_placed\":32,\"seed\":73015,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":27,\"visited_count\":4,\"wallet_count\":5}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":14,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73016\",\"1#73016\",\"1#73016\"],\"levels_cleared\":0,\"mine_dmg\":14,\"mine_hits\":5,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":10,\"route\":\"min\",\"seal_hit\":20,\"seal_hit_gain\":86,\"seal_miss\":4,\"seal_miss_penalty\":-2,\"seal_placed\":26,\"seed\":73016,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":28,\"visited_count\":3,\"wallet_count\":4}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":2,\"choices\":1,\"cleared\":false,\"encounter_max_hp_seq\":[26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":5,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73017\",\"1#73017\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":3,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":4,\"route\":\"min\",\"seal_hit\":10,\"seal_hit_gain\":320,\"seal_miss\":2,\"seal_miss_penalty\":8,\"seal_placed\":14,\"seed\":73017,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":21,\"visited_count\":2,\"wallet_count\":3}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":13,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73018\",\"1#73018\",\"1#73018\",\"1#73018\"],\"levels_cleared\":0,\"mine_dmg\":0,\"mine_hits\":3,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":10,\"route\":\"min\",\"seal_hit\":24,\"seal_hit_gain\":509,\"seal_miss\":5,\"seal_miss_penalty\":20,\"seal_placed\":33,\"seed\":73018,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":13,\"visited_count\":4,\"wallet_count\":5}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":17,\"hp_pct\":0,\"keystone_total\":0,\"level_maps\":[\"1#73019\",\"1#73019\",\"1#73019\",\"1#73019\"],\"levels_cleared\":0,\"mine_dmg\":14,\"mine_hits\":2,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":8,\"route\":\"min\",\"seal_hit\":26,\"seal_hit_gain\":387,\"seal_miss\":3,\"seal_miss_penalty\":6,\"seal_placed\":35,\"seed\":73019,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":31,\"visited_count\":4,\"wallet_count\":5}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":17,\"choices\":19,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,26,26,36,36,36,36,36,36],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":131,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73020\",\"1#73020\",\"1#73020\",\"1#73020\",\"1#73020\",\"1#73020\",\"1#73020\",\"2#73020\",\"2#73020\",\"2#73020\",\"2#73020\",\"2#73020\",\"2#73020\",\"2#73020\",\"2#73020\",\"2#73020\",\"2#73020\",\"3#73020\",\"3#73020\",\"3#73020\",\"3#73020\",\"3#73020\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":20,\"attack\":20,\"feat_ticket\":0,\"heal\":3,\"max_hp\":20},\"refreshes\":0,\"relics\":[\"executioner\",\"field_dressing\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":0,\"candidates\":[{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"chain_traps\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":30,\"heal_gained\":5,\"hp_after\":21,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"hp_after\":21,\"hp_before\":21,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":10,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":19,\"heal_gained\":7,\"hp_after\":35,\"hp_before\":28,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"field_dressing\",\"feat_name\":\"战地包扎\",\"hp_after\":35,\"hp_before\":35,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":79,\"route\":\"min\",\"seal_hit\":263,\"seal_hit_gain\":1207,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":278,\"seed\":73020,\"shop_buys\":7,\"shop_gp_seq\":[43,60],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":131,\"visited_count\":22,\"wallet_count\":25}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":16,\"choices\":18,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,31,31,31,31,31,31,36,36,36,36,36],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":141,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73021\",\"1#73021\",\"1#73021\",\"1#73021\",\"1#73021\",\"1#73021\",\"1#73021\",\"2#73021\",\"2#73021\",\"2#73021\",\"2#73021\",\"2#73021\",\"2#73021\",\"2#73021\",\"2#73021\",\"2#73021\",\"2#73021\",\"3#73021\",\"3#73021\",\"3#73021\",\"3#73021\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":20,\"attack\":21,\"feat_ticket\":0,\"heal\":3,\"max_hp\":20},\"refreshes\":2,\"relics\":[\"skip_mining\",\"venom_edge\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":5,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":24,\"heal_gained\":1,\"hp_after\":31,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"skip_mining\",\"feat_name\":\"跳格采掘\",\"hp_after\":31,\"hp_before\":31,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":10,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"venom_edge\",\"kind\":\"relic\",\"tier\":\"rare\"}],\"gp\":40,\"heal_gained\":6,\"hp_after\":36,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"venom_edge\",\"feat_name\":\"淬毒刃纹\",\"hp_after\":36,\"hp_before\":36,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":83,\"route\":\"min\",\"seal_hit\":291,\"seal_hit_gain\":2396,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":316,\"seed\":73021,\"shop_buys\":6,\"shop_gp_seq\":[22,79],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":141,\"visited_count\":21,\"wallet_count\":23}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":19,\"choices\":21,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,31,31,31,31,31,31,41,41,41,41,41,41,41,41],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":141,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73022\",\"1#73022\",\"1#73022\",\"1#73022\",\"1#73022\",\"1#73022\",\"1#73022\",\"2#73022\",\"2#73022\",\"2#73022\",\"2#73022\",\"2#73022\",\"2#73022\",\"2#73022\",\"2#73022\",\"2#73022\",\"2#73022\",\"3#73022\",\"3#73022\",\"3#73022\",\"3#73022\",\"3#73022\",\"3#73022\",\"3#73022\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":19,\"attack\":22,\"feat_ticket\":0,\"heal\":3,\"max_hp\":20},\"refreshes\":2,\"relics\":[\"breaker_strike\",\"trap_instinct\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":5,\"candidates\":[{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":23,\"heal_gained\":6,\"hp_after\":23,\"hp_before\":17,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"breaker_strike\",\"feat_name\":\"破封一击\",\"hp_after\":23,\"hp_before\":23,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"trap_instinct\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":26,\"heal_gained\":8,\"hp_after\":39,\"hp_before\":31,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"trap_instinct\",\"feat_name\":\"陷阱本能\",\"hp_after\":39,\"hp_before\":39,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":101,\"route\":\"min\",\"seal_hit\":359,\"seal_hit_gain\":1765,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":374,\"seed\":73022,\"shop_buys\":6,\"shop_gp_seq\":[17,63],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":141,\"visited_count\":24,\"wallet_count\":26}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":19,\"choices\":21,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,26,26,36,36,36,36,36,36,36,36],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":152,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73023\",\"1#73023\",\"1#73023\",\"1#73023\",\"1#73023\",\"1#73023\",\"1#73023\",\"2#73023\",\"2#73023\",\"2#73023\",\"2#73023\",\"2#73023\",\"2#73023\",\"2#73023\",\"2#73023\",\"2#73023\",\"2#73023\",\"3#73023\",\"3#73023\",\"3#73023\",\"3#73023\",\"3#73023\",\"3#73023\",\"3#73023\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":20,\"attack\":19,\"feat_ticket\":0,\"heal\":3,\"max_hp\":21},\"refreshes\":0,\"relics\":[\"map_exchange\",\"perfect_insurance\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":0,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":21,\"heal_gained\":5,\"hp_after\":21,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"map_exchange\",\"feat_name\":\"弃图兑换\",\"hp_after\":21,\"hp_before\":21,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":10,\"candidates\":[{\"feat_id\":\"seal_echo\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"perfect_insurance\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":29,\"heal_gained\":7,\"hp_after\":35,\"hp_before\":28,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"perfect_insurance\",\"feat_name\":\"完美保险\",\"hp_after\":35,\"hp_before\":35,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":87,\"route\":\"min\",\"seal_hit\":285,\"seal_hit_gain\":1841,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":308,\"seed\":73023,\"shop_buys\":7,\"shop_gp_seq\":[30,71],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":152,\"visited_count\":24,\"wallet_count\":27}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":16,\"choices\":19,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,36,36,36,36,36,46,46,46,46,46,46],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":100,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73024\",\"1#73024\",\"1#73024\",\"1#73024\",\"1#73024\",\"1#73024\",\"1#73024\",\"2#73024\",\"2#73024\",\"2#73024\",\"2#73024\",\"2#73024\",\"2#73024\",\"2#73024\",\"2#73024\",\"2#73024\",\"2#73024\",\"3#73024\",\"3#73024\",\"3#73024\",\"3#73024\",\"3#73024\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":21,\"attack\":20,\"feat_ticket\":0,\"heal\":3,\"max_hp\":19},\"refreshes\":2,\"relics\":[\"blood_honey\",\"seal_burst\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":10,\"candidates\":[{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":9,\"heal_gained\":7,\"hp_after\":36,\"hp_before\":29,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blood_honey\",\"feat_name\":\"血蜜\",\"hp_after\":36,\"hp_before\":36,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":20,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_burst\",\"kind\":\"relic\",\"tier\":\"rare\"}],\"gp\":11,\"heal_gained\":9,\"hp_after\":32,\"hp_before\":23,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_burst\",\"feat_name\":\"封印爆裂\",\"hp_after\":32,\"hp_before\":32,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":65,\"route\":\"min\",\"seal_hit\":218,\"seal_hit_gain\":416,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":241,\"seed\":73024,\"shop_buys\":7,\"shop_gp_seq\":[25,55],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":100,\"visited_count\":22,\"wallet_count\":24}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":19,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,36,36,36,36,36,36,41,41,41,41,41,41,41,51],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":157,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#73025\",\"1#73025\",\"1#73025\",\"1#73025\",\"1#73025\",\"1#73025\",\"1#73025\",\"2#73025\",\"2#73025\",\"2#73025\",\"2#73025\",\"2#73025\",\"2#73025\",\"2#73025\",\"2#73025\",\"2#73025\",\"2#73025\",\"3#73025\",\"3#73025\",\"3#73025\",\"3#73025\",\"3#73025\",\"3#73025\",\"3#73025\",\"3#73025\",\"3#73025\",\"3#73025\",\"3#73025\",\"3#73025\",\"3#73025\",\"3#73025\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":19,\"attack\":19,\"feat_ticket\":0,\"heal\":3,\"max_hp\":17},\"refreshes\":3,\"relics\":[\"last_stand\",\"seal_stake\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":10,\"candidates\":[{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_stand\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":26,\"heal_gained\":7,\"hp_after\":30,\"hp_before\":23,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"last_stand\",\"feat_name\":\"背水\",\"hp_after\":30,\"hp_before\":30,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":44,\"heal_gained\":8,\"hp_after\":38,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_stake\",\"feat_name\":\"封印桩\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":118,\"route\":\"min\",\"seal_hit\":367,\"seal_hit_gain\":1643,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":385,\"seed\":73025,\"shop_buys\":12,\"shop_gp_seq\":[33,72,116],\"shop_mode\":\"pool\",\"stopped_at\":0,\"taken\":157,\"visited_count\":31,\"wallet_count\":32}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":15,\"choices\":17,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,36,36,36,36,36,36,36,36,36,36],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":140,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73026\",\"1#73026\",\"1#73026\",\"1#73026\",\"1#73026\",\"1#73026\",\"1#73026\",\"2#73026\",\"2#73026\",\"2#73026\",\"2#73026\",\"2#73026\",\"2#73026\",\"2#73026\",\"2#73026\",\"2#73026\",\"2#73026\",\"3#73026\",\"3#73026\",\"3#73026\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":19,\"attack\":21,\"feat_ticket\":0,\"heal\":3,\"max_hp\":20},\"refreshes\":1,\"relics\":[\"geologist\",\"seal_echo\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":10,\"candidates\":[{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":19,\"heal_gained\":7,\"hp_after\":31,\"hp_before\":24,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"geologist\",\"feat_name\":\"地质学家\",\"hp_after\":31,\"hp_before\":31,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":10,\"candidates\":[{\"feat_id\":\"seal_echo\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"trap_instinct\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":29,\"heal_gained\":7,\"hp_after\":23,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_echo\",\"feat_name\":\"封印回响\",\"hp_after\":23,\"hp_before\":23,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":81,\"route\":\"min\",\"seal_hit\":267,\"seal_hit_gain\":1267,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":280,\"seed\":73026,\"shop_buys\":7,\"shop_gp_seq\":[29,72],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":140,\"visited_count\":20,\"wallet_count\":23}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":15,\"choices\":18,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,26,36,36,36,36,36],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":126,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73027\",\"1#73027\",\"1#73027\",\"1#73027\",\"1#73027\",\"1#73027\",\"1#73027\",\"2#73027\",\"2#73027\",\"2#73027\",\"2#73027\",\"2#73027\",\"2#73027\",\"2#73027\",\"2#73027\",\"2#73027\",\"2#73027\",\"3#73027\",\"3#73027\",\"3#73027\",\"3#73027\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":19,\"attack\":22,\"feat_ticket\":0,\"heal\":3,\"max_hp\":20},\"refreshes\":2,\"relics\":[\"last_drop\",\"executioner\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":0,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":12,\"heal_gained\":1,\"hp_after\":26,\"hp_before\":25,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"last_drop\",\"feat_name\":\"一滴不剩\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":10,\"candidates\":[{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":60,\"heal_gained\":7,\"hp_after\":33,\"hp_before\":26,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"hp_after\":33,\"hp_before\":33,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":78,\"route\":\"min\",\"seal_hit\":276,\"seal_hit_gain\":954,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":289,\"seed\":73027,\"shop_buys\":6,\"shop_gp_seq\":[18,82],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":126,\"visited_count\":21,\"wallet_count\":22}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":15,\"choices\":17,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,31,31,31,31,31,31,31,31,31,31],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":127,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73028\",\"1#73028\",\"1#73028\",\"1#73028\",\"1#73028\",\"1#73028\",\"1#73028\",\"2#73028\",\"2#73028\",\"2#73028\",\"2#73028\",\"2#73028\",\"2#73028\",\"2#73028\",\"2#73028\",\"2#73028\",\"2#73028\",\"3#73028\",\"3#73028\",\"3#73028\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":18,\"attack\":21,\"feat_ticket\":0,\"heal\":3,\"max_hp\":21},\"refreshes\":0,\"relics\":[\"geologist\",\"market_close\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":5,\"candidates\":[{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":17,\"heal_gained\":5,\"hp_after\":31,\"hp_before\":26,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"geologist\",\"feat_name\":\"地质学家\",\"hp_after\":31,\"hp_before\":31,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":5,\"candidates\":[{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":30,\"heal_gained\":6,\"hp_after\":28,\"hp_before\":22,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"market_close\",\"feat_name\":\"收盘\",\"hp_after\":28,\"hp_before\":28,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":87,\"route\":\"min\",\"seal_hit\":225,\"seal_hit_gain\":1838,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":244,\"seed\":73028,\"shop_buys\":7,\"shop_gp_seq\":[27,67],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":127,\"visited_count\":20,\"wallet_count\":23}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":15,\"choices\":17,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,31,31,31,31,31,31,31,31,31,31],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":107,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73029\",\"1#73029\",\"1#73029\",\"1#73029\",\"1#73029\",\"1#73029\",\"1#73029\",\"2#73029\",\"2#73029\",\"2#73029\",\"2#73029\",\"2#73029\",\"2#73029\",\"2#73029\",\"2#73029\",\"2#73029\",\"2#73029\",\"3#73029\",\"3#73029\",\"3#73029\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":18,\"attack\":21,\"feat_ticket\":0,\"heal\":3,\"max_hp\":21},\"refreshes\":1,\"relics\":[\"breaker_strike\",\"stone_skin\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":5,\"candidates\":[{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":28,\"heal_gained\":6,\"hp_after\":12,\"hp_before\":6,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"breaker_strike\",\"feat_name\":\"破封一击\",\"hp_after\":12,\"hp_before\":12,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":5,\"candidates\":[{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":16,\"heal_gained\":6,\"hp_after\":25,\"hp_before\":19,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"stone_skin\",\"feat_name\":\"石肤\",\"hp_after\":25,\"hp_before\":25,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":66,\"route\":\"min\",\"seal_hit\":233,\"seal_hit_gain\":1341,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":248,\"seed\":73029,\"shop_buys\":7,\"shop_gp_seq\":[40,58],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":107,\"visited_count\":20,\"wallet_count\":23}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":16,\"choices\":18,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,31,31,31,31,31,31,51,51,51,51,51],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":156,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73030\",\"1#73030\",\"1#73030\",\"1#73030\",\"1#73030\",\"1#73030\",\"1#73030\",\"2#73030\",\"2#73030\",\"2#73030\",\"2#73030\",\"2#73030\",\"2#73030\",\"2#73030\",\"2#73030\",\"2#73030\",\"2#73030\",\"3#73030\",\"3#73030\",\"3#73030\",\"3#73030\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":20,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":17},\"refreshes\":2,\"relics\":[\"blood_insight\",\"seal_smith\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":5,\"candidates\":[{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":37,\"heal_gained\":6,\"hp_after\":19,\"hp_before\":13,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blood_insight\",\"feat_name\":\"见血开窍\",\"hp_after\":19,\"hp_before\":19,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":25,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_smith\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":57,\"heal_gained\":10,\"hp_after\":39,\"hp_before\":29,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_smith\",\"feat_name\":\"封印匠\",\"hp_after\":39,\"hp_before\":39,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":93,\"route\":\"min\",\"seal_hit\":300,\"seal_hit_gain\":1749,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":313,\"seed\":73030,\"shop_buys\":7,\"shop_gp_seq\":[38,89],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":156,\"visited_count\":21,\"wallet_count\":24}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":15,\"choices\":18,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,31,31,31,31,31,41,41,41,41,41],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":138,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73031\",\"1#73031\",\"1#73031\",\"1#73031\",\"1#73031\",\"1#73031\",\"1#73031\",\"2#73031\",\"2#73031\",\"2#73031\",\"2#73031\",\"2#73031\",\"2#73031\",\"2#73031\",\"2#73031\",\"2#73031\",\"2#73031\",\"3#73031\",\"3#73031\",\"3#73031\",\"3#73031\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":19,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":19},\"refreshes\":1,\"relics\":[\"perfect_insurance\",\"market_close\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":5,\"candidates\":[{\"feat_id\":\"perfect_insurance\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":31,\"heal_gained\":6,\"hp_after\":31,\"hp_before\":25,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"perfect_insurance\",\"feat_name\":\"完美保险\",\"hp_after\":31,\"hp_before\":31,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":38,\"heal_gained\":8,\"hp_after\":34,\"hp_before\":26,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"market_close\",\"feat_name\":\"收盘\",\"hp_after\":34,\"hp_before\":34,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":85,\"route\":\"min\",\"seal_hit\":281,\"seal_hit_gain\":635,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":295,\"seed\":73031,\"shop_buys\":6,\"shop_gp_seq\":[21,66],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":138,\"visited_count\":21,\"wallet_count\":22}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":17,\"choices\":19,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,31,31,31,31,31,31,36,36,36,36,36,36],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":124,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73032\",\"1#73032\",\"1#73032\",\"1#73032\",\"1#73032\",\"1#73032\",\"1#73032\",\"2#73032\",\"2#73032\",\"2#73032\",\"2#73032\",\"2#73032\",\"2#73032\",\"2#73032\",\"2#73032\",\"2#73032\",\"2#73032\",\"3#73032\",\"3#73032\",\"3#73032\",\"3#73032\",\"3#73032\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":19,\"attack\":21,\"feat_ticket\":0,\"heal\":3,\"max_hp\":20},\"refreshes\":0,\"relics\":[\"execution_scholar\",\"venom_edge\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":5,\"candidates\":[{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":21,\"heal_gained\":6,\"hp_after\":23,\"hp_before\":17,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"execution_scholar\",\"feat_name\":\"斩首学者\",\"hp_after\":23,\"hp_before\":23,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":10,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"venom_edge\",\"kind\":\"relic\",\"tier\":\"rare\"}],\"gp\":49,\"heal_gained\":7,\"hp_after\":31,\"hp_before\":24,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"venom_edge\",\"feat_name\":\"淬毒刃纹\",\"hp_after\":31,\"hp_before\":31,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":82,\"route\":\"min\",\"seal_hit\":284,\"seal_hit_gain\":931,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":309,\"seed\":73032,\"shop_buys\":7,\"shop_gp_seq\":[28,90],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":124,\"visited_count\":22,\"wallet_count\":25}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":19,\"choices\":23,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,41,41,41,41,41,41,41,41,41,41,41,41,41,41],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":164,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73033\",\"1#73033\",\"1#73033\",\"1#73033\",\"1#73033\",\"1#73033\",\"1#73033\",\"2#73033\",\"2#73033\",\"2#73033\",\"2#73033\",\"2#73033\",\"2#73033\",\"2#73033\",\"2#73033\",\"2#73033\",\"2#73033\",\"3#73033\",\"3#73033\",\"3#73033\",\"3#73033\",\"3#73033\",\"3#73033\",\"3#73033\",\"3#73033\",\"3#73033\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":18,\"attack\":22,\"feat_ticket\":0,\"heal\":3,\"max_hp\":20},\"refreshes\":1,\"relics\":[\"blood_insight\",\"last_drop\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":24,\"heal_gained\":8,\"hp_after\":28,\"hp_before\":20,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blood_insight\",\"feat_name\":\"见血开窍\",\"hp_after\":28,\"hp_before\":28,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":30,\"heal_gained\":1,\"hp_after\":41,\"hp_before\":40,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"last_drop\",\"feat_name\":\"一滴不剩\",\"hp_after\":41,\"hp_before\":41,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":99,\"route\":\"min\",\"seal_hit\":336,\"seal_hit_gain\":1256,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":352,\"seed\":73033,\"shop_buys\":7,\"shop_gp_seq\":[35,66],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":164,\"visited_count\":26,\"wallet_count\":27}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":17,\"choices\":20,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,36,36,36,36,36,36,36,36,36,36,36,36],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":142,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73034\",\"1#73034\",\"1#73034\",\"1#73034\",\"1#73034\",\"1#73034\",\"1#73034\",\"2#73034\",\"2#73034\",\"2#73034\",\"2#73034\",\"2#73034\",\"2#73034\",\"2#73034\",\"2#73034\",\"2#73034\",\"2#73034\",\"3#73034\",\"3#73034\",\"3#73034\",\"3#73034\",\"3#73034\",\"3#73034\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":18,\"attack\":21,\"feat_ticket\":0,\"heal\":3,\"max_hp\":21},\"refreshes\":1,\"relics\":[\"last_drop\",\"field_dressing\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":10,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":30,\"heal_gained\":7,\"hp_after\":31,\"hp_before\":24,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"last_drop\",\"feat_name\":\"一滴不剩\",\"hp_after\":31,\"hp_before\":31,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":10,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":38,\"heal_gained\":2,\"hp_after\":36,\"hp_before\":34,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"field_dressing\",\"feat_name\":\"战地包扎\",\"hp_after\":36,\"hp_before\":36,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":83,\"route\":\"min\",\"seal_hit\":286,\"seal_hit_gain\":1785,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":302,\"seed\":73034,\"shop_buys\":7,\"shop_gp_seq\":[38,75],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":142,\"visited_count\":23,\"wallet_count\":25}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":17,\"choices\":21,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,31,31,31,31,31,31,31,31,31,31,31,31],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":124,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73035\",\"1#73035\",\"1#73035\",\"1#73035\",\"1#73035\",\"1#73035\",\"1#73035\",\"2#73035\",\"2#73035\",\"2#73035\",\"2#73035\",\"2#73035\",\"2#73035\",\"2#73035\",\"2#73035\",\"2#73035\",\"2#73035\",\"3#73035\",\"3#73035\",\"3#73035\",\"3#73035\",\"3#73035\",\"3#73035\",\"3#73035\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":21,\"attack\":18,\"feat_ticket\":0,\"heal\":3,\"max_hp\":21},\"refreshes\":0,\"relics\":[\"hard_hat\",\"blast_accounting\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":5,\"candidates\":[{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":17,\"heal_gained\":6,\"hp_after\":20,\"hp_before\":14,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"hard_hat\",\"feat_name\":\"安全帽\",\"hp_after\":20,\"hp_before\":20,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":5,\"candidates\":[{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":17,\"heal_gained\":6,\"hp_after\":30,\"hp_before\":24,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blast_accounting\",\"feat_name\":\"爆破会计\",\"hp_after\":30,\"hp_before\":30,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":80,\"route\":\"min\",\"seal_hit\":273,\"seal_hit_gain\":304,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":284,\"seed\":73035,\"shop_buys\":7,\"shop_gp_seq\":[34,56],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":124,\"visited_count\":24,\"wallet_count\":25}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":18,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,36,36,36,36,36,36,51,51,51,51,51,51,56],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":131,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#73036\",\"1#73036\",\"1#73036\",\"1#73036\",\"1#73036\",\"1#73036\",\"1#73036\",\"2#73036\",\"2#73036\",\"2#73036\",\"2#73036\",\"2#73036\",\"2#73036\",\"2#73036\",\"2#73036\",\"2#73036\",\"2#73036\",\"3#73036\",\"3#73036\",\"3#73036\",\"3#73036\",\"3#73036\",\"3#73036\",\"3#73036\",\"3#73036\",\"3#73036\",\"3#73036\",\"3#73036\",\"3#73036\",\"3#73036\",\"3#73036\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":19,\"attack\":19,\"feat_ticket\":0,\"heal\":3,\"max_hp\":17},\"refreshes\":0,\"relics\":[\"frontier_miner\",\"blood_honey\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":10,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":17,\"heal_gained\":7,\"hp_after\":35,\"hp_before\":28,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"frontier_miner\",\"feat_name\":\"边陲矿工\",\"hp_after\":35,\"hp_before\":35,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":25,\"candidates\":[{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":26,\"heal_gained\":10,\"hp_after\":38,\"hp_before\":28,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blood_honey\",\"feat_name\":\"血蜜\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":93,\"route\":\"min\",\"seal_hit\":302,\"seal_hit_gain\":720,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":328,\"seed\":73036,\"shop_buys\":12,\"shop_gp_seq\":[26,106,76],\"shop_mode\":\"pool\",\"stopped_at\":0,\"taken\":131,\"visited_count\":31,\"wallet_count\":31}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":16,\"choices\":18,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":108,\"hp_pct\":0,\"keystone_total\":6,\"level_maps\":[\"1#73037\",\"1#73037\",\"1#73037\",\"1#73037\",\"1#73037\",\"1#73037\",\"1#73037\",\"2#73037\",\"2#73037\",\"2#73037\",\"2#73037\",\"2#73037\",\"2#73037\",\"2#73037\",\"2#73037\",\"2#73037\",\"2#73037\",\"3#73037\",\"3#73037\",\"3#73037\",\"3#73037\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":17,\"attack\":21,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":1,\"relics\":[\"blood_honey\",\"stone_skin\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":0,\"candidates\":[{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":14,\"heal_gained\":5,\"hp_after\":19,\"hp_before\":14,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blood_honey\",\"feat_name\":\"血蜜\",\"hp_after\":19,\"hp_before\":19,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":0,\"candidates\":[{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":51,\"heal_gained\":5,\"hp_after\":21,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"stone_skin\",\"feat_name\":\"石肤\",\"hp_after\":21,\"hp_before\":21,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":78,\"route\":\"min\",\"seal_hit\":250,\"seal_hit_gain\":1410,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":265,\"seed\":73037,\"shop_buys\":7,\"shop_gp_seq\":[27,73],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":108,\"visited_count\":21,\"wallet_count\":24}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":20,\"choices\":28,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,26,26,41,41,41,41,41,41,41,41,41],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":177,\"hp_pct\":0,\"keystone_total\":9,\"level_maps\":[\"1#73038\",\"1#73038\",\"1#73038\",\"1#73038\",\"1#73038\",\"1#73038\",\"1#73038\",\"2#73038\",\"2#73038\",\"2#73038\",\"2#73038\",\"2#73038\",\"2#73038\",\"2#73038\",\"2#73038\",\"2#73038\",\"2#73038\",\"3#73038\",\"3#73038\",\"3#73038\",\"3#73038\",\"3#73038\",\"3#73038\",\"3#73038\",\"3#73038\",\"3#73038\",\"3#73038\",\"3#73038\",\"3#73038\",\"3#73038\",\"3#73038\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":19,\"attack\":18,\"feat_ticket\":0,\"heal\":3,\"max_hp\":19},\"refreshes\":2,\"relics\":[\"trap_burst\",\"field_dressing\"],\"result\":\"completed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":0,\"candidates\":[{\"feat_id\":\"trap_instinct\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"seal_echo\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"trap_burst\",\"kind\":\"relic\",\"tier\":\"rare\"}],\"gp\":27,\"heal_gained\":5,\"hp_after\":25,\"hp_before\":20,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"trap_burst\",\"feat_name\":\"陷阱爆发\",\"hp_after\":25,\"hp_before\":25,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":15,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":32,\"heal_gained\":8,\"hp_after\":37,\"hp_before\":29,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"field_dressing\",\"feat_name\":\"战地包扎\",\"hp_after\":37,\"hp_before\":37,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":111,\"route\":\"min\",\"seal_hit\":364,\"seal_hit_gain\":1711,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":388,\"seed\":73038,\"shop_buys\":11,\"shop_gp_seq\":[26,70,106],\"shop_mode\":\"pool\",\"stopped_at\":0,\"taken\":177,\"visited_count\":31,\"wallet_count\":32}",
"RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":9,\"choices\":8,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"hero_dmg\":58,\"hp_pct\":0,\"keystone_total\":3,\"level_maps\":[\"1#73039\",\"1#73039\",\"1#73039\",\"1#73039\",\"1#73039\",\"1#73039\",\"1#73039\",\"2#73039\",\"2#73039\",\"2#73039\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":20,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[\"breaker_strike\"],\"result\":\"failed\",\"reward_events\":[{\"curio_pick\":{\"aux_cleared\":0,\"bonus_max_hp\":0,\"candidates\":[{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":21,\"heal_gained\":5,\"hp_after\":21,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"breaker_strike\",\"feat_name\":\"破封一击\",\"hp_after\":21,\"hp_before\":21,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":43,\"route\":\"min\",\"seal_hit\":139,\"seal_hit_gain\":2048,\"seal_miss\":0,\"seal_miss_penalty\":0,\"seal_placed\":149,\"seed\":73039,\"shop_buys\":2,\"shop_gp_seq\":[23],\"shop_mode\":\"pool\",\"stopped_at\":2,\"taken\":58,\"visited_count\":10,\"wallet_count\":12}"
],
"jsonl": {
"normal": {
"sha256": "b39d5a5a659776e6e3a25a7a1f24af4d8a98b138b2c52cec846bfd7d394364fb",
"rows": 7124
},
"perfect": {
"sha256": "c3d0d37c5f40d208fb35d323b4c7a11c4f7ebe77f9f0eaba0fc3445f00971277",
"rows": 59435
}
}
}
}
}
+79 -2
View File
@@ -4,6 +4,14 @@
# 零命中/空值=用法错 quit(2)。两个来源分开定性——开发期直跑(默认)仅供最窄确认,不产闸回执、
# 不作验收凭据;闸机关联档选跑(--only-source=gates,由 tools/run_all_gates.sh 在 02 闸传入)
# 是收口验收跑批本体,回执 scope=related。无任何选跑参数=全量态,行为与全量口径完全一致。
# 分片态:追加 `-- --shard=k --shard-count=n`(须成对,1≤k≤n,n≥2 才有意义)——把选套集
# (--only 过滤后)切成 n 片互斥子集,本进程只跑第 k 片;闸机据此多进程并行缩短 02 闸墙钟。
# 分片分配走「串行道优先」:开 RNG 事件日志会话的套件(SERIAL_PATTERNS,new_adventure 调用方)
# 全部钉第 1 片——事件日志按种子命名同文件追加(user://logs/rng_log_<seed>.jsonl),跨片并行
# 写同一文件互踩内容,断言日志纯净序的套件(cross_system_flow)必读错行。新增日志会话套件必须
# 同步登记 SERIAL_PATTERNS,漏登记=并行期日志互踩假红。空片合法(--only 小集 × 多片):空片
# 直接 quit(0) 放行,判定职责由其余片承担。各片独立退出码,由闸机聚合;片内三硬闸与
# 快速失败语义不变,逐套件日志名加 _s<k> 段防跨片覆写。
# 协议:套件清单逐条"套件 → 期望退出码";致命类独立进程(expect=1)。
# 执行形态:逐套件起独立 Godot 子进程实跑
#(OS.get_executable_path() 解析本引擎二进制——--script 模式无 autoload/全局标识符,
@@ -30,6 +38,23 @@ const SUITE_TIMEOUT_DEFAULT_SEC := 120
# 快速失败阈值(可调常量,初值 3):连续 N 个套件超时即中止整轮。
const CONSECUTIVE_TIMEOUT_ABORT := 3
# 串行道套件(路径子串匹配):开 RNG 事件日志会话(new_adventure)的调用方——事件日志按种子
# 命名同文件追加(user://logs/rng_log_<seed>.jsonl),跨分片并行写同一文件互踩;断言日志内容
# 的套件(cross_system_flow)必须读到本会话纯净追加序。分片分配时全部钉第 1 片(见头注分片态)。
# 新增日志会话套件时同步登记此处,漏登记=并行期假红。
const SERIAL_PATTERNS: Array[String] = [
"board_generator_test",
"node_map_generator_test",
"minesweeper_rules_test",
"rng_stream_boot_test",
"cross_system_flow_test",
"node_map_determinism_test",
]
# 分片态成员:0=未分片(单进程原路径);>0=第 _shard_k/_shard_n 片(_run_suite 取名用)。
var _shard_k := 0
var _shard_n := 0
func _init() -> void:
# 选跑态解析:--only=<词[,词…]>(经 -- 分隔的 user args)——对套件 res:// 路径做子串匹配;
@@ -46,6 +71,22 @@ func _init() -> void:
only_terms.append(term)
elif a == "--only-source=gates":
only_from_gates = true
elif a.begins_with("--shard="):
var sv := a.trim_prefix("--shard=").strip_edges()
if sv.is_valid_int():
_shard_k = int(sv)
elif a.begins_with("--shard-count="):
var sn := a.trim_prefix("--shard-count=").strip_edges()
if sn.is_valid_int():
_shard_n = int(sn)
if (_shard_k == 0) != (_shard_n == 0):
push_error("[run_all] --shard 与 --shard-count 必须成对出现")
quit(2)
return
if _shard_n < 0 or (_shard_n > 0 and (_shard_k < 1 or _shard_k > _shard_n)):
push_error("[run_all] 分片参数非法(须 1≤k≤n,n≥1):k=%d n=%d" % [_shard_k, _shard_n])
quit(2)
return
if has_only and only_terms.is_empty():
push_error("[run_all] --only= 空值——用法错(选跑禁把零套件当全量)")
quit(2)
@@ -64,6 +105,14 @@ func _init() -> void:
print("[run_all] GATE FILTERED RUN — 闸机关联档选跑 %d/%d 套件(--only=%s)" % [suites.size(), manifest.size(), ",".join(only_terms)])
else:
print("[run_all] FILTERED RUN — 选跑 %d/%d 套件(--only=%s)——仅供开发期最窄确认,不产闸回执、不作验收凭据" % [suites.size(), manifest.size(), ",".join(only_terms)])
var sharded := _shard_n > 1
if sharded:
suites = _shard_suites(suites, _shard_k, _shard_n)
if suites.is_empty():
print("[run_all] SHARD %d/%d — 本片零套件,空片放行(判定职责由其余片承担)" % [_shard_k, _shard_n])
quit(0)
return
print("[run_all] SHARD %d/%d — 本片 %d 套件" % [_shard_k, _shard_n, suites.size()])
print("[run_all] unit/contract suite entry — %d suites" % suites.size())
if DisplayServer.get_name() != "headless":
push_warning("[run_all] 建议以 --headless 运行(D2 冒烟协议)")
@@ -101,7 +150,10 @@ func _init() -> void:
push_error("[run_all] %d 个套件失败" % failures)
quit(1)
return
print("[run_all] all green (%d suites%s)" % [suites.size(), ",filtered" if filtered else ""])
var tag := ",filtered" if filtered else ""
if sharded:
tag += ",shard %d/%d" % [_shard_k, _shard_n]
print("[run_all] all green (%d suites%s)" % [suites.size(), tag])
quit(0)
@@ -134,6 +186,8 @@ func _suites() -> Array:
{"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值)
{"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # AC⑤/T9——排空零分叉/零丢行/延迟双态
{"path": "res://tests/unit/presentation/standee_fx_sweep_test.gd", "expect": 0}, # 呈现域缺陷扫掠(d2 蓄力档/d4 庆祝句柄/d5+d8 互斥/d12 站桩按 id 解析)
{"path": "res://tests/unit/presentation/tune_sim_batch_parity_test.gd", "expect": 0}, # 测速循环批双实现对拍(帧索引/运行态/进度串/stats/报告五面——信号步进 vs await 基线)
{"path": "res://tests/unit/presentation/standee_present_values_test.gd", "expect": 0}, # 战斗一屏样张批 V5–V9(血条危险红/名字行素底不压地台/徽标 FS_HINT/护甲 PNG 绑定/素材入包)
{"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 奖励节点 UI 直验(盲区补漏)
{"path": "res://tests/unit/presentation/level_map_info_fog_test.gd", "expect": 0}, # 信息迷雾——呈现显隐/Boss 豁免/详情零显示/大图拟合
{"path": "res://tests/unit/presentation/forge_workbench_test.gd", "expect": 0}, # 工作间两区六路线 UI 直验(V13 自动化前置面)
@@ -202,7 +256,8 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> Dictiona
# 后又没有退出码读取 API,故由 cmd /V:ON 在套件退出后把 !ERRORLEVEL! 回显落盘,
# 值与阻塞式 execute 经 cmd 回传的同一。
var proj := ProjectSettings.globalize_path("res://")
var log := proj + "/tests/.gates/run_all%s_%02d.log" % ["_filter" if filtered else "", idx]
var shard_tag := "" if _shard_n == 0 else "_s%d" % _shard_k
var log := proj + "/tests/.gates/run_all%s%s_%02d.log" % ["_filter" if filtered else "", shard_tag, idx]
var codefile := log + ".code"
if FileAccess.file_exists(codefile):
DirAccess.remove_absolute(codefile) # 陈值防护:上轮临界残件不许冒充本轮退出码
@@ -292,6 +347,28 @@ func _filter_suites(manifest: Array, terms: PackedStringArray) -> Array:
return out
# 分片分配:串行道套件(SERIAL_PATTERNS 命中)全钉第 1 片;其余按片轮转(片内相对序 j%n+1)。
# 分配对每个 k 确定性、互斥且完备——各片并起来=传入全集零重复零遗漏。
func _shard_suites(suites: Array, k: int, n: int) -> Array:
var out: Array = []
var j := 0
for suite in suites:
var path: String = suite.get("path", "")
var is_serial := false
for pat in SERIAL_PATTERNS:
if path.contains(pat):
is_serial = true
break
if is_serial:
if k == 1:
out.append(suite)
else:
if j % n + 1 == k:
out.append(suite)
j += 1
return out
# 引擎二进制必须取 console 版:OS.get_executable_path() 返回的是 GUI 子系统 exe
#(Godot_v4.7.2-stable_win64.exe),它起的子进程 stdout/stderr 句柄无效——输出全丢,
# 脚本错误扫描就成了扫空气。GODOT_BIN 优先,其次按命名推导 *_console.exe。
+4 -4
View File
@@ -67,9 +67,9 @@ func _check_main_scene(main: Node) -> void:
return
var run_scope: Node = main.get("run_scope")
check(run_scope != null and run_scope.is_inside_tree(), "闲时会话 run_scope 在树")
var start_overlay: Control = main.get("_start_overlay")
var start_overlay: Control = main.get("ui").screens.start_overlay
check(start_overlay != null and start_overlay.visible, "启动态菜单可见(boot 后玩家见菜单)")
check(main.get("settings_store") != null, "系统设置存储已装配(settings_store)")
check(main.get("debug_badge") != null, "调试入口角标已装配(debug_badge)")
check(main.get("legend_overlay") != null, "图例覆盖层已装配(legend_overlay)")
check(main.get("status_panel") != null, "局内状态面板已装配(status_panel)")
check(main.get("ui").screens.debug_badge != null, "调试入口角标已装配(debug_badge)")
check(main.get("ui").modals.legend_overlay != null, "图例覆盖层已装配(legend_overlay)")
check(main.get("ui").modals.status_panel != null, "局内状态面板已装配(status_panel)")
+3 -3
View File
@@ -308,13 +308,13 @@ fi
# 可回放纯逻辑步骤流(同种子同输入逐位复现),await 会打断确定性——「模块归属」B1 行已写明
# 「禁 await——切片 88 冒烟实证」。本闸为防回归防线(2026-10-24 人工 grep 盘面干净)。
# 扫描面:B1 域文件集(与 architecture.md「模块归属」B1 行同口径)=battle_orchestrator.gd
# + battle_fx_host.gd + reward_grant.gd 两拆件 + settlement/ 五段段类 + damage_controller.gd,共 9 文件,显式清单见 S8_FILES。
# + battle_fx_host.gd + reward_grant.gd + board_installer.gd 三拆件 + settlement/ 五段段类 + damage_controller.gd,共 10 文件,显式清单见 S8_FILES。
# 执行侧字面值豁免:脚本必须带字面值才能工作;文件集与架构文档冲突时以 B1 域文件集为准。
# 判据:文件集内出现 await 关键字即红(词边界 \bawait\b,任何形态——await 信号/协程/
# get_tree());排除注释行(行首 #/##)与字符串字面量(双引号包裹段内的 await 非代码),
# 口径仿 S1/S2 的 grep 排除手法。
# 自检:清单内文件缺位(路径写错/被改名)即判红——防扫面为空静默绿。
S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd"
S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd"
S8_MISS=0
for f in $S8_FILES; do
if [ ! -f "$f" ]; then
@@ -356,7 +356,7 @@ fi
# 自检:扫描面为空即判红(require_scan_face);B1 文件集与三族自家文件缺位即判红
# (文件被改名=豁免名单失效,宁可红不可静默放行)。
if require_scan_face "S7" "$DIR_CORE"; then
S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/damage_controller.gd"
S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd"
S7_MISS=0
for f in $S7_B1_SET game/core/unit_container.gd game/core/status_container.gd game/core/reward_economy.gd; do
if [ ! -f "$f" ]; then
+41 -41
View File
@@ -81,20 +81,20 @@ func _smoke_scene_split() -> void:
# V1a 启动态顶层断言:boot 后自树顶向下第一个可见内容层=start_overlay(headless 可判)
#(内容层遍历——屏壳是容器不当候选:camp 壳 boot 态 vis=true 但成员恒隐,裸树遍历会误命中)
var top: Node = _topmost_visible_layer(main)
check(top == main._start_overlay,
check(top == main.ui.screens.start_overlay,
"V1a 启动态顶层可见层=start_overlay(实测顶层=%s——菜单被冒险屏盖住即红)〔装配生命批 V1〕"
% (top.name if top != null else "null"))
# V1b 语义关系:start_overlay 在冒险屏内容之上(菜单屏壳在冒险屏壳之上——装配序修复判据)
check(main._menu_screen.get_index() > main._adventure_screen.get_index(),
check(main.ui.screens.menu_screen.get_index() > main.ui.screens.adventure_screen.get_index(),
"V1b 菜单屏壳在冒险屏壳之上(start_overlay 盖住战斗内容)〔装配生命批 V1〕")
# V1c 合同树序链逐对保序(直接子层全链=屏壳/模态/全屏态/非视觉节点;宿住层树序随宿主不参与)
var chain_detail := ""
var chain_prev: Node = null
var chain_ok := true
for entry: Dictionary in main.LAYER_CONTRACT:
for entry: Dictionary in main.ui.LAYER_CONTRACT:
if entry.has("host"):
continue
var node: Node = main._layer_node(String(entry["shell"]) if entry.has("shell") else String(entry["key"]))
var node: Node = main.ui.node(String(entry["shell"]) if entry.has("shell") else String(entry["key"]))
if node == null:
chain_ok = false
chain_detail += " 缺失:%s" % String(entry["key"])
@@ -105,7 +105,7 @@ func _smoke_scene_split() -> void:
chain_prev = node
check(chain_ok, "V1c 合同树序链逐对保序(运行期树序==LAYER_CONTRACT 序;违例:%s)〔装配生命批 V1〕" % chain_detail)
# ③ 探针挂载假设句柄在座(main.X 一级路径——探针迁移面收口证明)
check(main._start_overlay == main._menu_screen.start_overlay and main.camp_overlay == main._camp_screen.camp_overlay and main.level_screen == main._adventure_screen.level_screen and main._battle_root == main._adventure_screen.battle_root and main.board_view == main._adventure_screen.board_view,
check(main.ui.screens.start_overlay == main.ui.screens.menu_screen.start_overlay and main.ui.modals.camp_overlay == main.ui.screens.camp_screen.camp_overlay and main.ui.modals.level_screen == main.ui.screens.adventure_screen.level_screen and main.ui.screens.battle_root == main.ui.screens.adventure_screen.battle_root and main.ui.battle.board_zone.board_view == main.ui.screens.adventure_screen.board_view,
"V2 组合根句柄指向屏壳成员(探针挂载假设 main.X 零迁移)")
check(main.run_scope != null and not main.run_scope.is_adventure,
"V2 闲时会话自 _ready 即在(永非 null;迭代 27 T3——gp 影子账退役无账本成员)")
@@ -116,10 +116,10 @@ func _smoke_scene_split() -> void:
# 顶层可见内容层(V1a 用——候选=合同模态/全屏态+两屏壳的 boot 内容成员;屏壳本身是容器不当候选:
# camp 壳 boot 态 vis=true 但成员恒隐,裸树遍历自顶向下会先命中透明壳)
func _topmost_visible_layer(main: Node) -> Node:
var candidates: Array[Node] = [main._start_overlay, main._battle_root, main._inter_overlay,
main.legend_overlay, main.node_overlay, main.level_screen, main.curio_overlay,
main._end_overlay, main.shop_overlay, main.camp_overlay, main.settings_overlay,
main.status_panel, main.career_overlay, main.confirm_dialog, main.debug_overlay]
var candidates: Array[Node] = [main.ui.screens.start_overlay, main.ui.screens.battle_root, main.ui.screens.inter_overlay,
main.ui.modals.legend_overlay, main.ui.modals.node_overlay, main.ui.modals.level_screen, main.ui.modals.curio_overlay,
main.ui.modals.end_overlay, main.ui.modals.shop_overlay, main.ui.modals.camp_overlay, main.ui.modals.settings_overlay,
main.ui.modals.status_panel, main.ui.modals.career_overlay, main.ui.modals.confirm_dialog, main.ui.modals.debug_overlay]
var best: Node = null
var best_x := -1
var best_y := -1
@@ -167,7 +167,7 @@ func _smoke_boot_chain() -> void:
"V2b 设置已应用·减动效(UiKit.motion_scale=%s=settings_store.reduced_motion=%s)〔装配生命批 V2〕" % [
str(ui_kit.motion_scale), str(ss.reduced_motion)])
# ③ 启动态菜单可见
check(main._start_overlay.visible,
check(main.ui.screens.start_overlay.visible,
"V2c 启动态 _start_overlay.visible=true(boot 后玩家见菜单)〔装配生命批 V2〕")
# ④ 续玩钮可见态==SaveManager 断点在档态
var sm = main.get_node("/root/SaveManager")
@@ -186,22 +186,22 @@ func _smoke_boot_chain() -> void:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
sm.slots.attach_path("user://smoke_boot_chain.json")
check(main._menu_screen.entersavebtn != null and main._menu_screen.settingsbtn != null
and main._menu_screen.quitbtn != null,
check(main.ui.screens.menu_screen.entersavebtn != null and main.ui.screens.menu_screen.settingsbtn != null
and main.ui.screens.menu_screen.quitbtn != null,
"V2d 三钮在位·进入存档/设置/退出游戏(续玩钮族退役后主菜单操作组承接)〔卡② mn1/mn8〕")
for old_btn in ["lvbtn", "resumebtn", "campbtn", "careerbtn"]:
check(not (old_btn in main._menu_screen),
check(not (old_btn in main.ui.screens.menu_screen),
"V2d 四旧钮句柄零命中·%s(彻底撤出——禁隐藏态禁置灰态残留)〔卡② mn2〕" % old_btn)
check(_count_visible_buttons(main._start_overlay) == 3,
check(_count_visible_buttons(main.ui.screens.start_overlay) == 3,
"V2d 渲染面可见按钮计数==3(旧钮接回即红——渲染面锚,句柄缝盲区闭合)〔卡② mn1/mn2〕")
# ⑤ 闲时会话(_ready 即建、永非 null、在树)
check(main.run_scope != null and not main.run_scope.is_adventure and main.run_scope.is_inside_tree(),
"V2e run_scope 非空且为闲时会话(is_adventure=false+is_inside_tree)〔装配生命批 V2〕")
# ⑥ 调试浮窗 boot 不开+菜单角标在(菜单优先——boot 不自动开浮窗,
# 入口=开始屏右下角「调试」角标+对局中两屏角标)
check(not main.debug_overlay.visible,
check(not main.ui.modals.debug_overlay.visible,
"V2f 调试浮窗 boot 不自动开(菜单优先)〔2026-10-16 用户令〕")
check(main.debug_badge != null and not main.debug_badge.visible,
check(main.ui.screens.debug_badge != null and not main.ui.screens.debug_badge.visible,
"V2f 调试浮层角标发布态恒隐(2026-10-25 删写死行批)")
main.queue_free()
await process_frame
@@ -223,7 +223,7 @@ func _smoke_run_lifecycle() -> void:
sm.slots.attach_path("user://smoke_run_lifecycle.json")
sm.meta_state.mark_first_adventure_done()
# ---- 第一局:开局→注脏→放弃→终局出口→会话收束 ----
main.debug_overlay.seed_edit.text = "42"
main.ui.modals.debug_overlay.seed_edit.text = "42"
main._start_adventure_from_menu()
await process_frame
check(main.run_scope != null and main.run_scope.is_adventure and main.run_scope.adventure != null,
@@ -243,22 +243,22 @@ func _smoke_run_lifecycle() -> void:
break
await process_frame
check(finished.done, "V3 冒险终局到达(abandon→adventure_finished 同步链)")
check(main._end_overlay.is_open(), "V3 终局面板弹起")
check(main.ui.modals.end_overlay.is_open(), "V3 终局面板弹起")
main._on_battle_end_continue() # 终局出口(回菜单+会话收束——手工置空清单已消亡)
await process_frame
check(not main.run_scope.is_adventure and main.run_scope.adventure == null and main.run_scope.runner == null,
"V3 会话收束(新闲时会话零冒险/关卡对象)")
check(main.run_scope.hints_shown.is_empty() and not main.run_scope.boss_active and main.run_scope.last_gain_amount == 0,
"V3 局态封印/账本全复位(queue_free 物化——注入脏状态零残留)")
check(main._start_overlay.visible, "V3 回开始菜单(终局出口路径)")
check(main.ui.screens.start_overlay.visible, "V3 回开始菜单(终局出口路径)")
# ---- 第二局:放弃终局无断点残留→直进开局→全新会话 ----
check(not sm.has_breakpoint(), "V3 放弃终局清断点(无残留断点)")
main.debug_overlay.seed_edit.text = "43"
main.ui.modals.debug_overlay.seed_edit.text = "43"
main._start_adventure_from_menu()
await process_frame
check(main.run_scope.is_adventure and main.run_scope.adventure != null and main.run_scope.hints_shown.is_empty(),
"V3 第二连局全新会话(两连局零泄漏)")
check(not main._start_overlay.visible, "V3 直进入局(菜单隐)")
check(not main.ui.screens.start_overlay.visible, "V3 直进入局(菜单隐)")
main.queue_free()
await process_frame
for p in ["user://smoke_run_lifecycle.json", "user://smoke_run_lifecycle.json.tmp"]:
@@ -281,14 +281,14 @@ func _smoke_sim_guards() -> void:
# wd6:闲时态 + 模拟运行态 → 调试两层入口守卫早退(开始层不收起、无结算对象)
check(main.run_scope.settlement == null and not main.run_scope.is_adventure,
"前置:闲时会话(settlement 空 + is_adventure=false)")
main._start_overlay.visible = true
main.ui.screens.start_overlay.visible = true
main._tune_sim._running = true
main._start_debug_levels()
check(main.run_scope.settlement == null and main._start_overlay.visible,
check(main.run_scope.settlement == null and main.ui.screens.start_overlay.visible,
"模拟运行态下调试两层入口守卫早退(开始层不收起)〔wd6〕")
main._tune_sim._running = false
# wd7:真实冒险关间态(is_adventure=true、settlement==null)→ 模拟入口守卫早退
main.debug_overlay.seed_edit.text = "42"
main.ui.modals.debug_overlay.seed_edit.text = "42"
main._start_adventure_from_menu()
await process_frame
check(main.run_scope.is_adventure and main.run_scope.settlement == null,
@@ -313,8 +313,8 @@ func _face_snapshot(main: Node) -> Dictionary:
var tu = main._treasure_ui
var bo = main._bonus
var cp = main._camp_pres
var mf = main._modal_flow
var rp = main._row_pacer
var mf = main.ui.modals.flow
var rp = main.ui.screens.row_pacer
return {
"treasure_settlement": tu._settlement_get.call(),
"treasure_runner": tu._runner_get.call(),
@@ -381,13 +381,13 @@ func _stage_faces(main: Node, tag: String, adv: String, run: String, set: String
if bool(snap["mf_rows_empty"]) != bool(extra[k]):
bad += " modal_flow.rows_empty_get 应=%s" % str(extra[k])
"level_vis":
if main.level_screen.visible != bool(extra[k]):
if main.ui.modals.level_screen.visible != bool(extra[k]):
bad += " level_screen.visible 应=%s" % str(extra[k])
"end_open":
if main._end_overlay.is_open() != bool(extra[k]):
if main.ui.modals.end_overlay.is_open() != bool(extra[k]):
bad += " _end_overlay.is_open 应=%s" % str(extra[k])
"start_vis":
if main._start_overlay.visible != bool(extra[k]):
if main.ui.screens.start_overlay.visible != bool(extra[k]):
bad += " _start_overlay.visible 应=%s" % str(extra[k])
check(bad.is_empty(), "%s(引用面违例:%s)〔装配生命批 V3〕" % [tag, bad])
@@ -419,7 +419,7 @@ func _smoke_lifecycle_matrix() -> void:
if FileAccess.file_exists(p):
DirAccess.remove_absolute(p)
sm.slots.attach_path("user://smoke_lifecycle.json")
main.debug_overlay.seed_edit.text = "42"
main.ui.modals.debug_overlay.seed_edit.text = "42"
# 阶段①闲时
await _stage_faces(main, "V3a 闲时", "null", "null", "null",
{"mf_start": true, "mf_inter": false, "mf_rows_empty": true, "start_vis": true})
@@ -430,7 +430,7 @@ func _smoke_lifecycle_matrix() -> void:
await _stage_faces(main, "V3b 开局停点", "alive", "alive", "null",
{"mf_start": false, "level_vis": true})
# 阶段③地图选路→战斗中(真实选路提交 press_candidate)
main.level_screen.press_candidate(0)
main.ui.modals.level_screen.press_candidate(0)
for i in range(10):
await process_frame
await _stage_faces(main, "V3c 战斗中", "alive", "alive", "alive", {"mf_start": false})
@@ -465,9 +465,9 @@ func _smoke_lifecycle_matrix() -> void:
check(main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live(),
"V3d 活战斗驱动完毕(战斗已收场——%d 轮,finished=%s)〔装配生命批 V3〕" % [guard, str(finished.done)])
await process_frame
main._flush_row_queue()
main.ui.screens.row_pacer.flush()
for i in range(12):
if main._end_overlay.is_open():
if main.ui.modals.end_overlay.is_open():
break
await process_frame
# 分支甲:战斗胜利停点(结算后)→确认进选路→放弃→终局面
@@ -482,13 +482,13 @@ func _smoke_lifecycle_matrix() -> void:
if finished.done:
break
await process_frame
main._flush_row_queue()
main.ui.screens.row_pacer.flush()
for i in range(8):
if main._end_overlay.is_open():
if main.ui.modals.end_overlay.is_open():
break
await process_frame
# 阶段⑤终局面(败北/放弃皆达)
check(finished.done and main._end_overlay.is_open(),
check(finished.done and main.ui.modals.end_overlay.is_open(),
"V3e 终局面到达(adventure_finished+终局面板开)〔装配生命批 V3〕")
await _stage_faces(main, "V3e 终局面", "alive", "alive", "null", {"mf_start": false, "end_open": true})
# 阶段⑥回菜单收束(旧局消亡+新会话即驻+无真空窗)
@@ -515,7 +515,7 @@ func _smoke_lifecycle_matrix() -> void:
main._open_save_select()
for i in range(2):
await process_frame
var ssel_new: Button = main.save_select_overlay._new_btns.get(1)
var ssel_new: Button = main.ui.modals.save_select_overlay._new_btns.get(1)
if ssel_new == null:
check(false, "V3g 营地入口缺失(选档层新档钮驱动中断)〔装配生命批 V3〕")
continue
@@ -551,7 +551,7 @@ func _smoke_lifecycle_matrix() -> void:
DirAccess.remove_absolute(slot_p)
# 阶段⑧两连局(第二局全新会话——旧局对象不复用、脏态零残留)
var first_adv = main.run_scope.adventure
main.debug_overlay.seed_edit.text = "43"
main.ui.modals.debug_overlay.seed_edit.text = "43"
main._start_adventure_from_menu()
for i in range(6):
await process_frame
@@ -560,12 +560,12 @@ func _smoke_lifecycle_matrix() -> void:
await _stage_faces(main, "V3h 两连局·开局停点", "alive", "alive", "null", {"mf_start": false, "level_vis": true})
check(main.run_scope.adventure.abandon(), "V3h 放弃受理(第二局)〔装配生命批 V3〕")
for i in range(12):
if main._end_overlay.is_open():
if main.ui.modals.end_overlay.is_open():
break
await process_frame
main._flush_row_queue()
main.ui.screens.row_pacer.flush()
for i in range(8):
if main._end_overlay.is_open():
if main.ui.modals.end_overlay.is_open():
break
await process_frame
await _stage_faces(main, "V3h 两连局·终局面", "alive", "alive", "null", {"mf_start": false, "end_open": true})
@@ -34,9 +34,31 @@ func _init() -> void:
_smoke_pool_wiring()
_smoke_damage_types()
_smoke_attack_chain_fresh_pool()
_smoke_hero_atk_derived()
quit(Kit.finish("roll_multiplier_test"))
# ---- 主角攻击基值派生读(注册面批同代编排归约 V4——_hero_atk 快照成员退役改派生读:
# enc 携 hero_atk 键受理后经派生读供 spawn 与攻击单 base_value 两消费点;缺省回退表值——
# 受理后不变量(encounter 受理后只读)。攻击单 base_value 贯通由 _smoke_damage_types 的
# hero_atk_val=100 全链路行值承载(同夹具既有覆盖——派生读在攻击链上工作即行值随 override 走)----
func _smoke_hero_atk_derived() -> void:
var rng_a = load("res://core/rng_service.gd").new()
rng_a.set_master_seed(4242)
var s_a = load("res://core/battle_orchestrator.gd").new(_db, rng_a)
Kit.check_quiet(s_a.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 100}),
"起战受理(override 100)〔编排归约 V4〕")
Kit.check_quiet(int(s_a._enemy_system.hero_unit().get("atk", -1)) == 100,
"主角攻击单基值==enc 携带 override(_hero_atk 派生读贯通 spawn 消费点)〔编排归约 V4〕")
var rng_b = load("res://core/rng_service.gd").new()
rng_b.set_master_seed(4242)
var s_b = load("res://core/battle_orchestrator.gd").new(_db, rng_b)
Kit.check_quiet(s_b.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}),
"起战受理(缺省)〔编排归约 V4〕")
Kit.check_quiet(int(s_b._enemy_system.hero_unit().get("atk", -1)) == int(_db.combat.hero.atk),
"缺省主角攻击单基值==hero 表值(派生读回退面)〔编排归约 V4〕")
# ---- 敌方攻击类型轴(表列/单位快照/单据载荷/矩阵命中证词)----
# 克制矩阵命中证词 = 同种子三甲型配对实测(不是查表存在性证词):同种子下 armor_roll 流消费逐位一致、
# 阈值(hero armor_class)不变,三甲型唯一差异 = 克制点数 cdelta(normal_vs_light 0 / heavy −1 / ward +1)
@@ -83,9 +83,10 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(42)
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
var out := {"stream": [], "events": [], "rounds": 0, "result": "", "terminated": false}
var out := {"stream": [], "events": [], "rounds": 0, "result": "", "terminated": false, "econ_refresh_used": -1}
settlement.step_row.connect(func(row): out.stream.append(row))
settlement.battle_finished.connect(func(payload): out.result = payload.get("result", ""))
settlement.settlement_snapshot.connect(func(snap): out.econ_refresh_used = int(snap.get("economy", {}).get("refresh_used", -1))) # 末次发射=终局轮快照
settlement.minesweeper.cell_revealed.connect(func(evt): out.events.append(evt))
settlement.minesweeper.seal_judged.connect(func(evt): out.events.append(evt))
settlement.minesweeper.round_end_board.connect(func(p): out.events.append({"ev": "round_end_board", "path_tag": p.path_tag}))
@@ -108,6 +109,10 @@ func _run_battle(L: int, D: int, axes: Dictionary = {}) -> Dictionary:
# P0-3 回归断言:notify_combat_end 曾被并进行尾注释从不执行——
# 战斗收束后相位必须回 Idle(RAP 清零),任何「相位卡 Waiting」回归在此被抓
Kit.check("战斗收束后行动经济相位回 Idle(notify_combat_end 生效,实测=%s)" % String(action_economy.phase), action_economy.phase == "Idle")
# 复位边界(注册面批同代编排归约 V2——BattleFacts.reset_round 不碰 refresh_used 的可观测提交面):
# 终局序列含 facts.reset_round(),其后呈现快照的 refresh_used 必须等于末轮结算快照的 economy 值
Kit.check("L%d·D%d 终局后呈现快照 refresh_used==末轮结算快照 economy 值(复位不碰 refresh_used)〔编排归约 V2〕" % [L, D],
int(settlement.presentation_snapshot().get("refresh_used", -1)) == out.econ_refresh_used)
return out
@@ -1019,7 +1024,7 @@ func _smoke_counter_buckets() -> void:
if not oA.minesweeper.current_board().is_mine(ca):
safe_a = ca
break
oA._grant_seg._judge_seal({"cell": safe_a, "hit": false}, 1)
oA._rounds._grant_seg._judge_seal({"cell": safe_a, "hit": false}, 1)
Kit.check("V6 第1战标错落 battle 桶 insurance_used=1(_apply_counter_hit 静态写口经 ctx)〔无状态化 V6〕", EffectEngine.counter(oA.fx_host().counters_ctx, "insurance_used") == 1)
# 卡④ wr2①——完美保险标错行=单一扩展行型(原 insurance_used 实名行型收敛)
var v6_ins := {}
@@ -374,10 +374,10 @@ func _smoke_clear_reward_zero_paths() -> void:
Kit.check("高光守卫构造战斗启动", s_b.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}))
for u in s_b._enemy_system.all_enemies():
s_b._enemy_system.apply_damage(u, int(u.hp))
s_b._perfect_hit_true = 1 # 测试通道:构造完美谓词成立(真值在正常流由封印判定累加)
s_b._perfect_hit_false = 0
s_b._facts.perfect_hit_true = 1 # 测试通道:构造完美谓词成立(真值在正常流由封印判定累加)
s_b._facts.perfect_hit_false = 0
rows_b.clear()
s_b.reward_grant().grant_clear_reward(1, s_b._board_from_midround_refresh, s_b.perfect_sweep_earned())
s_b.reward_grant().grant_clear_reward(1, s_b._facts.board_from_midround, s_b.perfect_sweep_earned())
Kit.check("② grant_clear_reward 守卫路径重走首份 → 未生效行仍在案", _has_source_row(rows_b, "reward_ineffective", "clear_reward", "clear_blast"))
Kit.check("② 首份零兑付 → 高光行不发(不记名义值 2)", _rows_of_type(rows_b, "highlight").is_empty())
# ③ 第二份被拒:唯一存活敌 HP ≤ 首份 aoe 量 → 首份击杀 → 第二份无存活敌(zero + 未生效行)
@@ -389,10 +389,10 @@ func _smoke_clear_reward_zero_paths() -> void:
Kit.check("第二份被拒构造战斗启动", s2.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}))
var boss = s2._enemy_system.all_enemies()[0]
boss.hp = 1 # 测试通道:压低至首份 aoe 必杀(弱形式——正常流 Boss 血量不可控)
s2._perfect_hit_true = 1
s2._perfect_hit_false = 0
s2._facts.perfect_hit_true = 1
s2._facts.perfect_hit_false = 0
rows2.clear()
s2.reward_grant().grant_clear_reward(1, s2._board_from_midround_refresh, s2.perfect_sweep_earned())
s2.reward_grant().grant_clear_reward(1, s2._facts.board_from_midround, s2.perfect_sweep_earned())
Kit.check("③ 首份 aoe 真实兑付(击杀唯一存活敌)", _has_source_row(rows2, "damage", "clear_reward"))
Kit.check("③ 第二份被拒产出 reward_ineffective 行(零兑付)", _has_source_row(rows2, "reward_ineffective", "clear_reward", "clear_blast"))
var hl2 := _rows_of_type(rows2, "highlight")
@@ -116,19 +116,19 @@ func _init() -> void:
"feats": [], "tree_rules": [], "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0,
"node_type": "battle", "adv_bucket": {}}), "V6 前置:战斗受理")
# facts 组包经真实摇号路径消费(mines 全零时条件技 sneak_ambush 不入集合——facts 四键在位的可观察面)
Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["last"]) == 0 and int(orch._mines_hits["round"]) == 0,
Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 0 and int(orch._rounds.mines_hits["last"]) == 0 and int(orch._rounds.mines_hits["round"]) == 0,
"V6 开战清零:三计数全零起〔触发器批 V6〕")
# 正向 blast(spark_aoe)不递增——负向计数守卫(flat 行应用面自编排器迁至奖励授予件
# ——迭代40 卡① 拆件;计数经授予件 battle_hooks.negative_blast 回宿主 _mines_hits)
var att_v6 := {"source_class": "reward", "cell": Vector2i(-1, -1), "ri": 1}
orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "positive", "power": 2,
"display_name": "爆裂", "reward_id": "spark_aoe"}, att_v6)
Kit.check_quiet(int(orch._mines_hits["total"]) == 0 and int(orch._mines_hits["round"]) == 0,
Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 0 and int(orch._rounds.mines_hits["round"]) == 0,
"V6 正向 blast(spark_aoe)不递增(负向守卫——清图/爆裂格零误计)〔触发器批 V6〕")
# 负向 blast(雷)递增
orch.reward_grant()._apply_flat_row({"cell_type": "blast", "polarity": "negative", "power": 2,
"display_name": "警觉", "reward_id": "enemy_alert"}, att_v6)
Kit.check_quiet(int(orch._mines_hits["total"]) == 1 and int(orch._mines_hits["round"]) == 1,
Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 1 and int(orch._rounds.mines_hits["round"]) == 1,
"V6 负向 blast(雷)递增恰一次〔触发器批 V6〕")
# ---- V7 池构成纪律(现行全表零违例+构造全条件池报错——跨表校验真实入口)----
+30 -9
View File
@@ -66,12 +66,33 @@ func _smoke_hygiene_gates() -> void:
var mv: String = FileAccess.get_file_as_string("res://presentation/main.gd")
Kit.check("V8 GP 影子账残留零命中(main 源码面——文件已删+委托缝已拆)〔迭代27 T3〕",
not mv.contains("gp_shadow_ledger") and not mv.contains("_gp_ledger"))
# V1d 层序落位单口:main.gd 的 move_child 调用全部收在 _apply_layer_contract 内(合同装配单口——
# 函数体之外零调用点;出现第二处落位=硬编码层序,须改合同表而非加调用)
var fn_start: int = mv.find("func _apply_layer_contract")
var fn_end: int = mv.find("\nfunc ", fn_start + 10)
if fn_end < 0:
fn_end = mv.length()
var outside_calls: int = (mv.substr(0, fn_start) + mv.substr(fn_end)).count("move_child(")
Kit.check("V1d 层序落位单口(_apply_layer_contract 之外 move_child 调用点=%d 应=0)〔装配生命批 V1〕" % outside_calls,
outside_calls == 0)
# V1d 层序落位单口:注册面建件后扫全 presentation——move_child 只允许出现在 ui_registry.gd 的
# apply_layer_contract 函数体内(合同装配单口——出现第二处落位=硬编码层序,须改合同表而非加调用)
var scanned: int = 0
var outside_calls: int = 0
for fn_path: String in _list_scripts("res://presentation"):
var src: String = FileAccess.get_file_as_string(fn_path)
scanned += 1
var fn_start: int = src.find("func apply_layer_contract") if fn_path.contains("ui_registry.gd") else -1
var fn_end: int = src.find("\nfunc ", fn_start + 10) if fn_start >= 0 else -1
if fn_end < 0:
fn_end = src.length()
outside_calls += (src.substr(0, fn_start) + src.substr(fn_end)).count("move_child(")
Kit.check("V1d 层序落位单口(presentation %d 文件——apply_layer_contract 之外 move_child 调用点=%d 应=0)〔注册面 V1〕" % [scanned, outside_calls],
scanned > 0 and outside_calls == 0)
# 目录 .gd 清单(DirAccess 平铺——presentation 无子目录;扫描面>0 前置由调用侧断言承载)
static func _list_scripts(dir_path: String) -> PackedStringArray:
var out: PackedStringArray = PackedStringArray()
var dir := DirAccess.open(dir_path)
if dir == null:
return out
dir.list_dir_begin()
var fname := dir.get_next()
while fname != "":
if fname.ends_with(".gd"):
out.append(dir_path + "/" + fname)
fname = dir.get_next()
dir.list_dir_end()
return out
+2 -2
View File
@@ -361,7 +361,7 @@ func _smoke_adventure() -> void:
if int(pb.get("level_index", -1)) == 2:
l2_price_bonus = int(pb.get("bonus", 0))
Kit.check("进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" % [hp_price + l2_price_bonus, hp_price, l2_price_bonus], gp0 >= hp_price + l2_price_bonus)
Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.buy_product("hp_up"))
Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.shop.buy_product("hp_up"))
var acc_shop: int = a8.journal().bonus_total("max_hp") # L1 三选一后 + 商店 3(首件随种子)
Kit.check("统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" % acc_shop, acc_shop >= 3)
var l2_cap := {"payload": {}} # 公开信号面采集(curio_choice_presented 停点载荷——容器写回规避 lambda 按值捕获)
@@ -850,7 +850,7 @@ func _drive_to_pref(autoplay, prefer: String) -> Dictionary:
r.pick_next_node(int(hit.get("id", -1)))
out["node"] = hit
if prefer == "battle":
out["enc"] = r.current_battle().encounter
out["enc"] = r.current_battle()._facts.encounter
else:
var cands: Array = (adr.stop().node_curio_candidates() as Dictionary).get("candidates", []) as Array
adr.stop().present_node_curio(cands)
@@ -49,9 +49,9 @@ func _init() -> void:
# ---- ⑤ 钱包单口支出 spend_gp(LevelRunner——三选一付费档与商店刷新共用同一冒险钱包)----
var lr = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
lr.gp = 30
Kit.check_quiet(lr.spend_gp(0) == false and int(lr.gp) == 30, "零额支出拒绝零副作用")
Kit.check_quiet(lr.spend_gp(31) == false and int(lr.gp) == 30, "余额不足拒绝零副作用")
Kit.check_quiet(lr.spend_gp(20) == true and int(lr.gp) == 10, "足额支出扣账(30→10)")
Kit.check_quiet(lr.shop.spend_gp(0) == false and int(lr.gp) == 30, "零额支出拒绝零副作用")
Kit.check_quiet(lr.shop.spend_gp(31) == false and int(lr.gp) == 30, "余额不足拒绝零副作用")
Kit.check_quiet(lr.shop.spend_gp(20) == true and int(lr.gp) == 10, "足额支出扣账(30→10)")
# ---- ⑥ Y 断点随存(缺陷锚:续玩自参数面整额重注入=用完 Y 退菜单再续玩满血复活)----
var bp_y := {"level_index": 1, "hero_hp": 10, "attune_refresh_y": 2, "graph_gen_v2": 1}
+11 -11
View File
@@ -174,10 +174,10 @@ func _init() -> void:
Kit.check_quiet(safe_cells.size() == 2, "保险 rig:两枚安全格定位(去重闸按格实例——限次样本须异格)〔卡② R2〕")
var rows_orch: Array[Dictionary] = []
orch.step_row.connect(func(row): rows_orch.append(row)) # step_row 公开信号面(insurance_used 载荷/seal_miss highlight=豁免账与误标账观测面)
Kit.check_quiet(int(orch._perfect_hit_false) == 0 and int(orch._perfect_forgiven) == 0,
Kit.check_quiet(int(orch._facts.perfect_hit_false) == 0 and int(orch._facts.perfect_forgiven) == 0,
"保险前置:误标账与豁免账双零起〔卡② R2〕")
orch._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1)
Kit.check_quiet(int(orch._perfect_hit_false) == 1,
orch._rounds._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1)
Kit.check_quiet(int(orch._facts.perfect_hit_false) == 1,
"计数纯净:首次标错 _perfect_hit_false 落 1 未被回退改写(契约 2)〔卡② R2〕")
var ins_row := {}
for r in rows_orch:
@@ -187,7 +187,7 @@ func _init() -> void:
"豁免一次:首次标错 _perfect_forgiven=1(forgive_seal_miss 派发)〔卡② R2〕")
Kit.check_quiet(int(orch.action_economy.pending_ap_delta()) == 1,
"事件不吞:同一标错的 seal_missed 仍被危险直觉·守收到(queue AP+1)〔卡② R2〕")
orch._grant_seg._judge_seal({"cell": safe_cells[1], "hit": false}, 2)
orch._rounds._grant_seg._judge_seal({"cell": safe_cells[1], "hit": false}, 2)
var ins_count := 0
var miss_hl_last := ""
for r in rows_orch:
@@ -199,11 +199,11 @@ func _init() -> void:
"限次:第二次标错不获豁免(forgiven 恒 1——when counter lt 1)〔卡② R2〕")
Kit.check_quiet(not orch.perfect_sweep_earned(),
"判定相减:误标 2 − 豁免 1 = 有效 1 → 完美不成立〔卡② R2 真值表〕")
orch._perfect_hit_false = 1
orch._perfect_hit_true = 1
orch._facts.perfect_hit_false = 1
orch._facts.perfect_hit_true = 1
Kit.check_quiet(orch.perfect_sweep_earned(),
"判定相减:误标1−豁免1=有效0且标中≥1→完美成立(保险每战限次一次迁移保绿)〔无状态化 V7〕")
orch._perfect_hit_true = 0
orch._facts.perfect_hit_true = 0
Kit.check_quiet(not orch.perfect_sweep_earned(),
"判定守卫:标中数 0 时零误标也不成立(整图 ≥1 标中条件零松动)〔卡② R2 真值表〕")
var rig_rng2 = RngScript.new()
@@ -218,7 +218,7 @@ func _init() -> void:
var rows_orch2: Array[Dictionary] = []
orch2.step_row.connect(func(row): rows_orch2.append(row)) # step_row 公开信号面(insurance_used 载荷=豁免账观测面)
var sc2 = safe_cells[0]
orch2._grant_seg._judge_seal({"cell": sc2, "hit": false}, 1)
orch2._rounds._grant_seg._judge_seal({"cell": sc2, "hit": false}, 1)
var ins_row2 := {}
for r in rows_orch2:
if String(r.get("effect_id", "")) == "insurance_used":
@@ -245,7 +245,7 @@ func _init() -> void:
if not o10.minesweeper.current_board().is_mine(c10):
sc10 = c10
break
o10._grant_seg._judge_seal({"cell": sc10, "hit": false}, 1)
o10._rounds._grant_seg._judge_seal({"cell": sc10, "hit": false}, 1)
var sm10: Dictionary = {}
for r10 in rows10:
if String(r10.get("type", "")) == "seal_miss":
@@ -272,14 +272,14 @@ func _init() -> void:
if not o11.minesweeper.current_board().is_mine(c11):
sc11 = c11
break
o11._grant_seg._judge_seal({"cell": sc11, "hit": false}, 1)
o11._rounds._grant_seg._judge_seal({"cell": sc11, "hit": false}, 1)
var sm11: Dictionary = {}
for r11 in rows11:
if String(r11.get("type", "")) == "seal_miss":
sm11 = r11
Kit.check_quiet(String(sm11.get("highlight", "")) == "",
"A11 豁免:首次标错获豁免 → seal_miss 行 highlight==空串〔卡① N7〕")
o11._perfect_hit_true = 1
o11._facts.perfect_hit_true = 1
Kit.check_quiet(o11.perfect_sweep_earned(),
"A11 真值:误标 1 − 豁免 1 = 有效 0 且标中≥1 → 完美成立(与 highlight==空 一致)〔卡① N7〕")
+2 -2
View File
@@ -57,13 +57,13 @@ func _init() -> void:
# ---- hr1-s:商店属性商品加算走日记单写原语(购买后 bonus_total 与旧径同值)----
runner._gain_wallet(1000) # 钱包注入(测试直调写口——测试构造惯例)
var products: Array = runner.shop_products()
var products: Array = runner.shop.shop_products()
var bought := false
for p in products:
if int(p.get("price", 1 << 30)) <= 1000 and String(p.get("attr", "")) != "":
var attr := String(p.get("attr", ""))
var before: int = runner.journal().bonus_total(attr)
bought = runner.buy_product(String(p.get("product_id", "")))
bought = runner.shop.buy_product(String(p.get("product_id", "")))
if bought:
Kit.check_quiet(runner.journal().bonus_total(attr) == before + int(p.get("flat_value", 0)),
"hr1-s 商店购买后 bonus_total 增量 = 商品 flat(加算走 gain_bonus 单写原语——同值承载)")
+96 -27
View File
@@ -251,6 +251,7 @@ func _init() -> void:
_smoke_shop_graph()
_smoke_multi_terminal_settlement()
_smoke_shop_pool()
_test_shop_node_entered_payload()
_smoke_generated_coverage()
quit(Kit.finish("level_shop_economy_test"))
@@ -1024,17 +1025,17 @@ func _smoke_shop_room() -> void:
return
Kit.check("到店开店态(覆盖层数据源就位——离开前不出选路口,零自动推进)", not runner.awaiting_choice() and runner.next_candidate_ids().is_empty())
# 现价 = 基价 + price_level_bonus(L2)=2 + 步长 × 已购档数
var prods: Array = runner.shop_products()
var prods: Array = runner.shop.shop_products()
Kit.check("三商品全陈列(atk_up/hp_up/armor_up——无刷新无摇号)", prods.size() == 3)
var by_id := _shop_products_by_id(prods)
Kit.check("L2 首购现价 = 基价 + 关级加值 2(atk_up 6 / hp_up 4 / armor_up 6)", int(by_id["atk_up"]["price"]) == 6 and int(by_id["hp_up"]["price"]) == 4 and int(by_id["armor_up"]["price"]) == 6)
# 购买 atk_up:钱包 −6 · 加算区 atk +1 · 二购现价 8
var gp0: int = runner.gp
Kit.check("购买 atk_up 受理(现价 6)", runner.buy_product("atk_up"))
Kit.check("购买 atk_up 受理(现价 6)", runner.shop.buy_product("atk_up"))
Kit.check("钱包扣现价 6(%d → %d)" % [gp0, runner.gp], runner.gp == gp0 - 6)
# 加算区住加算日记
Kit.check("统一冒险加算区 atk +1(加算区住加算日记)", runner.journal().bonus_total("atk") == 1 and runner.journal().bonus_total("max_hp") == 0 and runner.journal().bonus_total("armor_class") == 0)
by_id = _shop_products_by_id(runner.shop_products())
by_id = _shop_products_by_id(runner.shop.shop_products())
Kit.check("atk_up 二购现价 = 4+2+2=8(步长 2 × 已购 1 档,现价现算)", int(by_id["atk_up"]["price"]) == 8 and int(by_id["atk_up"]["purchased"]) == 1)
# 行进段百分比归零(购买/生效断言按加算区裸值口径——调试浮窗百分比后乘;hp 快照钳到新口径上限防 spawn 越界,测试通道)
runner.set_hero_override(0, 0)
@@ -1042,28 +1043,28 @@ func _smoke_shop_room() -> void:
# hp_up:上限与当前生命同步 +3(封顶新上限)
var hp_before: int = runner.hero_hp
var mhp_before: int = runner.effective_max_hp()
Kit.check("购买 hp_up 受理(现价 4)", runner.buy_product("hp_up"))
Kit.check("购买 hp_up 受理(现价 4)", runner.shop.buy_product("hp_up"))
Kit.check("钱包扣 4(%d)" % runner.gp, runner.gp == gp0 - 6 - 4)
Kit.check("生命上限 +3(%d → %d——加算区 max_hp 轴)" % [mhp_before, runner.effective_max_hp()], runner.effective_max_hp() == mhp_before + 3)
Kit.check("当前生命同步 +3 封顶新上限(%d → %d)" % [hp_before, runner.hero_hp], runner.hero_hp == mini(hp_before + 3, runner.effective_max_hp()))
# armor_up:AC 10+1
Kit.check("购买 armor_up 受理(现价 6)", runner.buy_product("armor_up"))
Kit.check("购买 armor_up 受理(现价 6)", runner.shop.buy_product("armor_up"))
Kit.check("AC = 10+1(armor_class 加算区轴,无百分比通道)", runner.effective_ac() == int(_db.combat.hero.armor_class) + 1)
# 余额不足购买被拒(零副作用):hp_up 连购至不可负担后兜底拒绝
var guard_drain := 20
var hp_buys := 1 # 夹具前段已购 1 笔(购买 hp_up 受理断言)——总数 = 前段 + 本循环
while guard_drain > 0:
guard_drain -= 1
var hp_price: int = int(_shop_products_by_id(runner.shop_products())["hp_up"]["price"])
var hp_price: int = int(_shop_products_by_id(runner.shop.shop_products())["hp_up"]["price"])
if runner.gp < hp_price:
break
if not runner.buy_product("hp_up"):
if not runner.shop.buy_product("hp_up"):
break
hp_buys += 1
var gp_dry: int = runner.gp
var tier_dry: int = runner.purchased_tier("hp_up")
var hp_dry: int = int(_shop_products_by_id(runner.shop_products())["hp_up"]["price"])
Kit.check("余额不足购买被拒(gp=%d < 现价 %d——零扣费零档位变动)" % [gp_dry, hp_dry], not runner.buy_product("hp_up") and runner.gp == gp_dry and runner.purchased_tier("hp_up") == tier_dry)
var tier_dry: int = runner.shop.purchased_tier("hp_up")
var hp_dry: int = int(_shop_products_by_id(runner.shop.shop_products())["hp_up"]["price"])
Kit.check("余额不足购买被拒(gp=%d < 现价 %d——零扣费零档位变动)" % [gp_dry, hp_dry], not runner.shop.buy_product("hp_up") and runner.gp == gp_dry and runner.shop.purchased_tier("hp_up") == tier_dry)
# 离开 → 回选路口(候选 = 该店出边——恰一条,指向终点 Boss)
Kit.check("离开商店 → 回选路口(候选 = 店出边 %s——零自动推进收口)" % str(shop_outs),
runner.continue_after_node() and runner.awaiting_choice() and _sorted_ids(runner.next_candidate_ids()) == _sorted_ids(shop_outs))
@@ -1252,7 +1253,7 @@ func _smoke_shop_pool() -> void:
if not _adv_drive_to_shop(a8, _adv_st(), autoplay):
continue
scen = {"a": a8, "s": s, "r": a8.runner()}
first_shelf_a = (a8.runner().shop_shelf() as Array).duplicate(true)
first_shelf_a = (a8.runner().shop.shop_shelf() as Array).duplicate(true)
break
Kit.check("池模式商店房场景可达(seed=%s)" % str(scen.get("s", -1)), not scen.is_empty())
if not scen.is_empty():
@@ -1261,19 +1262,19 @@ func _smoke_shop_pool() -> void:
a8b.set_hero_override(900, 400)
if a8b.start_adventure(int(scen["s"]), {"shop_mode": "pool"}) \
and _adv_drive_to_shop(a8b, _adv_st(), autoplay):
first_shelf_b = (a8b.runner().shop_shelf() as Array).duplicate(true)
first_shelf_b = (a8b.runner().shop.shop_shelf() as Array).duplicate(true)
Kit.check("进店抽 3(货架 %d 卡——同构交互)" % first_shelf_a.size(), first_shelf_a.size() == 3)
# MC-1 商店独立刷新(P-C2+Q2 裁——基础免费 0;GP 刷新 ceil(20×1.8^n) provisional;重抽同源)
if not scen.is_empty():
var r8 = scen["r"]
var gp0: int = int(r8.gp)
var cost0: int = r8.shop_refresh_cost()
var cost0: int = r8.shop.shop_refresh_cost()
Kit.check("刷新首价=基价(%d)" % cost0, cost0 == int(_db.run.adventure.shop_refresh_cost_base))
Kit.check("基础免费 0 次(Q2——天赋增益位后续添加)", r8.shop_refresh_free_left() == 0)
var shelf_pre: Array = (r8.shop_shelf() as Array).duplicate(true)
Kit.check("GP 刷新受理(gp=%d cost=%d)" % [gp0, cost0], r8.shop_refresh())
Kit.check("基础免费 0 次(Q2——天赋增益位后续添加)", r8.shop.shop_refresh_free_left() == 0)
var shelf_pre: Array = (r8.shop.shop_shelf() as Array).duplicate(true)
Kit.check("GP 刷新受理(gp=%d cost=%d)" % [gp0, cost0], r8.shop.shop_refresh())
Kit.check("GP 刷新扣费(%d→%d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - cost0)
Kit.check("刷新爬价 ×curve(%d)" % int(r8.shop_refresh_cost()), int(r8.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.run.adventure.shop_refresh_cost_curve))))
Kit.check("刷新爬价 ×curve(%d)" % int(r8.shop.shop_refresh_cost()), int(r8.shop.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.run.adventure.shop_refresh_cost_curve))))
var same_shelf := first_shelf_a.size() == first_shelf_b.size()
if same_shelf:
for i in first_shelf_a.size():
@@ -1289,10 +1290,10 @@ func _smoke_shop_pool() -> void:
# ② 货架投影形制:kind 词表封闭 + 现价公式推导(ceil(base×b^(关序−1)×A^0)——核心现算,呈现零复算)
var l2 := int(r8.level_row.level_index)
# 现价等价证明(d11——消费端读表推导 vs 迁移前硬编码字面量推导:同输入逐位一致)
var relic_price_now: int = r8.pool_price("relic")
var relic_price_now: int = r8.shop.pool_price("relic")
var relic_price_pre: int = int(ceil(15.0 * pow(_db.run.shop.price_b, float(l2 - 1)) * 1.0))
Kit.check("relic 现价 = ceil(15.0×b^(关序−1))(读表与迁移前字面量逐位一致,%d)〔wd11〕" % relic_price_now, relic_price_now == relic_price_pre)
for c in r8.shop_shelf():
for c in r8.shop.shop_shelf():
# T6 商店保底后 relic 卡合法入架(保底占位)——词表断言放行 relic;价格断言分叉
# (数值卡走 shop_pool_row 推导 / relic 卡走表 knob relic_base_price——同公式不同基价)
var ckind := String(c.get("kind_id", ""))
@@ -1307,7 +1308,7 @@ func _smoke_shop_pool() -> void:
Kit.check("首购现价 = ceil(基价×b^(关序−1))(%s: %d = %d 推导)" % [ckind, int(c.get("price", -1)), expect_p], int(c.get("price", -1)) == expect_p)
# ③ 买 1 全重抽:出账+加算区+账本三落点 + 现价 A 链递增(买后 visit 计数=1;TP0——消费在架令牌本体)
var buy_c: Dictionary = {}
for c in r8.shop_shelf():
for c in r8.shop.shop_shelf():
if int(c.get("price", 1 << 30)) <= int(r8.gp):
buy_c = c
break
@@ -1319,7 +1320,7 @@ func _smoke_shop_pool() -> void:
var gp0: int = int(r8.gp)
var taken0: int = int(a8._curio.taken.get(kind, 0))
var val0: int = a8.journal().bonus_total(String(val_map[kind]))
Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.buy_pool_item(kind))
Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.shop.buy_pool_item(kind))
Kit.check("钱包扣现价(%d → %d)" % [gp0, int(r8.gp)], int(r8.gp) == gp0 - int(buy_c.get("price", 0)))
Kit.check("加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.run.adventure.curio_value(kind, tier))], a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.run.adventure.curio_value(kind, tier)))
Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8.deck().taken.get(kind, 0))], int(a8.deck().taken.get(kind, 0)) == taken0 + 1)
@@ -1327,7 +1328,7 @@ func _smoke_shop_pool() -> void:
# A 链:白盒抬 GP 保证第二购场景确定构造(直改测试基建——非生产行为)
r8.gp = int(r8.gp) + 64
var second: Dictionary = {}
for c in r8.shop_shelf():
for c in r8.shop.shop_shelf():
if int(c.get("price", 1 << 30)) <= int(r8.gp):
second = c
break
@@ -1335,14 +1336,14 @@ func _smoke_shop_pool() -> void:
var prow2: Dictionary = _db.run.shop.pool_row(String(second.get("kind_id", "")))
var expect_p2: int = int(ceil(float(int(prow2.get("base_price", 0))) * pow(_db.run.shop.price_b, float(l2 - 1)) * _db.run.shop.price_a))
Kit.check("A 链爬价 = ceil(base×b^(关序−1)×A^1)(进店第 2 购——%d)" % expect_p2, int(second.get("price", -1)) == expect_p2)
Kit.check("同 visit 第二购受理(A 链软闸在价不在限)", r8.buy_pool_item(String(second.get("kind_id", ""))))
Kit.check("同 visit 第二购受理(A 链软闸在价不在限)", r8.shop.buy_pool_item(String(second.get("kind_id", ""))))
else:
Kit.check("A 链爬价场景未构造(抬 GP 后货架仍无可购第二卡)——场景绑定失败", false)
# ④ 切边零入店(白-box 重抽 40 次——GP 袋/规则改写型特性永不在架;重抽消费 shop_draw 签,场景尾不回写主时间线)
var cut_seen := false
for i in 40:
r8._shop_draw_shelf()
for c in r8.shop_shelf():
r8.shop._draw_shelf()
for c in r8.shop.shop_shelf():
if String(c.get("kind_id", "")) == "gp" or String(c.get("kind_id", "")) == "treasure_item":
cut_seen = true
Kit.check("切边件零入店(40 次重抽零 gp/aux——负交易切边+三轴 doctrine)", not cut_seen)
@@ -1352,8 +1353,8 @@ func _smoke_shop_pool() -> void:
a8._curio.deck.remove_at(di8)
var attack_seen := false
for i in 40:
r8._shop_draw_shelf()
if (r8.shop_shelf() as Array).any(func(c): return String(c.get("kind_id", "")) == "attack"):
r8.shop._draw_shelf()
if (r8.shop.shop_shelf() as Array).any(func(c): return String(c.get("kind_id", "")) == "attack"):
attack_seen = true
break
Kit.check("库存尽剔牌库(attack 令牌耗尽——池消耗即上限跨渠道共库 reward→shop 向)", not attack_seen)
@@ -1391,6 +1392,74 @@ func _smoke_shop_pool() -> void:
Kit.check("终局池余量在域 [0,配额](%s)" % str(ps), ps_ok)
# ---- 商店房 node_entered 载荷(尾波 V1——_enter_node pool/legacy 双发射路径归一后的行为保持断言)----
func _test_shop_node_entered_payload() -> void:
print("--- 商店房 node_entered 载荷(pool 含 shelf:true+gp 现值/legacy 无 shelf 键)---")
var autoplay = load("res://sim/level_autoplay.gd")
# pool 态:载荷字典经信号闭包当场取样(字典为引用累积器——lambda 按值捕获标量的绕行惯例)
var cap_pool := {"payload": {}, "gp": -1}
var pool_ok := false
for s in range(42, 92):
var pr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(s))
pr.set_hero_override(900, 400)
if not pr.start_adventure(s, {"shop_mode": "pool"}):
continue
var rp = pr.runner()
rp.node_entered.connect(func(node, payload):
if String(node.get("type", "")) == "shop":
cap_pool["payload"] = payload
cap_pool["gp"] = int(rp.gp))
if not _adv_drive_to_shop(pr, _adv_st(), autoplay):
continue
pool_ok = not (cap_pool["payload"] as Dictionary).is_empty()
if pool_ok:
break
Kit.check("pool 态进店信号可达(seed 扫描 42–91)", pool_ok)
Kit.check("pool 态 node_entered 载荷含 shelf:true〔尾波 V1〕", pool_ok and bool((cap_pool["payload"] as Dictionary).get("shelf", false)))
Kit.check("pool 态载荷 gp = 进店钱包现值(%d)〔尾波 V1〕" % int(cap_pool["gp"]), pool_ok and int((cap_pool["payload"] as Dictionary).get("gp", -1)) == int(cap_pool["gp"]))
# legacy 态:载荷无 shelf 键(两 payload 字典逐字保持的另一半——直构 LevelRunner 链)
var cap_leg := {"payload": {}, "gp": -1}
var leg_ok := false
for s in range(42, 92):
var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(s))
lr.set_hero_override(900, 400)
lr.start_level(2, s)
lr.node_entered.connect(func(node, payload):
if String(node.get("type", "")) == "shop":
cap_leg["payload"] = payload
cap_leg["gp"] = int(lr.gp))
var st := _battle_st("normal")
var guard := 60
while String(lr.level_result) == "" and String(lr.current_node().get("type", "")) != "shop" and guard > 0:
guard -= 1
if lr.awaiting_choice():
var ids: Array = lr.next_candidate_ids()
ids.sort()
lr.pick_next_node(int(ids[0]))
continue
var t := String(lr.current_node().get("type", ""))
if t == "reward" or t == "rest":
lr.continue_after_node()
continue
var b = lr.current_battle()
if b == null:
break
var fin := {}
b.battle_finished.connect(func(p): fin["p"] = p)
var g2 := 80
while lr.current_battle() == b and not fin.has("p") and g2 > 0:
g2 -= 1
autoplay._autoplay_battle(lr, b, st, null)
if String(lr.level_result) == "" and String(lr.current_node().get("type", "")) == "shop":
leg_ok = not (cap_leg["payload"] as Dictionary).is_empty()
if leg_ok:
break
Kit.check("legacy 态进店信号可达(seed 扫描 42–91)", leg_ok)
Kit.check("legacy 态 node_entered 载荷无 shelf 键〔尾波 V1〕", leg_ok and not (cap_leg["payload"] as Dictionary).has("shelf"))
Kit.check("legacy 态载荷 gp = 进店钱包现值(%d)〔尾波 V1〕" % int(cap_leg["gp"]), leg_ok and int((cap_leg["payload"] as Dictionary).get("gp", -1)) == int(cap_leg["gp"]))
# 入口→Boss 全路径枚举(DFS 迭代栈——图合法性校验保证单入口无环,路径有限)
@@ -339,7 +339,7 @@ func _meta_settle_and_workshop() -> void:
adr_res.resume_adventure(bp_min, p_resume))
Kit.check("继续上局受理面:tree_rules 随参数面进 runner._tree_rules(消费面 set_tree_rules 下传现成)",
adr_res.runner() != null and adr_res.runner().pick_next_node(int(adr_res.runner().next_candidate_ids()[0]))
and ((adr_res.runner().current_battle().encounter.get("tree_rules", []) as Array) == rules_e))
and ((adr_res.runner().current_battle()._facts.encounter.get("tree_rules", []) as Array) == rules_e))
_meta_smoke_cleanup()
@@ -705,16 +705,16 @@ func _smoke_tree_rules() -> void:
Kit.check("守侧 rig:首轮后 queue 干净(开局 posture queue 已兑现)",
int(orch.action_economy.pending_ap_delta()) == 0)
# 场景①:当回合新封印、结算判标错且封印仍在场 → queue +1(本轮 AP 零变化=queue 承载、下轮 notify_round_start 兑现)
orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 1)
orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 1)
Kit.check("守①:当回合标错且封印在场 → 下轮 AP+1(queue 排队——本轮 AP 逐位零变化)",
int(orch.action_economy.pending_ap_delta()) == 1)
# 场景③④:之前回合标错重判(封印在场状态)与同格重复——去重闸挡回,最多返还一次
orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 2)
orch._grant_seg._judge_seal({"cell": safe, "hit": false}, 3)
orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 2)
orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": false}, 3)
Kit.check("守③④:旧封印重判零触发+同格最多一次(_seal_miss_seen 去重闸=「当回合标错动作」判别器)",
int(orch.action_economy.pending_ap_delta()) == 1)
# 场景②负样本:封印命中(hit=true 正向兑付路径)零触发
orch._grant_seg._judge_seal({"cell": safe, "hit": true}, 4)
orch._rounds._grant_seg._judge_seal({"cell": safe, "hit": true}, 4)
Kit.check("守②负样本:标中/被挖封印零触发(标错事件只在 miss 分支发射——发射面过滤承载)",
int(orch.action_economy.pending_ap_delta()) == 1)
# 场景⑤:末轮判出→无下一轮=queue 不兑现(编排器每战新建——队列实例随战亡,合法作废态)
@@ -732,12 +732,12 @@ func _smoke_tree_rules() -> void:
var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004),
{"tree_rules": []}) # 构造注入(拆件批 T3——setter 群退役)
Kit.check("散未激活:商品标价原价(无 economy_spend 键)",
int(lr.current_price({"base_price": 100, "price_increment": 0})) == 100 + int(_db.run.shop.price_level_bonus_at(0)))
int(lr.shop.current_price({"base_price": 100, "price_increment": 0})) == 100 + int(_db.run.shop.price_level_bonus_at(0)))
lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777005),
{"tree_rules": ["economy_spend"]}) # 构造注入(拆件批 T3——setter 群退役)
Kit.check("散激活:商品购买价 ×80%(provisional 折率——floor 偏向玩家、下限 1;刷新价不折=另一计价点零触碰)",
int(lr.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount))))
and int(lr.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount)))))
int(lr.shop.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount))))
and int(lr.shop.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount)))))
# 财富之道·聚:战斗掉落乘算·入账前(编排 _roll_gp_drop——同种子配对对拍 1.5×)
var orch3 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005))
Kit.check("聚 rig:基线编排启动(无增益侧)",
@@ -746,7 +746,7 @@ func _smoke_tree_rules() -> void:
"feats": [], "tree_rules": [], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}))
orch3.encounter["tree_rules"] = []
orch3._facts.encounter["tree_rules"] = []
var gp_base: int = orch3._roll_gp_drop()
var orch4 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005))
Kit.check("聚 rig:增益编排启动(tree_rules=economy_hoard)",
@@ -755,7 +755,7 @@ func _smoke_tree_rules() -> void:
"feats": [], "tree_rules": ["economy_hoard"], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}))
orch4.encounter["tree_rules"] = ["economy_hoard"]
orch4._facts.encounter["tree_rules"] = ["economy_hoard"]
var gp_hoard: int = orch4._roll_gp_drop()
Kit.check("聚:战斗掉落乘算 ×1.5(provisional——同种子配对 %d→%d,入账前=结算框显示实得)",
gp_hoard == int(round(float(gp_base) * 1.5)) and gp_hoard >= gp_base)
+3 -3
View File
@@ -42,9 +42,9 @@ func _init() -> void:
Kit.check_quiet(not e.stop().skip_empty_curio(),
"空配置闸反例:非空停点拒绝跳过(零自动推进契约不破)〔专长批 T10〕")
var lr = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
lr.set_shop_pool([], Callable(), Callable(), Callable())
lr._shop_draw_shelf()
Kit.check_quiet((lr.shop_bp_shelf() as Array).is_empty(),
lr.shop.set_shop_pool([], Callable(), Callable(), Callable())
lr.shop._draw_shelf()
Kit.check_quiet((lr.shop.shop_bp_shelf() as Array).is_empty(),
"空配置闸:商店空货架=合法态(牌库空=零扰动不崩)〔专长批 T10〕")
quit(Kit.finish("pool_case_d"))
+6 -1
View File
@@ -14,6 +14,7 @@ const RngScript := preload("res://core/rng_service.gd")
const BoardGeneratorScript := preload("res://core/board_generator.gd")
const RewardEconomyScript := preload("res://core/reward_economy.gd")
const OrchestratorScript := preload("res://core/battle_orchestrator.gd")
const BoardInstallerScript := preload("res://core/board_installer.gd")
const TAG_OK := "board_gen" # 在册合法标签(PURPOSE_TAGS 含;wr7 必须用合法标签——未知标签检查排在播种检查之前)
@@ -64,11 +65,15 @@ func _default_form(db) -> bool:
# ①wr5(p5):两协作件违约经组装点接线传导 → orchestrator.battle_result 落 "fatal_error"
# (棋盘生成半边:装板域拆件后生成器随 BoardInstaller 件内自建——组装点=引用束 fatal_cb 接线,
# 经新组装点构建后触发生成器违约,封印传导断言不变)
func _seal_propagation(db) -> bool:
var rng_a = RngScript.new()
rng_a.set_master_seed(5)
var oc_bg = OrchestratorScript.new(db, rng_a)
oc_bg._board_generator._fatal("测试违约①棋盘生成")
var bi_bg = BoardInstallerScript.new({"db": db, "rng": rng_a, "minesweeper": oc_bg.minesweeper,
"reward_economy": oc_bg._reward_economy, "encounter": oc_bg._facts.encounter, "fatal_cb": oc_bg._fatal})
bi_bg.generator._fatal("测试违约①棋盘生成")
if oc_bg.battle_result != "fatal_error":
push_error("[run_time_fatal_test/wr5] 棋盘生成违约未传导封印(battle_result=%s)" % str(oc_bg.battle_result))
return false
+10 -7
View File
@@ -1,6 +1,6 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 结算五段 Segment 单测(Segment 类直接 new() + 编排器段序
# 接线 + 真实一轮结算走段序出裁定/行)。段体逐位一致性由 parity_anchor 三变体对拍承载,
# 接线 + 真实一轮结算走段序出裁定/行)。
# 本测兜段类可独立实例化与编排器装配面。
# 运行:godot --headless --path game --script res://tests/unit/core/settle_segments_test.gd(expect 0)
extends SceneTree
@@ -23,12 +23,15 @@ func _init() -> void:
Kit.check_quiet(load("res://core/settlement/decay_segment.gd").new(oc) != null, "V4 DecaySegment 直接 new()〔迭代27 T2〕")
Kit.check_quiet(load("res://core/settlement/verdict_segment.gd").new(oc) != null, "V4 VerdictSegment 直接 new()〔迭代27 T2〕")
# V4-2 编排器 _init 接线(段持宿主引用;段序编排面在位)
Kit.check_quiet(oc._grant_seg != null and oc._grant_seg.oc == oc, "V4 编排器接线·授予段(宿主回指)〔迭代27 T2〕")
Kit.check_quiet(oc._aura_seg != null and oc._aura_seg.oc == oc, "V4 编排器接线·增益/减益段〔迭代27 T2〕")
Kit.check_quiet(oc._combat_seg != null and oc._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕")
Kit.check_quiet(oc._decay_seg != null and oc._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕")
Kit.check_quiet(oc._verdict_seg != null and oc._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕")
# V4-2 编排器 _init 接线(段持宿主引用;段序编排面在位)——注册面批同代归约 V1:
# 段件宿主直持改经 _rounds(轮循环驱动件)承载,接线断言改指 _rounds 段族
Kit.check_quiet(oc._rounds != null and oc._rounds.oc == oc, "V1 编排器接线·轮循环驱动件在座且宿主回指〔编排归约 V1〕")
Kit.check_quiet(oc._facts != null, "V1 编排器接线·战斗事实件在座〔编排归约 V1〕")
Kit.check_quiet(oc._rounds._grant_seg != null and oc._rounds._grant_seg.oc == oc, "V4 编排器接线·授予段(宿主回指)〔迭代27 T2〕")
Kit.check_quiet(oc._rounds._aura_seg != null and oc._rounds._aura_seg.oc == oc, "V4 编排器接线·增益/减益段〔迭代27 T2〕")
Kit.check_quiet(oc._rounds._combat_seg != null and oc._rounds._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕")
Kit.check_quiet(oc._rounds._decay_seg != null and oc._rounds._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕")
Kit.check_quiet(oc._rounds._verdict_seg != null and oc._rounds._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕")
# counter_add 写口真实路径(geologist 订阅行承载:公共命令链 begin_battle→move→dig→stop
# 经授予段 counter_add 单点落 counters_ctx battle 桶——受理载荷+公开读数口 fx_host().counters_ctx)
var rng_c = load("res://core/rng_service.gd").new()
+15 -10
View File
@@ -162,13 +162,13 @@ func _init() -> void:
"线香:装配起战(battle 级充能 1)")
var inv8 = load("res://core/treasure_items.gd").new(db)
var rec8: Dictionary = inv8.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
ob8.treasure_inventory = inv8
ob8.reward_grant().treasure_inventory = inv8
var h8: int = int(rec8.get("handle", -1)) # handle 取 add_item 返回(snapshot 投影无 handle 键)
var tgt8: Vector2i = _first_closed_safe(ob8)
Kit.check_quiet(tgt8 != Vector2i(-9, -9), "线香:夹具存在未开安全格")
var v8: Dictionary = ob8.reward_grant().use_treasure(h8, tgt8)
Kit.check_quiet(bool(v8.get("ok", false)), "线香正常:单格真视受理(cell 目标解析)")
Kit.check_quiet(bool(ob8.treasure_inventory.query_overlays().get("mine_reveal", []).has(tgt8))
Kit.check_quiet(bool(ob8.reward_grant().treasure_inventory.query_overlays().get("mine_reveal", []).has(tgt8))
and ob8.minesweeper.is_revealed(tgt8) == false,
"线香断言:目标格入真视叠加层且未被开格(真视≠开格——信息红线)")
Kit.check_quiet(inv8.snapshot_inventory().is_empty() or bool(inv8.snapshot_inventory()[0].get("used", false)),
@@ -226,11 +226,11 @@ func _init() -> void:
# ⑩ 刷新执照(商店免费刷新 +2/店——读数叠加)
var lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free),
Kit.check_quiet(int(lv10.shop.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free),
"刷新执照基线:未装配=表值(%d)" % int(db.run.adventure.shop_refresh_free))
lv10 = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new(),
{"attunement": ["refresh_license"]}) # 构造注入(拆件批 T3——setter 群退役)
Kit.check_quiet(int(lv10.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free) + 2,
Kit.check_quiet(int(lv10.shop.shop_refresh_free_left()) == int(db.run.adventure.shop_refresh_free) + 2,
"刷新执照正常:免费刷新 +2/店(读数即本店可用——进店重置生命周期同 knob)")
# ⑪ 天眼(3×3 真视+安全格直开/危险格不开/封印格跳过)
@@ -240,7 +240,7 @@ func _init() -> void:
"天眼:装配起战(level 级充能 1)")
var inv11 = load("res://core/treasure_items.gd").new(db)
var rec11: Dictionary = inv11.add_item("sky_eye", "attunement", Vector2i(-1, -1), 0)
ob11.treasure_inventory = inv11
ob11.reward_grant().treasure_inventory = inv11
var h11: int = int(rec11.get("handle", -1))
var tgt11: Vector2i = _first_closed_safe(ob11)
var v11: Dictionary = ob11.reward_grant().use_treasure(h11, tgt11)
@@ -254,7 +254,7 @@ func _init() -> void:
if ob11.minesweeper.is_revealed(c):
opened_safe = false
Kit.check_quiet(mines_shown == 0 or opened_safe, "天眼反例①:危险格仅显示不被开格(棋盘事实不可变)")
var overlay11: Array = ob11.treasure_inventory.query_overlays().get("mine_reveal", [])
var overlay11: Array = ob11.reward_grant().treasure_inventory.query_overlays().get("mine_reveal", [])
var overlay_ok := true
for c_v in (v11.get("resolved", []) as Array):
if not overlay11.has(c_v):
@@ -289,7 +289,7 @@ func _init() -> void:
"计数链:装配起战(GainJournal 携带)")
var inv13 = load("res://core/treasure_items.gd").new(db)
var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0)
ob13.treasure_inventory = inv13
ob13.reward_grant().treasure_inventory = inv13
var h13: int = int(rec13.get("handle", -1))
var tgt13: Vector2i = _first_closed_safe(ob13)
if tgt13 != Vector2i(-9, -9):
@@ -306,8 +306,13 @@ func _init() -> void:
"装配不可变:双件装配冒险启动")
Kit.check_quiet(adr14.runner().pick_next_node(int(adr14.runner().next_candidate_ids()[0])), "装配不可变:进首战")
var ob14 = adr14.runner().current_battle()
Kit.check_quiet(ob14 != null and (ob14.attunement as Array) == ["morning_order", "wide_scout_band"],
Kit.check_quiet(ob14 != null and (ob14._facts.attunement as Array) == ["morning_order", "wide_scout_band"],
"装配不可变:战斗运行态装配集与冒险装配集逐位同源(运行零写口——快照对比)")
# 注册面批同代编排归约 V5:库存注入改址(level_runner 注入改经 reward_grant() 出口直注)后
# 引用同一性——战斗域经出口持有的库存与关卡域同一实例;use_treasure 受理链经新地址走通
# 由本套件 ob8/ob11/ob13 三段既有受理链承载(注入面同批改址后全绿即证据)。
Kit.check_quiet(ob14 != null and ob14.reward_grant().treasure_inventory == adr14.runner().treasure_inventory,
"库存注入改址:战斗域经 reward_grant 出口持有的库存与关卡域同一引用〔编排归约 V5〕")
# ⑮ 扩展行收敛(迭代 38 卡④ wr2④⑤——套件首次 feats 注入,V6 形态自 round_flow_skill_pool_test 移植:
# enc 带 feats 键+ledger,专长触发经 _judge_seal 真实封印判定管线)
@@ -332,7 +337,7 @@ func _init() -> void:
Kit.check_quiet(mines15.size() == 3, "封印匠:板上存在三枚危险格(断言前提)")
if mines15.size() == 3:
for mv in mines15:
ob15._grant_seg._judge_seal({"cell": mv, "hit": true}, 1)
ob15._rounds._grant_seg._judge_seal({"cell": mv, "hit": true}, 1)
var smith_row := {}
for r in rows15:
if String(r.get("effect_id", "")) == "mine_neutralized":
@@ -354,7 +359,7 @@ func _init() -> void:
cell15b = cv
break
if cell15b != null:
ob15b._grant_seg._judge_seal({"cell": cell15b, "hit": true}, 1)
ob15b._rounds._grant_seg._judge_seal({"cell": cell15b, "hit": true}, 1)
var beacon_row := {}
for r in rows15b:
if String(r.get("effect_id", "")) == "fx_preview_area":
+1 -1
View File
@@ -3,7 +3,7 @@
# 在册词表组合;本测试直构 rebuild 同构合成行(与 effect_engine.gd rebuild 树规则段逐字段同源)
# 调 validate_trigger_row 断零违例。实现侧扩闭集未呈报时,本断言以在册词表为准转红。
# 聚散两键(economy_hoard/economy_spend)不入事件面:消费点=tree_rules 成员判定
# (battle_orchestrator._roll_gp_drop GP 掉落乘算 / level_runner._spend_discounted 商品折价),
# (battle_orchestrator._roll_gp_drop GP 掉落乘算 / shop_session._spend_discounted 商品折价),
# rebuild 树规则段对两键零合成行——此状态在此被显式断言(旧版对两键取空夹具静默跳过=假绿):
# rebuild 实装面行数对照——四键逐键与夹具同行数(rebuild 分支被删/漂移即转红),
# 两键断零行(将来被加合成行即转红,提示先复核效果闭集再同步夹具与实现)。
@@ -58,7 +58,7 @@ func _init() -> void:
main._start_adventure_from_menu()
await process_frame
# 入口停点选路进首战(真实停点链)
if not main.level_screen.visible:
if not main.ui.modals.level_screen.visible:
_problems.append("入口停点未呈现(前置失守)")
_finish()
return
@@ -100,12 +100,12 @@ func _init() -> void:
# ---- 断言①:排空前后核心状态逐位一致(演出关停零分叉)----
var snap_before: String = JSON.stringify(battle.diagnostic_snapshot())
var hp_before: int = int(battle.action_economy.ap) + int(battle.action_economy.rap) * 0 # 观测面双录
main._flush_row_queue()
main.ui.screens.row_pacer.flush()
var snap_after: String = JSON.stringify(battle.diagnostic_snapshot())
check(snap_before == snap_after, "排空前后核心诊断快照逐位一致(AC⑤ 演出关停零分叉)")
# ---- 断言②:零丢行(结构判据:排空后队列恒空 + 末信号行原文在带——append_row 直落不吞行)----
check(rows_seen.size() > 0, "结算行已产生(断言前提)")
check(main._row_queue.is_empty(), "排空后队列恒空(flush 完整性)")
check(main.ui.screens.row_pacer.pending_rows().is_empty(), "排空后队列恒空(flush 完整性)")
var last_text := ""
for i in range(rows_seen.size() - 1, -1, -1):
var cand_row: Dictionary = rows_seen[i]
@@ -114,7 +114,7 @@ func _init() -> void:
last_text = String(cand_row.get("display_name", ""))
if last_text != "":
break
check(last_text != "" and main.log_view.parsed_text().contains(last_text),
check(last_text != "" and main.ui.battle.log_view.parsed_text().contains(last_text),
"末信号行原文在明细带(排空不吞行:『%s』)" % last_text)
# ---- 断言③:段间延迟取数面双态 ----
UiKit.motion_scale = 1.0
@@ -129,7 +129,7 @@ func _init() -> void:
var fstage := FakeStage.new(null)
var ffx := FakeFx.new()
rp.stage = fstage
rp.board_view = main.board_view
rp.board_view = main.ui.battle.board_zone.board_view
rp.settlement_fx = ffx
rp._play_settlement_mark({"type": "damage", "target": "地精", "target_id": "goblin_b", "value": 3})
check(fstage.calls.size() == 1 and fstage.calls[0] == ["goblin_b", "地精"],
@@ -144,9 +144,9 @@ func _init() -> void:
UiKit.motion_scale = 1.0 # 常态入队(减动效快路同帧直落不入队——夹具须真落队列)
main._on_step_row({"type": "damage", "target": "hero", "value": 1})
main._on_step_row({"type": "heal", "target": "hero", "value": 2})
check(main._row_queue.size() == 2, "会话更替夹具已入队(前提:%d 行)〔wr4〕" % main._row_queue.size())
check(main.ui.screens.row_pacer.pending_rows().size() == 2, "会话更替夹具已入队(前提:%d 行)〔wr4〕" % main.ui.screens.row_pacer.pending_rows().size())
main._begin_session(false)
check(main._row_queue.is_empty(), "会话更替后待播队列清零(_begin_session 内机制 flush)〔wr4〕")
check(main.ui.screens.row_pacer.pending_rows().is_empty(), "会话更替后待播队列清零(_begin_session 内机制 flush)〔wr4〕")
_finish()
@@ -0,0 +1,84 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 战斗一屏样张批 V5–V9 断言面(2026-10-29 迭代 43)——五项实装定值的真实入口断言:
# V5 血条填充 = 危险红(UiKit.HP_BAR_FILL 值单点 + 绘制点源码锚:全仓零 #689CE8 与零裸
# Color(0.41,0.61,0.91);条底收 HP_BAR_TRACK 常量);
# V6 站桩名字行垫 HP_BAR_BACK 素底且不压地台(名字行基线 y < 地台顶 y——name_line_probe 几何出口);
# V7 顺位徽标提档 UiKit.FS_HINT(值断言 + 绘制点源码锚,§7.4 清单外 11–13px 提档);
# V8 护甲图标三键(light/ward/heavy)绑对应 24×24 PNG(ResourceLoader 装载结果 + name_line_probe
# 绑定面);
# V9 地台件 scene_stage_plaza.png + 护甲三枚入包可加载且尺寸 48×12 / 24×24(§5.2/§5.3 工单)。
# headless 无渲染面:像素采样以常量单点 + 源码锚 + 探针几何承载(观测面注记,验收对照表 V5 行)。
# 运行:godot --headless --path game --script res://tests/unit/presentation/standee_present_values_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const StandeeBodyScript := preload("res://presentation/views/standee_body.gd")
# 源码锚读面(静态判据——源文件即提交面)
func _read_source(rel_path: String) -> String:
var f := FileAccess.open(rel_path, FileAccess.READ)
if f == null:
return ""
return f.get_as_text()
func _init() -> void:
# ---- V5 血条填充 = 危险红 #A8321F(§4.2 语义物件显式落法) ----
Kit.check("血条填充常量 = 危险红 #A8321F〔样张批 V5〕",
UiKit.HP_BAR_FILL.to_html(false) == "a8321f")
Kit.check("血条条底收常量 HP_BAR_TRACK(视图原裸值原样入册)〔样张批 V5〕",
UiKit.HP_BAR_TRACK.is_equal_approx(Color(0.10, 0.10, 0.11)))
var body_src := _read_source("res://presentation/views/standee_body.gd")
Kit.check("血条绘制点引用 UiKit.HP_BAR_FILL(源码锚)〔样张批 V5〕",
body_src.contains("UiKit.HP_BAR_FILL"))
Kit.check("全源零违例值 #689CE8 与零裸 Color(0.41,0.61,0.91)(源码锚)〔样张批 V5〕",
not body_src.contains("689CE8") and not body_src.contains("689ce8")
and not body_src.contains("0.41, 0.61, 0.91"))
# ---- V6/V8 站桩实例面(名字行几何 + 护甲绑定) ----
var standee: Node2D = StandeeBodyScript.new("enemy")
root.add_child(standee)
standee.set_data({"display_name": "地精", "enemy_id": "goblin", "hp": 3, "max_hp": 3,
"order": 1, "armor_type": "light"})
var nl: Dictionary = standee.name_line_probe()
Kit.check("名字行基线 y < 地台顶 y(文字禁压地台——V6 判据坐标面)〔样张批 V6〕",
float(nl.baseline_y) < float(nl.plinth_top_y))
Kit.check("素底牌底沿 ≤ 地台顶(整行不越地台)〔样张批 V6〕",
Rect2(nl.plate).end.y <= float(nl.plinth_top_y) + 0.01)
Kit.check("名字行素底 = HP_BAR_BACK 深档 #282220(挂载底色口径)〔样张批 V6〕",
Color(nl.plate_color).to_html(false) == "282220")
Kit.check("基线读数与实装几何同源(基线 = 地台顶上方 9px 档:149 < 158)〔样张批 V6〕",
absf(float(nl.baseline_y) - 149.0) < 0.51 and absf(float(nl.plinth_top_y) - 158.0) < 0.51)
# ---- V7 顺位徽标提档 FS_HINT ----
Kit.check("FS_HINT = 14(§7.4 六档)〔样张批 V7〕", UiKit.FS_HINT == 14)
Kit.check("顺位徽标绘制点取 UiKit.FS_HINT(源码锚)〔样张批 V7〕",
body_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT"))
# ---- V8 护甲图标三键绑 24×24 PNG ----
var armor_paths := {"light": UiKit.TEX_ARMOR_LIGHT, "ward": UiKit.TEX_ARMOR_WARD,
"heavy": UiKit.TEX_ARMOR_HEAVY}
for key: String in armor_paths:
var tex := UiKit.load_tex(armor_paths[key])
Kit.check("护甲 %s 键绑对应 PNG 且 24×24(§5.3 工单)〔样张批 V8〕" % key,
tex != null and tex.get_width() == 24 and tex.get_height() == 24)
var ward_standee: Node2D = StandeeBodyScript.new("enemy")
root.add_child(ward_standee)
ward_standee.set_data({"display_name": "蛛化卓尔精灵", "enemy_id": "drider", "hp": 5, "max_hp": 5,
"order": 1, "armor_type": "ward"})
var nl_ward: Dictionary = ward_standee.name_line_probe()
Kit.check("甲档键 → 纹理路径绑定(name_line_probe 出 armor_icon 路径)〔样张批 V8〕",
bool(nl_ward.armor_tex_bound) and String(nl_ward.armor_icon) == UiKit.TEX_ARMOR_WARD)
# ---- V9 地台件与护甲三枚入包可载且尺寸对 ----
var plaza := UiKit.load_tex(UiKit.TEX_STAGE_PLAZA)
Kit.check("地台件 scene_stage_plaza.png 可加载且 48×12(§5.2 工单)〔样张批 V9〕",
plaza != null and plaza.get_width() == 48 and plaza.get_height() == 12)
for key: String in armor_paths:
var tex9 := UiKit.load_tex(armor_paths[key])
Kit.check("护甲三枚入包(24×24)且路径常量在 UiKit 单点〔样张批 V9〕" if key == "light"
else "护甲三枚入包(24×24)〔样张批 V9〕",
tex9 != null and tex9.get_width() == 24 and tex9.get_height() == 24)
quit(Kit.finish("standee_present_values"))
@@ -0,0 +1 @@
uid://c7a6acc5aici0
@@ -675,8 +675,9 @@ func _smoke_contrast_inks() -> void:
Kit.check("V4 RAP 反馈行 STONE_DEEP 对数据板 %.2f:1 ≥4.5:1" % r_rap, r_rap >= 4.5)
# 消费点源码锚定:新墨在位、旧裸用点清零、语义岗常量不动
var sb_src: String = FileAccess.get_file_as_string("res://presentation/views/standee_body.gd")
Kit.check("V4 顺位徽标①②③ 墨=GOLD_BRIGHT(13px 行原位)",
sb_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_BRIGHT"))
Kit.check("V4 顺位徽标①②③ 墨=GOLD_BRIGHT(FS_HINT 14 提档原位——迭代 43 V7 换代替代,旧裸 13 清零)",
sb_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, UiKit.GOLD_BRIGHT")
and not sb_src.contains("-1, 13, UiKit.GOLD_BRIGHT"))
Kit.check("V4 魔法护甲徽标墨=HERO_BLUE_BRIGHT", sb_src.contains("col = UiKit.HERO_BLUE_BRIGHT"))
Kit.check("V4 重甲徽标墨=GOLD_BRIGHT", sb_src.contains("col = UiKit.GOLD_BRIGHT"))
Kit.check("V4 意图蓄力档判据=分档布尔(standee_body:179 CHARGE_SKILL_IDS 判定;旧 GOLD_DEEP 恒不可达判据清零)〔wd2〕",
@@ -721,8 +722,8 @@ func _smoke_diag_button_retired() -> void:
var main_src: String = FileAccess.get_file_as_string("res://presentation/main.gd")
Kit.check("V6 主区无 build_diag_button 调用残留(旧实现同批清理)",
not main_src.contains("build_diag_button"))
Kit.check("V6 转储入口连接随迁(组合根接浮窗 diagnose_requested 信号)",
main_src.contains("debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)"))
Kit.check("V6 转储入口连接随迁(组合根经注册面接浮窗 diagnose_requested 信号)〔注册面 V2〕",
main_src.contains("ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)"))
Kit.check("V6 转储编排承接函数在位(dump_diagnosis 卡死取证保留)",
main_src.contains("func _on_diagnose_pressed()"))
var asm_src: String = FileAccess.get_file_as_string("res://presentation/ui_assembler.gd")
@@ -0,0 +1,245 @@
# 双实现对拍配对套件(注册面批同代测速循环批 v1.1——帧相位等价的执行化证据):
# 产品码(process_frame 信号步进状态机)vs 夹具(现行 await 形态逐字副本——循环内 await+收尾四语句在 run() 尾)。
# 五面对拍(帧索引序列/运行态序列/进度串序列/stats 六键/报告全文)逐位相等=
# 每步恰一帧+相位同点+输出逐字的当轮真实执行证据(测量语义逐点等价四判据的落码面)。
# 拓扑钉死:测试宿主先连 process_frame 帧计数、后启动两形态——两形态下「计数先于步进体」相对次序同构,序列可比。
# n=3 常规案+n=0 退化案(同步收尾零帧——_mean/_p90 空守卫渲染面)。
# 运行:godot --headless --path game --script res://tests/unit/presentation/tune_sim_batch_parity_test.gd(expect 0)
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const TuneSimScript := preload("res://presentation/tune_sim_controller.gd")
# 假调试面板(wd8 同形 setter 面+进度/运行态/报告三记录数组供序列对拍)
class FakePanel extends Node:
var running := false
var progress := ""
var report := ""
var atk := 0
var hp := 0
var progress_log: Array[String] = []
var report_log: Array[String] = []
var running_log: Array[bool] = []
func atk_pct() -> int:
return atk
func hp_pct() -> int:
return hp
func set_sim_running(r: bool) -> void:
running = r
running_log.append(r)
func set_sim_progress(t: String) -> void:
progress = t
progress_log.append(t)
func set_sim_report(t: String) -> void:
report = t
report_log.append(t)
# 假自动策略(固定返回值两档 victory/defeat 交替——wd8 :53-55 同款取值,零随机;
# 逐局记录帧索引与运行态读数——帧索引经宿主注入的计数 Callable 读取)
class FakeDriver extends RefCounted:
var ctrl
var frame_counter: Callable # 宿主帧计数(0 参 Callable——拓扑钉死:计数先于步进体)
var seen: Array[bool] = []
var frames: Array[int] = []
var calls := 0
func run_level(_db: Variant, _seed: int, _level: int, _atk: int, _hp: int, _other: bool, _logger: Variant) -> Dictionary:
calls += 1
seen.append(ctrl.is_running())
frames.append(int(frame_counter.call()))
if calls % 2 == 1:
return {"hero_dmg": 12.0, "rounds": 4, "result": "victory", "hp_left": 3.0, "hp_at_boss": 5.0}
return {"hero_dmg": 6.0, "rounds": 2, "result": "defeat",
"defeat_node_index": 3, "defeat_node_type": "battle", "hp_left": 0.0, "hp_at_boss": 0.0}
# 假配置库(TuneSimController 只读 db.run.action_economy.ap_per_round——wd8 同款最小域对象图)
class FakeActionEconomy:
var ap_per_round := 2
class FakeRun:
var action_economy := FakeActionEconomy.new()
class FakeDB:
var run := FakeRun.new()
# 现行 await 形态逐字副本(tune_sim_controller.gd run(n) 改形前形态——循环内 await+收尾四语句在 run() 尾;
# 全仓唯一在测夹具——产品码禁留第二实现,tests/ 不在 S13/S8/S9/S10 扫描面、夹具内循环 await 合法在册)。
# 与产品码新形态的申报差异仅一处:批末把 stats 六键束存 out_stats 成员供对拍读取
# (产品码 stats 经报告公开面观测——内部字段直查受控例外申报在卡 §17)。
class AwaitLoopRunner extends RefCounted:
const SEED_BASE := 4200
const ParityLogger := preload("res://presentation/playtest_logger.gd")
var db
var panel
var _base_hp := 0
var _driver = null
var _running := false
var out_stats: Dictionary = {}
func _init(db_ref, panel_ref, base_hp: int, driver = null) -> void:
db = db_ref
panel = panel_ref
_base_hp = base_hp
_driver = driver
func is_running() -> bool:
return _running
func run(n: int) -> void:
_running = true
var logger = ParityLogger.new()
logger.ap_per_round = int(db.run.action_economy.ap_per_round)
logger.start_session(SEED_BASE)
panel.set_sim_running(true)
panel.set_sim_progress("模拟中:0/%d …" % n)
var stats := {"wins": 0, "dmg_all": [], "hp_left_win": [], "hp_boss": [], "rounds_all": [], "death_nodes": {}}
var autoplay = _driver
for i in range(n):
_run_one_level(autoplay, stats, i, panel, logger)
panel.set_sim_progress("模拟中:%d/%d …" % [i + 1, n])
await panel.get_tree().process_frame
logger.close_session()
panel.set_sim_report(HudPresenter.tune_sim_report(n, panel.atk_pct(), panel.hp_pct(), stats, _base_hp))
panel.set_sim_running(false)
_running = false
out_stats = stats
func _run_one_level(autoplay, stats: Dictionary, i: int, panel, logger) -> void: # type-hint: ignore(驱动/面板/日志件鸭子型注入——同产品码构造缝)
var st = autoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger)
(stats["dmg_all"] as Array).append(st.hero_dmg)
(stats["rounds_all"] as Array).append(st.rounds)
if st.result == "victory":
stats["wins"] = int(stats["wins"]) + 1
(stats["hp_left_win"] as Array).append(st.hp_left)
(stats["hp_boss"] as Array).append(st.hp_at_boss)
elif st.result == "defeat":
var key := "%d.%s" % [st.defeat_node_index, st.defeat_node_type]
stats["death_nodes"][key] = int(stats["death_nodes"].get(key, 0)) + 1
func _init() -> void:
var frame_idx := {"i": -1} # 帧计数(宿主最先连——拓扑钉死「计数先于步进体」)
process_frame.connect(func() -> void: frame_idx["i"] += 1)
await process_frame # 树初始化就绪帧(SceneTree._init 时点 root 未挂树——面板 get_tree() 首帧后才有效;wd8 同款先例:panel 段前有 await 过帧)
var r3 := await _run_pair(3, frame_idx)
_check_n3(r3)
var r0 := await _run_pair(0, frame_idx)
_check_n0(r0)
quit(Kit.finish("tune_sim_batch_parity"))
# 对拍驱动:两形态各经 run(n) 真实入口(同参假面板/假驱动各自实例);先启动两形态、后分别 await——
# 两形态在调用点同一同步段内先后挂起(并行于同一帧时钟),驱动协程返回 int 承接 void run 的可等待面
func _run_pair(n: int, frame_idx: Dictionary) -> Dictionary:
# 两形态串行驱动(各经 run(n) 真实入口 await 调用——GDScript 协程动态调用必须 await,
# 无并行启动形态):帧序列各自记录、断言按相对首帧偏移归一后逐位比对——拓扑不变式
# 「计数先连、启动后于计数、计数先于步进体」在两形态下相对次序同构,等价证据结构不变。
var panel_a := FakePanel.new()
root.add_child(panel_a)
var drv_a := FakeDriver.new()
var a: Variant = TuneSimScript.new(FakeDB.new(), panel_a, 100, drv_a)
drv_a.ctrl = a
drv_a.frame_counter = func() -> int: return frame_idx["i"]
await a.run(n)
var panel_b := FakePanel.new()
root.add_child(panel_b)
var drv_b := FakeDriver.new()
var b: Variant = AwaitLoopRunner.new(FakeDB.new(), panel_b, 100, drv_b)
drv_b.ctrl = b
drv_b.frame_counter = func() -> int: return frame_idx["i"]
await b.run(n)
return {"a": a, "b": b, "pa": panel_a, "pb": panel_b, "da": drv_a, "db": drv_b}
func _check_n3(r: Dictionary) -> void:
var pa: FakePanel = r["pa"]
var pb: FakePanel = r["pb"]
var da: FakeDriver = r["da"]
var drv_b: FakeDriver = r["db"]
var a = r["a"]
var b = r["b"]
Kit.check("n=3 帧序列相对偏移逐位相等(%s vs %s)+相邻差恒一(每步恰一帧)〔测速 V1〕",
str(_norm(da.frames)) == str(_norm(drv_b.frames)) and da.frames.size() == 3
and _consecutive(da.frames) and _consecutive(drv_b.frames))
Kit.check("n=3 运行态读数序列逐位相等+收尾双运行态假+面板置假序列同构〔测速 V2〕",
str(da.seen) == str(drv_b.seen) and da.seen == [true, true, true]
and not a.is_running() and not b.is_running()
and not pa.running and not pb.running
and str(pa.running_log) == str(pb.running_log))
Kit.check("n=3 进度串序列逐位相等(%d 段)〔测速 V3〕",
str(pa.progress_log) == str(pb.progress_log) and pa.progress_log.size() == 4)
var got: Dictionary = r["b"].out_stats
var deaths: Dictionary = got.get("death_nodes", {})
Kit.check("n=3 夹具 stats 六键与按假驱动固定返回值重算的期望逐键相等〔测速 V4〕",
int(got.get("wins", -1)) == 2
and _arr_eq(got.get("dmg_all"), [12.0, 6.0, 12.0])
and _arr_eq(got.get("hp_left_win"), [3.0, 3.0])
and _arr_eq(got.get("hp_boss"), [5.0, 5.0])
and _arr_eq(got.get("rounds_all"), [4, 2, 4])
and deaths.size() == 1 and int(deaths.get("3.battle", -1)) == 1)
Kit.check("n=3 报告全文逐字相等且含渲染面四关键串〔测速 V5〕",
pa.report == pb.report and pa.report != ""
and pa.report.contains("关卡完成率") and pa.report.contains("关卡完成剩余 HP 均")
and pa.report.contains("败北节点") and pa.report.contains("N=3"))
func _check_n0(r: Dictionary) -> void:
var pa: FakePanel = r["pa"]
var pb: FakePanel = r["pb"]
var da: FakeDriver = r["da"]
var drv_b: FakeDriver = r["db"]
var a = r["a"]
var b = r["b"]
Kit.check("n=0 退化:两形态驱动零回调、帧索引序列空(同步收尾零帧)〔测速 V1〕",
da.frames.is_empty() and drv_b.frames.is_empty() and da.calls == 0 and drv_b.calls == 0)
Kit.check("n=0 运行态置假序列 [true, false] 双形态同构+收尾双假〔测速 V2〕",
str(pa.running_log) == str(pb.running_log) and pa.running_log == [true, false]
and not a.is_running() and not b.is_running())
Kit.check("n=0 进度串单段逐字相等〔测速 V3〕",
str(pa.progress_log) == str(pb.progress_log) and pa.progress_log == ["模拟中:0/0 …"])
Kit.check("n=0 夹具 stats 六键零计〔测速 V4〕",
int((r["b"].out_stats as Dictionary).get("wins", -1)) == 0
and ((r["b"].out_stats as Dictionary).get("dmg_all") as Array).is_empty()
and ((r["b"].out_stats as Dictionary).get("hp_left_win") as Array).is_empty()
and ((r["b"].out_stats as Dictionary).get("hp_boss") as Array).is_empty()
and ((r["b"].out_stats as Dictionary).get("rounds_all") as Array).is_empty()
and ((r["b"].out_stats as Dictionary).get("death_nodes") as Dictionary).is_empty())
Kit.check("n=0 报告全文逐字相等且非空(空 stats 渲染面)〔测速 V5〕",
pa.report == pb.report and pa.report != "")
# 帧序列归一(相对首帧偏移——串行驱动下两形态帧段错位、结构可比性归一承载)
func _norm(fr: Array) -> Array:
var out: Array = []
var base := int(fr[0]) if fr.size() > 0 else 0
for i in fr.size():
out.append(int(fr[i]) - base)
return out
# 相邻差恒一(每步恰占一帧——判据①的序列面证据)
func _consecutive(fr: Array) -> bool:
for i in range(1, fr.size()):
if int(fr[i]) - int(fr[i - 1]) != 1:
return false
return true
# 数组逐元素浮点比对(stats 桶重算期望——类型化比较,消 str 形态歧义)
func _arr_eq(a, b: Array) -> bool:
var aa := a as Array
if aa == null or aa.size() != b.size():
return false
for i in b.size():
if absf(float(aa[i]) - float(b[i])) > 0.0001:
return false
return true
+26
View File
@@ -0,0 +1,26 @@
# 迭代 44 反哺点清单——2026-10-30
要写进权威文档的点逐条列落点(改哪一节、改成什么、依据);用户在会话中裁决后归文档批落文。实现批禁写 `design/` 与根规则文件,本清单是唯一移交通道。
## 架构文档(design/engineering/architecture/architecture.md)
1. **「模块归属·呈现层」ui_registry 行现状化**——现登记「实现批建件」态;实现后改现状陈述:UiRegistry/UiScreens/UiBattle/UiModals 四件已建(全 RefCounted、类变量 3/9/5/14、UiModals.flow 随容器构造即建)。
2. **同表 main_ui_build 行「层序逻辑不在此件——唯一事实源仍是 main.LAYER_CONTRACT + _apply_layer_contract()」句失效**——唯一事实源已迁 `ui_registry.LAYER_CONTRACT`+`ui.apply_layer_contract(root)`;该句改指 ui_registry(依据=设计文档 §3.3 建议句+实现实况)。
3. **「呈现层装配与会话生命周期」节屏成员寻址迁注册面句核对**——v1.25 已预写「探针挂载假设=main.ui.<域>.<句柄>」;实现实况与其一致,但需补两句实况细节:①ModalFlow 实例随 UiModals 容器构造建立(装配早期注入点依赖其先在座);②探针契约面新增两件取证探针(shot_probe/fx_shot_probe)随批改址。
4. **「战斗编排域拆件定稿」节轴丁两行「构造=引用束注入」句与实态口径差**——实态=RoundCycle 三参直构(host/fx_host_ref/reward_grant_ref,五段构造透传不落成员)、BattleFacts 零参直构;句按设计文档 §8 预判「三参直构无需束」现状化。
5. **「模块归属·功能层」RoundCycle/BattleFacts 两行实测值换代**——RoundCycle 类变量 8/公开方法 3;BattleFacts 类变量 15(贴顶)/公开方法 4(reset_round+三 mark);宿主 battle_orchestrator 实测 15 成员/811 行(文件长键 1035 已远超实况——基线 refresh 后自然消失)。
6. **「测试与质量域」S13 豁免第三条注销句现状化**——v1.25 预写「设计闭合、实现批注销」;键账已达成(宿主实测 15),注销句转已实施事实。**同节 tune_sim 测量面豁免注销句现状化随卡③ v1.1 恢复**:裸键已消除(实测命中消失、全文件零循环);注销句保持(注销对象是 await 豁免、不依赖行为改动申报),句内「随批申报行为改动一处(收尾置假还原)」半句已失效(前提被迭代 41 测试债批 215da315 消化、申报已剥离)——现状化时一并修正为收尾现行陈述(「运行态收尾在 run() 尾每批一次执行」)。
## gdstyle 基线与闸面(tools/)
7. **基线 refresh 义务(用户人工,非落文)**——`python tools/gdstyle_gate.py --refresh`:预期消失键=main.gd max-class-variables=49(实测 15)+battle_orchestrator max-class-variables=38(实测 15)+battle_orchestrator max-file-length=1035(实测 811)+轴丙遗留归绿键;合计消失 ≥2 条触发骤降守卫,须 `--force`。与待办清单「零豁免五键清偿批」批末一次人工 refresh 合并执行。
8. **S13 no-self-assign 检查的误报形态观察**(登记不阻塞)——`X.y.z = y` 形态(赋值链尾名与右侧成员同名)被判自赋值;本批以 self 前缀消解(level_runner:520)。若后续批次再现此巧合,可考虑工程批把检查器改为比较完整接收链(登记归「迭代 42 批末移交小件」同类工具小件)。
## 待办清单(根流程清单——状态回退拟稿,落笔归上游段/用户)
9. **「零豁免五键清偿批」条目测速循环批子项状态(净零处理——原回退拟稿作废)**:卡③ 停卡呈报→用户裁重裁→写卡会话重写 v1.1(零待裁点,重裁时订正 S8 十文件与两处行号注记)→本会话恢复施工收口——条目状态直推「文档设计已闭合+已写卡+已编迭代 44」无回退中间态;重裁经过实况记录于实现报告 §5/§5.1,本条不再承载状态回退义务。
10. **「零豁免五键清偿批」条目键账进度换代**——五键中两键已真实清偿(main.gd 49→15、battle_orchestrator 37→15),待基线 refresh 收账;呈现层状态件批(迭代 45 在册)两键不受影响。
## 其他
11. **main.gd 死 preload 存量**(工程小件登记建议)——拆件遗留的零使用 preload 常量 8 条(UiAssembler/TuneSimController/DebugSessionScript/TreasureInteractController/CurioInteractController/CampShopPresenter/BoardResolversScript/三场景 preload 中 main 零使用者);本批仅清理与迁移直接相关的 ModalStack/RowPacer 两条,其余属存量债不扩范围;归工程改动直改批顺路清理。
+118
View File
@@ -0,0 +1,118 @@
# 迭代 44 实现报告(组合根注册面批+战斗编排归约批+测速循环批)——2026-10-30
状态:三卡全部施工收口(卡③ 按停卡呈报建议案重裁为 v1.1 后施工);迭代总收口(批末闸/审计/合并)进行中。
## 0. 各卡状态
| 卡 | 状态 | 提交(worktree iter44,基点 c8f5091d) |
|---|---|---|
| 卡① 组合根注册面批 | **收口**——main.gd 49→15、四新件建件、全消费面改址 | bee0682f(T1 建件)/b89ee850(T2 改形)/784d581b(T3 改址)/1929516a(T4 消灭收口) |
| 卡② 战斗编排归约批 | **收口**——battle_orchestrator 38→15(实测 37 轴丙基数起算)、RoundCycle/BattleFacts 两新件 | 24f3ca51(T1 BattleFacts)/a832965a(T2 RoundCycle)/15c833e0(T3 宿主收束)/3e9d3afa(T4 键账) |
| 卡③ 测速循环批 | **收口(v1.1 重裁后施工)**——v1.0 停卡呈报→用户裁重裁→v1.1 剥离行为改动申报面后 T0–T3 全落 | c82ef23f(T1 步进状态机)/04783fae(T2 对拍套件) |
## 1. 卡① 实际对照表(卡片 §8 十一行 → 实况)
| # | 需求点 | 改动实况 | 验收测试实况 | 结果 |
|---|---|---|---|---|
| 1 | 注册面建件+合同机器迁驻 | ui_registry.gd(LAYER_CONTRACT 逐字迁入+node(key) 19 键 match+apply_layer_contract 唯一落位口)+ui_screens/ui_battle/ui_modals 三容器 | 系统测试 V1c(system_test.gd:94-106 改址 main.ui.LAYER_CONTRACT/main.ui.node,尾缀〔装配生命批 V1〕保留)——树序逐对保序 | 绿 |
| 2 | main.gd 改形回归(49→15) | 28 迁出+7 消灭+1 新增=22(T2 中间态)→15(T4 终态);六转发方法+_flush_row_queue 退役 | run_smoke 15 项 0 失败+run_system_test 69 项 0 失败;S13 静态判据=gdstyle 实测 15(基线键 49 已不命中、成死键) | 绿 |
| 3 | 装配与流程件改址 | main_ui_build 八步赋值目标 root.ui.*(八步顺序/add_child 序/连接语句逐条不变);三流程件同表映射;smoke_boot get() 4 改 2 留 | 冒烟主屏存活面六断言+系统测试 V2 句柄等价五连比(system_test.gd:108-109 改址) | 绿 |
| 4 | 探针面批量改址 | 探针九件+probe_ui_util+shot_probe/fx_shot_probe(决定记录 2)共约 460 行机械改写 | probe_gate --click --full 全段退出码 0(九件 PASS) | 绿 |
| 5 | 系统测试面改址+RunScope 生命周期 | system_test V1a/V1c/V2/V3 矩阵取址改指 | V1–V9 全绿(V3a–V3i 存活性矩阵第五引用面逐面同源) | 绿 |
| 6 | 别名/接缝成员消灭(7 个) | _seed_edit/board_view/ap_bar/_row_queue/_modals/_settlement_stop_pending/_deferred_modal 全删;消费方改址(pacing_queue 4 处+接缝探针三件+main 自身) | pacing_queue 绿+ui_struct_probe 登记序断言(改 flow.stack)+t17_defer_probe 挂起态断言(改 flow._deferred) | 绿 |
| 7 | 六转发模态方法退役 | _open_modal/_close_modal/_on_modal_closed/_input_locked/_open_modal_after_playback/_on_pump_ended 退役;调用方改指 ui.modals.flow 五真方法+pump_ended 注入改 flow 方法 | modal_flow 绿+系统测试 V3g 模态开合段+ui_click_probe 模态开合路径 | 绿 |
| 8 | V1d 源码断言重锚 | source_guard_test.gd 重锚全 presentation 目录扫描(move_child 只许 ui_registry.apply_layer_contract 体内),尾缀〔注册面 V1〕 | source_guard 绿(8 checks) | 绿 |
| 9 | V6 源码断言改串 | theme_projection_test.gd V6 改断言 ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed),尾缀〔注册面 V2〕 | theme_projection 绿(164 checks) | 绿 |
| 10 | 视觉零变化主证 | 全批呈现层改址零像素改动 | render_gate golden 零差异(shot_probe 七屏+golden 全过) | 绿 |
| 11 | 改址完整性收口 | 49 名逐名 grep 残留=0(词形态+字符串键形态两轮全量扫描,T3 提交信息留档) | 批末代码域闸归迭代总收口(待跑);grep 自检静态判据已留档 | 静态部分绿 |
静态判据申报执行实况(卡片 §8 域口径):S13 键账对账=gdstyle 实测 main.gd 15、四新件零新键(T1/T4 两轮 static_scan 确认);基线 refresh 留用户人工(见 §4 遗留义务)。
## 2. 卡② 实际对照表(卡片 §11 V0–V6 行 → 实况)
| 行 | 需求点 | 改动实况 | 验收测试实况 | 结果 |
|---|---|---|---|---|
| V0 | 行为零变化(总判据) | 成员迁移与改指;RNG 五流零触碰(behavior_roll 随件迁移取号序逐字);五信号留宿主;两快照签名载荷逐字 | 关联套件全绿零改址应红项(round_flow/settle_segments/skill_unlock/trigger_cond/seal_polarity/feat_vouchers/variant_placement/meta_forge/adventure_feats/treasure_effects/roll_multiplier/breakpoint_resume/level_shop_economy/table_structure_guard) | 绿 |
| V1 | 两新件建件接线 | battle_facts.gd(15 成员贴顶+reset_round 逐字+三 mark 写口)+round_cycle.gd(8 成员+三参直构+五段构造逐字迁入) | settle_segments_test V1 断言(_rounds 在座且 oc 回指+_facts 在座+五段段件齐 _rounds)〔编排归约 V1〕 | 绿 |
| V2 | reset_round 复位边界 | reset_round 只清 reveals/seals/path 三项 | round_flow_skill_pool_test 内嵌断言:终局后 presentation_snapshot()["refresh_used"] == 末轮结算快照 economy.refresh_used〔编排归约 V2〕 | 绿 |
| V3 | 段驱动与摇号迁移 | 三函数迁出;宿主调用点五处改址;trigger_cond 3 处改指 | round_flow 全套+settle_segments V4-3+skill_unlock+trigger_cond(改指后绿)——同种子回放断言零改址钉住 | 绿 |
| V4 | _hero_atk 派生读 | 成员删→私有函数;combat_segment :56 改调 | roll_multiplier_test 内嵌:override 贯通 spawn+缺省回退表值〔编排归约 V4〕;攻击单 base_value 贯通由既有 hero_atk_val=100 全链路夹具承载 | 绿 |
| V5 | treasure_inventory 注入改址 | 宿主成员与 setter 删、五处读改指、level_runner:520 改经 reward_grant() 出口直注 | treasure_effects_test 内嵌引用同一性断言〔编排归约 V5〕+ob8/ob11/ob13 既有 use_treasure 受理链改址后全绿 | 绿 |
| V6 | 封印判定账/装板溯源/受理事实迁移 | 事实族改指(grant_segment 18 处+宿主全量);三 mark 写口升级;attune 读源改 _facts | seal_polarity/feat_vouchers(契约 2 计数纯净 R2)/variant_placement/meta_forge/adventure_feats/table_structure_guard(attune 外部面零改址应绿——最硬的外部面零变化证)全绿 | 绿 |
行为改动申报核对:diff 中行为改动零条(全部非行为改动——成员迁移与改指;完美写口升级=同一时点同一增量改经方法单点)。测试削弱申报:零申报兑现(测试改指只动成员寻址;新增断言 V1/V2/V4/V5 四条+V1d/V6 重锚两条=卡片 §8 申报全集)。
## 3. 实现决定记录(代拍板白名单外事项备案)
卡①:
1. **ModalFlow 实例随 UiModals 容器构造即建**(ui_modals._init)——装配第二步 row_pacer.pump_ended 注入早于第八步 flow 建立,转发壳退役后需 flow 先行在座;查询/钩子 Callable 注入时点不变(_build_modal_flow 内语句序不变),八步顺序零变化。
2. **shot_probe/fx_shot_probe 纳入改址**——两件是 render_gate 取证探针、不在卡片「探针九件」清单(probe_gate 段构成),但同为 main.X 一级地址消费方;T3 改址完整性 grep 自检抓漏后纳入(render_gate 对照表行 10 的承载件,属消费面全量改址义务)。
3. **ui_tune_probe 零连接巡检白名单登记 flow.close**——表 D 申报「closed bind 目标改 flow.close.bind」的机检联动:巡检白名单原钉 main._on_modal_closed(组合根转发壳),改钉 main.ui.modals.flow.close(同一连接的新宿主);判据面「closed 外发消费目标是模态编排口」语义不变,conn_count=11 断言零变化。
4. **layout_probe 三区 get() 字符串键改容器字段键**——卡片表 D 字符串键清单只列 smoke_boot 6 处;layout_probe:60 `main.get(key)`(key="stage"/"info_panel"/"board_zone")为漏网字符串键,改 `main.get("ui").battle.get(key)`(战区域容器字段名与三区合同键同名)。
5. **curio_overlay/confirm_dialog 两词在装配/流程件替换表漏列**——main.gd 表有、main_ui_build 与三流程件表漏,冒烟(Invalid assignment on main.gd)与 ui_click_probe sl7(开窗调用静默失效)抓漏后补改;教训=机械改写必须逐词对照 T0 名单全量扫描(T3 终检已按 49 名逐词复查全域归零)。
卡②:
1. **grant_segment._seal_preview_area 的 oc.treasure_inventory 读为 T3 盘点漏项第五处**(卡片 T3 列宿主四处读)——SCRIPT ERROR 抓漏后改指 reward_grant.treasure_inventory。
2. **treasure_effects_test 五处直构注入 obX.treasure_inventory**(ob8/ob11/ob13 注入与读、ob14 段)——设计文档 §11 盘点「1 处」漏项;同属「成员寻址改指、断言零变化」改经 reward_grant() 出口。
3. **level_runner 注入行加 self 前缀**消 S13 no-self-assign 文本巧合(`X.treasure_inventory = treasure_inventory` 左侧链尾名与右侧成员同名);语义零变化。
4. **_hero_atk 派生读落位**:受理段写点删除、spawn 读点改调、combat_segment 消费改调——「读面即引用、无第二账」先例同款。
5. 宿主内 encounter/attunement 词替换以占位符保护三处字符串键(behavior_roll 前置流名 "encounter"/BoardInstaller bundle 键 "encounter"/enc.get("attunement") 键)——键字面零变化。
## 4. 遗留读数义务与登记项
- **gdstyle 基线 refresh(用户人工,批末总收口后)**:`python tools/gdstyle_gate.py --refresh`——卡① 死键 1 条(main.gd max-class-variables=49)+卡② 预期死键 1 条(battle_orchestrator max-class-variables=38;文件长键 1035 已由轴丙归绿基线在册需一并核对)+轴丙遗留归绿键,合计消失 ≥2 条触发骤降守卫须 `--force`(与待办清单「零豁免五键清偿批」批末一次人工 refresh 合并执行口径一致)。
- **实机目检总清单(登记不阻塞)**:①三屏切换与模态开合实机过一遍(卡① T4 义务——冒烟/探针/golden 已机检承载,真人观感复核登记);②战斗一局全流程观感(卡② 结构迁移零行为变化的一般性目检)。
- **能力清单核对(收尾核对义务)**:本批卡① 零新增能力(改址只换地址)、卡② 零新增能力(新增断言全部内嵌既有函数)——game/tests/README.md 零增删改,批末核对表上代表用例名与实际落地一致(未改名,核对通过)。
- **批末代码域闸**(run_all_gates.sh --code)归迭代总收口——卡③ 裁决后与 reshape 串联执行。
## 5. 卡③ 停卡呈报(缺陷移交——按卡片 §13 L3 条款①预设停机点)
**分歧点**:卡③ §3 断言「`_run_one_level` 尾部两行 :67 `panel.set_sim_running(false)`/:68 `_running = false` 每局执行一次(潜伏红本体)」与现行代码不符——实测两行在 `run()` 尾 :53-54(for 循环外、每批恰一次),`_run_one_level` 尾部 :67-68 实为败局节点记账。
**定谳证据**(三重):
1. git 历史:迭代 41 测试债批提交 215da315(2026-10-29 合回)提交信息明载「两个产品回归随批修(……**tune_sim 运行态收尾归还 run**)」——潜伏红已由该批先修复。
2. T0 实跑面一:`--only=standee_fx_sweep` 全绿(18 checks)——wd8 三断言(`drv.seen == [true, true]`/收尾双运行态假/报告非空,现位 :145-147)现行全 PASS。
3. T0 实跑面二:ui_tune_probe 全链路 PASS——⑤ 守卫环 1800 帧预算内退出、输出四串在(模拟 N=10 完整输出)。
**两种读法**:
- 读法 A(证据支持):卡片成文(2026-10-29 v1.0 定稿)时点潜伏红确实在(u23 卷入形态),同日合回的迭代 41 测试债批把收尾归还 run() 尾——卡片「现状考证」被上游批施工消化变质。215da315 提交信息与代码现状一致。
- 读法 B:卡片考证当时即误读(:67-68 为败局记账非置假)。与 215da315 提交信息矛盾,不采。
**影响面**:
- 已申报行为改动 V3(收尾置假还原)**无对象**——还原动作已由迭代 41 测试债批完成并申报。
- 目标 2「wd8 三断言与 ui_tune_probe ⑤ 零改动**转绿**」变「零改动**保持绿**」。
- 核心义务仍有效:S13 await-in-loop 裸键消除(tools/gdstyle_baseline.txt:15 在册、基线 16 键之一)与双实现对拍套件(帧相位等价执行化证据)——对拍夹具与产品码的申报差异由两处减为一处(收尾还原差异消失,仅余 stats 暴露成员)。
- T0「实跑定谳」义务已完成(上列三重证据),实现段零施工(停摆卡不提交)。
**建议案**(供写卡段/用户裁决):
1. 卡③ 退回写卡段重裁:剥离行为改动申报(对照表 V3 行删、§1 目标 2/3 措辞改「保持绿」)、T0 判读出口改「两面全绿=前提已由迭代 41 测试债批消化,直接按无行为改动形态开工」;保留裸键消除+双实现对拍义务(夹具=现行产品码 await 形态逐字副本——收尾在 run() 尾形态——与产品码唯一申报差异=夹具暴露 stats 供对拍)。
2. 若用户不愿重写卡:裁「卡③ 剥离出迭代 44」(roadmap 行拆批),裸键消除与对拍随下一迭代重编。
**待办清单条目状态回退拟稿**(落笔归上游段/用户,本批禁写根流程清单):「零豁免五键清偿批」条目的测速循环批子项状态由「文档设计已闭合+已写卡+已编迭代 44」回退为「文档设计已闭合+待写卡(现状考证已被迭代 41 测试债批消化——行为改动申报面失效,卡片重裁后重编)」。
## 6. 批末全量域闸与闸红修复(收口实况)
**闸况**:reshape 后第三次全量域闸(--code)全绿——00 冒烟/01 系统测试/03 场景探针/05 渲染闸/10 熵增闸绿;**02 单元与集成、09 静态扫描从基线红改善归绿**(基线记的迭代 41 时代存量红指纹已在册待换代——现真绿);回执 commit 对齐分支尖(b27db010)。
**闸红修复三笔(新红必修,均孤跑复绿)**:
1. S9 R1:tune_sim_controller `_sim_stats` 裸 Dictionary 注解改 `Dictionary[Variant, Variant]`(Variant 参数化现行口径)——本批新码写法违规。
2. ui_input_probe :482/:488 case⑮ 装配直构注入 `settlement.attunement` → `_facts.attunement`——卡② 成员迁移的探针消费漏网(T0 映射谱系只扫 main./root./self. 前缀,直构注入形态不在谱系);SCRIPT ERROR 中断 _ready 致探针挂死超时。
3. run_time_fatal_test :75 BoardInstaller 束键值 `oc.encounter` → `oc._facts.encounter`+meta_progression_forge_test 三处编排器 encounter 直读改指(:340/:749/:758)——同型漏网(负面套件名与触面无字面关联=关联档选测盲区;meta_forge 分片复现、单进程未现的夹具路径差异)。
**实现决定记录补(白名单外备案)**:跨卡触面(卡② 迁移成员被卡① 已改址的探针/未申报套件消费)只有全量闸能抓——关联档选测的盲区实证;全域终扫(encounter/attunement 直读形态)后同类漏网清零。
## 7. 独立验收审计结论与记录级缺漏补记
审计报告:reports/iter44-验收审计-2026-10-30.md——**结论=通过(可合并)**。21 行对照表逐行通过、判据绑定面自跑全绿、测试削弱零发现(37 条被删断言逐条有配对新行)、非行为改动成立、证据链在案。
记录级缺漏三项(不阻塞,随本节补记):
1. 卡③ 对拍串行驱动为白名单外决定——已在 §5.1 卡③ 实现决定记录 1 备案(审计方读的是早期版本);提交 04783fae 信息同载。
2. main_overlay_flow.gd:4 头注仍写已退役的 root._open_modal/_close_modal 转发方法名——词面遗留(卡面头注义务只列 main.gd 与 main_ui_build 两处);闸后代码面禁改(改则回执作废),留反哺点通道随文档批或下一代码批顺路改写。
3. 卡② grant_segment 事实族改指实测 18 处 vs 卡面「约 23」估计偏差——记录偏轻,本节补记(T0 点名清单实数为准)。
例外备案:V1d 扫描面为 presentation 平铺 27 文件口径(与卡面 glob 一致、只扩不缩;递归口径下批前既有 ui_kit.gd:819 即红——非本批引入)。
## 8. 停机状态与收口(进行中)
- 迭代总收口(冻结→并 master→reshape→批末全量域闸→独立验收审计→合并)**未启动**——按停机规则「迭代收口合并须等停摆卡闭合、或用户裁拆批后整体进行」。
- 卡① 卡② 全部收口提交在 worktree iter44 分支(9 笔提交),冻结待收口。
+99
View File
@@ -0,0 +1,99 @@
# 迭代 44 独立验收审计报告——2026-10-30
审计方:未参与实现的独立会话(只读审计+自跑最窄抽查)。对象=worktree D:\code\g-plan2-iter44 分支 iter44 尖 9f1dda40,对照基线 master 尖 ab3b4a9d;材料=三张需求卡片(组合根注册面批/战斗编排归约批/测速循环批 v1.1)+实现报告+闸回执与日志。本报告不修改任何代码与测试,缺漏如实记录不代修。
**结论行:通过(可合并)**——三卡 21 行对照表逐行核到落点、判据绑定面自跑全绿、测试削弱零发现(断言计数 22 文件零减少)、非行为改动判定成立、证据链在案;2 项记录级缺漏+1 处注释级陈旧指址+4 项例外备案,均不动行为、判据与验收闭合(见 §6),不构成合并阻塞。
## 1. 逐行核对——卡①(§8 十一行)
| # | 判定 | 凭据(文件:行/自跑) |
|---|---|---|
| 1 | 通过 | V1c 改址 `main.ui.LAYER_CONTRACT`/`main.ui.node`(system_test.gd:90/:94,尾缀〔装配生命批 V1〕保留);自跑 run_system_test 69 项 0 失败;LAYER_CONTRACT 与基线逐字一致(git show ab3b4a9d 对读,唯一差异=注释指址 `_apply_layer_contract`→`apply_layer_contract`,白名单 3) |
| 2 | 通过 | main.gd `^var` 计数 49(基线)→15(尖);自跑 run_smoke 15 项 0 失败+run_system_test 69 项 0 失败;gdstyle 零基线外新键 |
| 3 | 通过 | main_ui_build.gd `build()` 八步名序与基线逐字同;`_build_screens` 赋值序/add_child 序不变;42 条 connect 一一对应(仅 :97/:206 两处 closed bind 改 `flow.close.bind`=表 D 申报);smoke_boot get() 4 改 2 留(settings_store/run_scope 留);冒烟六断言+V2 五连比(system_test.gd:108-109)自跑绿 |
| 4 | 通过 | 探针面 diff 归一化对账 88 处非平凡行全部落表 A/C/D+实现决定 2/3/4;probe_gate 日志全绿(.gates/probe.log 06:53,全段);ui_tune_probe 审计自跑 PASS |
| 5 | 通过 | system_test 改址全对账(V1a/V1b/V1c/V2/V3 矩阵 `_face_snapshot` mf→`main.ui.modals.flow`、rp→`main.ui.screens.row_pacer`);V3a–V3i 存活性矩阵自跑全绿(69 项含) |
| 6 | 通过 | 七别名消费方改址逐处对上(pacing_queue 4 处、ui_struct_probe `flow.stack`、t17_defer_probe `flow._deferred`、ui_click_probe `settlement_stop_timer.is_armed()`);自跑 `--only=pacing_queue --only=modal_flow` 双绿;改后断言判据零变化(归一化对账) |
| 7 | 通过 | 六转发方法+_flush_row_queue 全库零残留(grep 只余 1 处陈旧头注,见 §6 缺漏 2);modal_flow 24 checks 绿+系统测试 V3g 段绿 |
| 8 | 通过 | source_guard_test.gd:69-92 重锚——尾缀〔注册面 V1〕;自跑输出「V1d 层序落位单口(presentation 27 文件——apply_layer_contract 之外 move_child 调用点=0)」;扫描面较改前(单文件)只扩不缩;口径例外见 §6 例外 1 |
| 9 | 通过 | theme_projection_test.gd:725-726 改断言新串 `ui.modals.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)`,尾缀〔注册面 V2〕;自跑 164 checks 绿 |
| 10 | 通过 | render_gate 日志全绿(.gates/render.log 06:54,shot_probe 七屏+golden);diff 零像素面改动(改址只动地址词) |
| 11 | 通过 | 审计独立 grep:49 名旧一级地址 `main./root./self.` 代码级残留=0(仅 3 处注释级命中);代码域闸日志六件全绿(§5);留档偏薄见 §6 例外 5 |
## 2. 逐行核对——卡②(§11 V0–V6)
| 行 | 判定 | 凭据 |
|---|---|---|
| V0 | 通过 | 关联套件全绿:审计自跑 8 套件(settle_segments/round_flow_skill_pool/roll_multiplier/treasure_effects/pacing_queue/modal_flow/source_guard/theme_projection)+批末闸 unit.log 分片全绿(15 suites shard 4/4);behavior_roll 随件迁移取号序逐字(§4 核对);五信号留宿主(battle_orchestrator.gd:12-16);两快照出口在宿主(:677/:739) |
| V1 | 通过 | settle_segments_test.gd:28-29 两断言〔编排归约 V1〕:`oc._rounds != null and oc._rounds.oc == oc`+`oc._facts != null`(check_quiet 静默通过=套件 18 checks 零失败);五段段件齐在 _rounds 由 :30-34 五条既有接线断言改指 `oc._rounds._X_seg` 承载(尾缀〔迭代27 T2〕保留,纯改址) |
| V2 | 通过 | round_flow_skill_pool_test.gd:112-115〔编排归约 V2〕:终局后 `presentation_snapshot()["refresh_used"]` == 末轮结算快照 `economy.refresh_used`(经 settlement_snapshot 末次发射承接);自跑实跑输出 5 实例(L1·D0×3+L2·D0×2)全 PASS;reset_round 只清三项(battle_facts.gd:32-35) |
| V3 | 通过 | roll_round_actions 与基线 `_roll_round_actions` 逐行对读一致(仅成员改指;`oc.rng.open_stream("behavior_roll")` 流名与开流时点不变,round_cycle.gd:102);settle_if_due 闸序 settling→battle_result→phase 逐字、终局早退不回置;trigger_cond_test 3 处改指(:119/:126/:131);自跑 round_flow 134 checks+settle_segments+trigger_cond 全绿 |
| V4 | 通过 | roll_multiplier_test.gd:45-59 四断言〔编排归约 V4〕:override 100→spawn hero_unit atk==100;缺省→hero 表值(check_quiet 静默通过);攻击单 base_value 贯通由既有 `_hero_attack_row_with`(:343,hero_atk_val=100 全链路停手结算行)承载——两消费点(spawn+base_value)闭合;combat_segment.gd:56 改调 `oc._hero_atk()`,宿主派生读 :69/:282 |
| V5 | 通过 | treasure_effects_test.gd:311-315〔编排归约 V5〕:`ob14.reward_grant().treasure_inventory == adr14.runner().treasure_inventory` 引用同一性;use_treasure 受理链由 ob8/ob11/ob13 既有段承载(:311 ob8 直写改指后绿);level_runner.gd:520 注入改经 `reward_grant()` 出口(`self.` 前缀=实现决定 3) |
| V6 | 通过 | seal_polarity(_perfect 直写→_facts 8 处对读)/feat_vouchers(契约 2 计数纯净段改指后全绿)/variant_placement/meta_forge/adventure_feats/table_structure_guard——批末闸 unit 分片全绿;三 mark 写口(grant_segment.gd:311/:379/:394 与基线直写同点同增量);attune 三外部消费面零改址(main.gd/treasure_interact_controller/table_structure_guard 均不在改址 diff) |
## 3. 逐行核对——卡③ v1.1(§12 V1–V3)
| 行 | 判定 | 凭据 |
|---|---|---|
| V1 | 通过 | tune_sim_batch_parity_test.gd 五面断言〔测速 V4/V5〕自跑绿(n=3 stats 六键逐键重算+报告逐字含四关键串;n=0 六键零计);run 可等待契约由 wd8 `await ctrl.run(2)`(standee_fx_sweep_test.gd:144,零改动)承载;全文件零循环(grep -c "for \|while " = 0)、代码 await 仅 :69 一处 `await _batch_finished` |
| V2 | 通过 | 五面对拍断言〔测速 V1–V3〕自跑绿:帧序列相对偏移逐位+相邻差恒一(每步恰一帧)、运行态序列逐位+收尾双假、进度串逐位;n=3+n=0 两案各 5 断言全 PASS;manifest 增行(run_all.gd:189);拓扑例外见 §6 缺漏 1 |
| V3 | 通过 | wd8 三断言〔wd8〕自跑 PASS(18 checks:`drv.seen == [true, true]`/收尾双运行态假/报告非空——standee_fx_sweep_test.gd 不在本批 diff=零改动);ui_tune_probe 审计自跑 PASS(⑤ 守卫环 1800 帧预算内退出+输出四串);收尾四语句迁入 `_finish_batch()`(:87-92)与基线 run() 尾次序逐句同(close→报告→置假→_running),`_run_one_level` 零触碰(不在 diff) |
| 静态 | 通过 | gdstyle_gate.py 审计自跑 exit 0:命中 7 全基线内、基线外新违规 0;基线 :15 `tune_sim await-in-loop` 裸键不命中=死键(消失键不参与红判定) |
## 4. 测试削弱检查(对照三卡申报全集)
- **断言计数对账**:22 个改动测试/探针文件 `Kit.check|check(|check_quiet` 计数基线→尖逐一比较——零文件减少;仅 5 增(roll_multiplier 36→40=V4 四断言、round_flow 100→101=V2、settle_segments 16→18=V1 两条、treasure_effects 58→59=V5、tune_sim_batch_parity 0→10=新套件)。与申报新增面(卡② V1/V2/V4/V5 四条+卡① V1d/V6 重锚+卡③对拍五断言)完全一致。
- **删除断言逐条对账**:全 diff 被删 check/断言行 37 条逐条找到配对新行——语义不变只换地址(含 smoke_boot 三 get、system_test V2 五连比、pacing 四断言、V1d/V6 两条);无任何断言删除后无配对。
- **阈值/扫描面**:零阈值放宽;扫描面唯一改动=V1d 单文件→27 文件(申报为强化,成立);static_scan.sh S8/S7 两清单增行 round_cycle.gd(卡② T2 申报;battle_facts.gd 未增行=卡片明示「零协作调用不入清单」,成立)。
- **换下层直调**:零发现——探针仍走真实按钮/输入路径(pressed.emit/push_input 等既有形态未动)。
- **wd8 三断言**:文件不在 diff=零改动,自跑保持绿。**ui_tune_probe 巡检白名单**(实现决定 3):`host == main and m == "_on_modal_closed"` → `host == flow_host and m == "close"`(ui_tune_probe.gd:214-216)——属卡①表 D「closed bind 目标改 flow.close.bind」的机检联动,同一连接新宿主登记;conn_count=11 断言与消息逐字不变(:223-224 与基线 :221-222 对读);判据面「closed 外发消费目标=模态编排口」语义保持。**判定:不属削弱**。
- 卡② §15 零申报、卡③ §16 零申报——兑现成立(超出申报的删改零处)。
## 5. 非行为改动判定与证据链
**卡①**:装配序(八步名序、add_child 序、赋值序)逐字不变;42 条连接语句一一对应;LAYER_CONTRACT 19 项逐字迁驻;move_child 单口由 V1d 27 文件扫描绿+grep 复证(全 presentation 仅 ui_registry :84/:98 两处调用,views/ui_kit.gd:819 为批前既有、不在 diff);main._ready 仅插入 `ui = UiRegistry.new()` 一行于 _build_ui 之前;`_build_modal_flow` 注入语句序与基线逐字同(flow 实例改随 UiModals._init 构造=实现决定 1,注入时点不变)。
**卡②**:成员迁移与改指——roll_round_actions/settle_if_due 逐字迁移对读成立;grant_segment 三 mark=同点同增量;encounter/attunement 词替换的三处字符串键保护(实现决定 5)grep 复证(behavior_roll 前置流名 "encounter" 字面在位);treasure_inventory 注入时序(宿主构造后 :520)不变。
**卡③**:收尾四语句迁移=同帧同相位(对拍 V2 运行态序列+V5 报告逐字承载,自跑绿);`_run_one_level` 零触碰。
**证据链**:
1. 卡③ T0 三重证据在实现报告 §5 在案:215da315 提交信息明载「tune_sim 运行态收尾归还 run」(审计直读提交信息复证)+两面实跑读数;审计自跑两面(wd8 套件+ui_tune_probe)复证现行全绿——前提消化链条完整。
2. 卡③ v1.1 重裁凭据:卡 v1.1 状态行+§15 裁决记录(2026-10-30 停卡重裁三点)在案。
3. 卡①卡②实现决定记录(报告 §3 共 5+5 条)对照卡① §10 七条、卡② §17 五条白名单——白名单外事项(shot/fx_shot 纳入、layout_probe get 键、curio/confirm 漏列补改、flow 随容器构造、grant 第五处读、treasure_effects 五处直构、self 前缀、占位符保护)均有备案,无遗漏。
4. 闸凭据:receipt.json 现为 docs 域回执(9f1dda40 提交时 pre-commit 钩子生成,b27db010 时代,全绿);代码域末跑凭据=.gates 六件日志(smoke 15/0、system_test 69/0、static_scan ok、probe_gate 全绿、render_gate 全绿、unit 分片全绿),时间戳 06:49–06:54 恰落 b27db010(06:49:41 提交)与 9f1dda40(06:57:19 提交)之间——绑定终态代码(9f1dda40 只含报告文件)。
5. 红线:diff 只触 `game/`+`reports/` 两目录;零 `randomize()`/自建 RandomNumberGenerator;零触及 `design/`、`game/tests/README.md`、两份根流程清单。
## 6. 缺漏与例外(如实记录,不修)
**缺漏(记录级,不动判据)**:
1. **卡③实现决定记录缺位**:对拍驱动由卡 T2 的「先启动两形态、后分别 await」并行拓扑改为**串行驱动+相对帧偏移归一**(tune_sim_batch_parity_test.gd:145-162)——卡 §14 白名单六条不覆盖,应在实现报告「实现决定记录」节备案;实际只在提交 04783fae 信息中有案(理由=GDScript 协程动态调用强制 await、无并行启动形态+_init 时点 root 未挂树实测)。报告 §3 无卡③条目。实质判据未损(相对偏移+相邻差恒一仍承载「每步恰一帧+相位同点」,审计自跑绿)。
2. **main_overlay_flow.gd:4 头注陈旧指址**:注释仍写「全部开合走组合根模态单口(root._open_modal/_close_modal)」,两转发方法已退役、实际调用全改 `root.ui.modals.flow.open/close`——「指址须真」词面违例,行为无涉,批内漏改。
3. **卡②改指处数偏差记录偏轻**:grant_segment 实改 18 处与卡片「约 23」的 L2 偏差,实况数在报告 §2 V6 行在案但未入「实现决定记录」节(卡 §14 L2 要求记入该节)。
**例外(依据在案,不判红)**:
1. **V1d 扫描面口径**:实现为 DirAccess 平铺(res://presentation 顶层 27 个 .gd),不含 views/、scenes/ 子目录——与卡片字面 glob(`game/presentation/*.gd`)一致、较改前单文件只扩不缩;但「全 presentation 目录零第二落位点」按递归读法不成立:批前既有 `views/ui_kit.gd:819` 有一处 `body.move_child(...)`(视图内行排序、非层序落位;ui_kit.gd 不在本批 diff、归迭代 42 尾波清偿批在册债)。ui_registry.gd:3 头注「全仓唯一 move_child 点」沿袭旧 main.gd 注释口径、与该既有事实并存。若日后递归化扫描面,须先处置该行。
2. **回执被覆盖**:receipt.json 为 docs 域回执(报告提交钩子生成);代码域末跑以六件日志为凭(§5 第 4 条),与报告 §6「第三次全量域闸全绿、02/05/09 从基线红改善归绿」叙述吻合。
3. **`_finished` 成员写而不读仍保留**(tune_sim_controller.gd:26/:64/:88)——卡 §14 白名单 3 明示「零读者确认后可删」,保留属白名单内两可选择,键账只升 1(11/15 在阈内)。
4. **报告 row4 计数口径差**:「约 460 行机械改写」(报告 §1)与 784d581b「422 行」——统计口径差,不影响判据。
## 7. 审计自跑记录(独立复证清单)
| 项 | 结果 |
|---|---|
| `python tools/run_smoke.py` | PASS——15 项 0 失败 |
| `python tools/run_system_test.py` | PASS——69 项 0 失败(V1a/V1c/V2/V3a–V3i 全过) |
| `--only=source_guard --only=theme_projection` | 绿——V1d「presentation 27 文件…调用点=0」〔注册面 V1〕+V6 新串〔注册面 V2〕+theme 164 checks |
| `--only=settle_segments --only=round_flow_skill_pool --only=roll_multiplier --only=treasure_effects` | 4 套件绿——V1 两断言、V2 五实例、V4 四断言、V5 一断言(check_quiet 静默通过=零失败) |
| `--only=pacing_queue --only=modal_flow` | 双绿 |
| `--only=tune_sim_batch_parity --only=standee_fx_sweep` | 双绿——对拍 10 断言〔测速 V1–V5〕+wd8 三断言 |
| `res://probe/ui_tune_probe.tscn`(headless) | PASS——⑤ 守卫环预算内退出+输出四串+全链路通过 |
| `python tools/gdstyle_gate.py` | exit 0——命中 7 全基线内、零新违规;tune_sim 裸键不命中 |
| grep:tune_sim 循环/await | 循环 0;代码 await 仅 run() :69 一处 |
| grep:49 名旧一级地址残留 | 代码级 0(仅注释级 3 处) |
| 成员数 | main.gd 49→15;battle_orchestrator 37→15;容器 3/9/5/14 |
| 断言计数对账 | 22 文件零减少、5 增(+4/+1/+2/+1/+10) |
| diff 归一化对账 | 88 处非平凡行对全部落表 A/C/D+已记录决定 |
## 8. 总结论
**通过(可合并)。** 三卡对照表 21 行全部核到落点且判据绑定面当轮实跑成立;测试削弱零发现;三卡「非行为改动」判定在代码级对读与实跑两面成立;证据链(卡③ T0 三重证据、v1.1 重裁、卡①卡②决定记录、闸日志)在案。缺漏三项(卡③决定记录缺位、main_overlay_flow.gd:4 陈旧头注、卡②计数记录偏轻)与例外四项均属记录/注释级,不触行为零变化、判据绑定与验收闭合;建议随批末收口由实现侧补记(补记动作归收口段,不由本审计代作)。gdstyle 基线 refresh(--force)仍属用户人工遗留义务(反哺点 7 在案)。
@@ -1,230 +0,0 @@
# 需求卡片——gdstyle 尾波清偿批
> **版本状态**:v2.0 定稿——设计段裁决已闭合(D1–D6 裁决记录见第 6 节,2026-10-29 用户裁),零待裁点,可编排。
> **接收方**:待办清单条目「gdstyle 尾波清偿批——基线余量 11 键定向整改(2026-10-29 登记)」。
> **来源**:gdstyle 尾波清偿分析(2026-10-29 基线重锚后余量清点)。**唯一事实源**:`tools/gdstyle_baseline.txt`(2026-10-29 重锚版,余量 16 键)+ `reports/审计-gdstyle存量债清点与清偿方案-2026-10-26.md`(§三波次方案、§六文档设计段裁定记录——裁定全部继续有效)。
## 1. 目标与完成定义
这批做什么:把 gdstyle 静态质量闸(S13)基线余量 16 键中的 11 键定向清偿——6 键降到阈值内(闸不再记它,下称归绿),5 键按第 6 节裁决维持或降值后走 architecture 豁免登记(批末随实测值落文)。手段只有两类:存量文件内部拆私有函数(呈现层先例)、按职责拆协作件(核心层先例)。
什么状态算完:
1. 11 键逐键达到第 7 节写明的预期结果(归绿 6/豁免 5——豁免键实测值落 architecture 豁免登记段)。
2. 游戏行为零变化:同种子下事件流、步骤流、断点快照、棋盘渲染逐位一致(第 4 节红线)。
3. 批末代码域闸全绿(冒烟+单元+集成+探针+渲染 golden+静态扫描),键账对账完成(第 13 节)。
4. 实现报告与反哺点清单落 `reports/`(末卡产出物落位卡)。
## 2. 设计事实源
本卡属需求性工程条目(结构整改、改游戏代码面),按五段流水线凭工程债证据写卡;四个拆件项的设计段裁决于 2026-10-29 经用户裁闭合(记录见第 6 节)。证据指认:
- `tools/gdstyle_baseline.txt`——2026-10-29 人工重锚后的现行基线,16 键;本卡 11 键逐条列于第 3 节,键名与本卡逐字一致。
- `reports/审计-gdstyle存量债清点与清偿方案-2026-10-26.md` §六裁定记录 7 条(段组织、波 2/波 6 形态、config_db 定性、判定口径、独立审查折入、2026-10-27 三卡归并)——继续有效,本卡沿用其中对呈现层「内部拆私有函数、不拆构建步件」与「全部波不改游戏规则与玩家可见行为」的裁定。
- `design/engineering/architecture/architecture.md` v1.24——轴丙 BoardInstaller(装板域拆件)已随本卡设计闭合落文(模块归属功能层新行+拆件定稿节增补),实现照册施工。
- 并排执行卡:`reports/需求卡片-level_runner商店委托层拆除-2026-10-29.md` v1.1(键 7 拆除执行合同——D6 并排编队,见第 6 节)。
## 3. 现状事实(代码级,开工自行复核;行号为 2026-10-29 实查)
基线 16 键中本批承接 11 键,其余 5 键(`main.gd` 类变量 49、`tune_sim_controller.gd` 循环内 await、`board_cell_unit.gd` 类变量 30、`standee_body.gd` 类变量 19、`battle_orchestrator.gd` 类变量 38)走豁免通道另行讨论,不在本卡。
| # | 基线键 | 现状/阈值 | 涉案位置 |
|---|---|---|---|
| 1 | `board_view.gd\|quality/max-function-length\|_paint_cell = 139` | 139/50 | `game/presentation/views/board_view.gd:601-739` |
| 2 | `board_view.gd\|quality/max-branches\|_paint_cell = 24` | 24/8 | 同上 |
| 3 | `board_view.gd\|quality/max-local-variables\|_paint_cell = 22` | 22/10 | 同上 |
| 4 | `board_view.gd\|quality/allocation-in-loop`(裸键) | 命中 | `board_view.gd:249`——rebuild() 建格循环内 `_CELL_UNIT.new()` |
| 5 | `board_view.gd\|quality/max-class-variables = 32` | 32/15 | `board_view.gd:49-85`(var 成员 31 + `cell_size` 只读属性 1,以闸实测为准) |
| 6 | `board_view.gd\|quality/max-public-methods = 27` | 27/20 | 全文件:11 setter+6 探针出口+4 几何契约出口+3 入站事件+2 生命周期(rebuild/refresh)+1 访问器(board()) |
| 7 | `level_runner.gd\|quality/max-public-methods = 50` | 50/20 | `game/core/level_runner.gd`——其中商店同签名委托 17 个公开方法(751-829 行)+3 个私有转发(`_bind_shop`:728、`_spend_discounted`:772、`_shop_draw_shelf`:783)+`_shop` 成员(:44) |
| 8 | `level_runner.gd\|quality/max-class-variables = 30` | 30/15 | `level_runner.gd` 类成员区(含 `hero_hp`/`gp` 两属性) |
| 9 | `level_runner.gd\|quality/max-returns\|_enter_node = 7` | 7/6 | `level_runner.gd:486-513`——shop 分支 pool/legacy 双 return(:509、:511) |
| 10 | `battle_orchestrator.gd\|quality/max-file-length = 1035` | 1035/1000 | `game/core/battle_orchestrator.gd`——装板与变异布置域六函数合计约 140 行(:414-549) |
| 11 | `adventure_runner.gd\|quality/max-class-variables = 28` | 28/15 | `game/core/adventure_runner.gd:13-45` |
支撑事实:
- board_view 公开方法 27 个全是契约面:setter 是组合根注入面(依赖单向纪律),探针出口(cells_probe/diggable_probe/hover_probe/fog_params_probe/type_glow_probe/cell_rect_probe)是探针断言面,几何出口与入站事件是演出与输入契约。可降空间极小。
- battle_orchestrator 装板域六函数全仓零外部调用,仅本文件三处调用(:271 begin_battle、:801 _exit_distribution、:835 player_request_refresh)——拆件不触任何外部调用面。
- 六函数现行均无超标键(基线无其函数级键);`_install_new_board` 体内 442-451 行为宿主态复位块(五处计数清零+板序推进+board_changed 发射+秘宝叠加层退役)——按第 6 节 D1 裁决留宿主薄壳。
- `_board_generator` 全文件仅四处使用(:42 声明、:233 创建、:239 fatal 接线、:416 装板消费)——装板族独占,随件迁移后宿主成员退役(38→37)。
- level_runner 商店委托面被呈现层 5 文件(camp_shop_presenter/main_adventure_flow/main_overlay_flow/main_ui_build/shop_overlay_view)、`game/sim/level_autoplay.gd`、测试 4 套件(level_shop_economy/pool_case_d/tree_rules_v11/hero_ledger)引用——shop_overlay_view 三处命中均为注释投影引用(:15/:88/:152 实查),代码调用面在 presenter/组合根拆件/sim 侧。
- `tools/run_all_gates.sh` PARITY_TOUCH_FACES 含 `game/core/battle_orchestrator.gd`——本批必触面。
- level_runner 商店委托面的完整行级调用点清单(19 壳含 2 死壳、跨类同名陷阱、呈现/sim/测试逐点行号)以并排卡 v1.1 §3 为准——本卡不重复罗列。
## 4. 设计契约(违反即返工的红线条款)
- 🔴 **行为零变化**:本批全部改动为结构整改,游戏行为零变化——同种子下事件流/步骤流/断点快照/存档往返/棋盘渲染逐位一致;玩家可见面零变化;`design/gdd/systems/` 零修订预期。
- 🔴 **RNG 取号序零漂移**:禁 `randomize()`/自建随机;battle_orchestrator 装板域拆件时 `board_gen`/`variant_place` 两封闭流随件迁移**取号序不变**(流名、开流时点、消费序零漂移——先例句「fx_reveal 封闭流随件迁移取号序不变」同款)。
- 🔴 **PARITY 触面**:battle_orchestrator.gd 触 PARITY_TOUCH_FACES——实跑对拍须经用户批准、不得借机重锚。
- 🔴 **呈现零变化**:board_view 改动的画面判据=`game/tests/golden/` 既有三张棋盘基线(L1 dungeon/L2 underdark/L3 deep_underdark),对拍差异非零即红;重建基线只许用户批准,实现批禁自行换锚。
- 🔴 **拆分边界纪律**(判据权威=code-conventions §1.1,此处立卡内执行条款):①每个拆出单元须有可陈述的职责语句与单一变化原因(实现报告逐件一行承载);②拆出单元须有集体身份——命不出名字、只能说「剩下的成员」的打包=凑计数袋,禁;③「过阈值」只许作为发现信号,作为拆分或收拢的动机即违规;④按纪律拆不出内聚单元 → 停卡呈报走豁免/还债通道,禁硬拆凑绿;⑤拆分沿职责轴不沿阈值轴。
- 🔴 **禁为机检文本保留无效成员**:禁止为迎合机检消息文本(含计数)而保留或新增无效成员/别名——装板域拆件后宿主 `_board_generator` 成员随之退役(38→37);机检键与卡片契约互斥时,实现段一律停卡呈报,不得自造第三条路。
- 🔴 **基线纪律**:禁为过闸 refresh 换绿;还债=数值下降自然绿;出现恶化=停卡呈报(细则第 13 节)。
- 🔴 **attune 族与 begin_battle 序列禁动**:battle_orchestrator 的 attune 族原位保留(迭代 39 副卡语义领地);begin_battle 受理段序与五段驱动逻辑禁动——装板域拆件只改 `_install_new_board()` 调用点的取件改址。
## 5. 范围边界
做什么:第 3 节 11 键中本卡直接执行的三项改动(第 7 节逐键方案、第 12 节任务卡)——board_view 拆分(键 1–4)、_enter_node 载荷合并(键 9)、装板域拆件(键 10);键 7 的拆除执行合同=并排卡「level_runner 商店委托层拆除」v1.1(D6 裁定并排编队,见第 6 节)。
不做什么:
- 商店委托壳拆除的执行——D6 裁并排编队:执行合同=在册定稿卡 v1.1(零待裁点),本卡不重复执行、只承接键账与豁免登记。
- 悬停域拆件与断点域拆件——D2/D4 裁不拆(第 6 节),键 5/6/11 直送豁免终局。
- 5 个豁免讨论键(main.gd/tune_sim_controller/board_cell_unit/standee_body/battle_orchestrator 类变量)——去向:豁免通道批末与本批 5 键合并一次落文,本卡不落文、不落反哺点。
- 波次方案其余文件(审计报告 §三波 1–8)——已由迭代 39/40/41 清偿,不在本卡。
- `game/tests/README.md`——实现批禁写;GP 钱包行真实入口列已随本卡 v2.0 改为 ShopSession 直连(计划入口,闸双通道认卡),批末核对表上名字与实际落地一致。
- 数值表、RNG 流词表、信号载荷 schema——零触碰。
## 6. 设计段裁决记录(2026-10-29 用户裁——D1–D6 全部闭合)
| 编号 | 裁决 | 依据(推导链摘要) |
|---|---|---|
| D1 | ✅ 装板域拆新件 BoardInstaller,两修正 | 六函数构成可命名内聚单元(生成→决议→装板→摇号→布置,单一变化原因=棋盘设置顺序与变异布置算法);三调用点全宿主内实查零外泄;不在 v1.22 保留面;文件长键唯一归绿路径。**修正一**:复位块(442-451 行)留宿主薄壳 `_install_and_reset`,非逐字搬运;**修正二**:`_board_generator` 装板族独占(四处实查)随件迁移,宿主成员退役 38→37。已落 architecture v1.24 轴丙。 |
| D2 | ❌ 悬停域不拆 | 键 5/6 无一可由此归绿(类变量 32→30 仍红、公开方法持平 27);波 6 既定裁=普通视图内部拆私有函数、不立周边件;悬停 11 函数全部低于阈值(无 S13 标的),拆分仅剩计数动机、踩 §1.1-③ 灰区;新件+束注入+入站口委托的代价不成比例。键 5/6 直送豁免;`_r0` 归属认定随之作废。 |
| D3 | ✅ 商店域候选 A(删委托壳),`_shop` 公开化留宿主 | ShopSession 迭代 37 已建(33 公开方法实查),委托面=纯 Middle Man(2026-10-26 真拆分设计会话已裁「纯转传委托=反模式」);拆除方向 2026-10-26 已在册登记,本卡系执行既定发令;公开方法 50→33。`_shop` 公开化留宿主——`_enter_node` shop 分支与断点片段仍需持引用,非无效成员;类变量 30 不降,归豁免。执行合同=并排卡「level_runner 商店委托层拆除」v1.1(零待裁点)——其契约精度高于本卡初案(19 壳含 2 死壳行级清单、`_shop`→公开成员更名形态、两段式中间态、跨类同名判别),按其施工。 |
| D4 | ❌ 断点域不拆 | 键 11 无键可绿(28→22 仍红);现位置不违反任何已立边界——SlotStore 拆分立的界约束的是 save_manager 不吞断点逻辑,非要求断点逻辑离宿主,断点协议住在冒险生命周期 owner 内本身内聚;`_save_manager` 非断点独占(:371 boss 首杀读、:521 abandon 清断点写盘实查),件化须双持引用、束复杂度超收益;27 键冻结契约扰动收益为零。若未来断点协议自身扩展,届时按职责轴立件。键 11 直送豁免。 |
| D5 | ✅ 五个降值键豁免登记终局 | board_view 类变量 32/公开方法 27、level_runner 公开方法 33(删壳后)/类变量 30、adventure_runner 类变量 28——可降途径已按代价裁完,残留面全为契约承载或运行本态;无偿还计划的债=未注记豁免,不如把理由落权威文档。批末按实测值落 architecture「测试与质量域」S13 豁免登记段(先例=battle_orchestrator 第三条形态:键永久基线内、禁再恶化、豁免不等于了结)。 |
| D6 | ✅ 商店域并排编队(写卡期修正:原「吸收」案否决) | v2.0 初稿裁「本卡吸收在册条目、删除其卡」;落笔时发现该域已有定稿执行卡——「level_runner 商店委托层拆除」v1.1(2026-10-29,复核门通过零待裁点),契约精度高于本卡 T6 粗案,销定稿卡重写粗案=无收益损耗。改判:**两卡同迭代并排编队**(编排段并列排入、执行顺序先后皆可——level_runner 交叠为不相邻区段零冲突),本卡承接键账与豁免登记、不重复执行拆除;在册条目与其卡维持,随其自身批完成删除。 |
## 7. 逐键方案(11 键:现状值/阈值/方案方向/预期结果)
预期分两档:**归绿**(拆后实测低于阈值,下批重锚后键消失)与**豁免终局**(维持或降值,批末随实测落 architecture 豁免登记段)。
| 键 | 现状/阈值 | 方案方向 | 预期结果(档位) |
|---|---|---|---|
| 1 board_view _paint_cell 长度 | 139/50 | 拆分层私有函数族(键 1–3 同一改动,见 T1)——五段=基底瓦片→字形与色牌→立面投影→秘宝/变异叠加→迷雾,段间只经 u.st/glyph/digit_col 传值;段内分支仍超 8 时沿绘制层轴继续下钻 | ≤50,**归绿** |
| 2 board_view _paint_cell 分支 | 24/8 | 同上 | ≤8,**归绿** |
| 3 board_view _paint_cell 局部变量 | 22/10 | 同上 | ≤10,**归绿** |
| 4 board_view allocation-in-loop | 命中 | `.new()` 移入逐项构建私有函数 `_build_cell_unit(c, stride)`,循环体只调构建器(先例=battle_end_overlay._seal_img/_ledger_row 族) | 消除,**归绿** |
| 5 board_view 类变量 | 32/15 | 无拆分动作(D2 裁不拆——第 6 节) | 32 维持,**豁免终局** |
| 6 board_view 公开方法 | 27/20 | 无动作(27 个逐类核对全为探针契约面与依赖单向纪律承载面) | 27 维持,**豁免终局** |
| 7 level_runner 公开方法 | 50/20 | 执行合同=并排卡「level_runner 商店委托层拆除」v1.1(19 委托壳删除+`_shop` 公开化;本卡不重复执行——D6) | 约 33,**降值后豁免终局**(豁免登记随本卡批末落文) |
| 8 level_runner 类变量 | 30/15 | 无动作(`_shop` 留宿主——D3 裁,非无效成员) | 30 维持,**豁免终局** |
| 9 level_runner _enter_node 返回数 | 7/6 | shop 分支 if/else 双 return 合并为载荷变量单 return;两个 payload 字典逐字保持(pool 分支 `{"gp": gp, "shelf": true}`、legacy 分支 `{"gp": gp}`),零契约风险。备选(code-conventions §1.1-③ 红线):若实现期发现合并构成纯计数扭曲则留债呈报——本卡按「合并属同一停点语义的两条发射路径归一」认定有内聚收益 | 6,**归绿** |
| 10 battle_orchestrator 文件长 | 1035/1000 | D1 装板域拆件:六函数迁出+宿主薄壳承接复位块+`_board_generator` 成员退役 | 约 910–920(拆后实测),**归绿** |
| 11 adventure_runner 类变量 | 28/15 | 无动作(D4 裁不拆——第 6 节) | 28 维持,**豁免终局** |
## 8. 关联文档同步面
| 文档 | 关联点(本批实现会改变它的哪条陈述) | 同步义务 |
|---|---|---|
| design/engineering/architecture/architecture.md | 「战斗编排域拆件定稿」节轴丙与功能层 BoardInstaller 行——**已随本卡 v2.0 设计闭合落文(v1.24)**,实现照册施工;level_runner 行「外部调用面由同签名委托承载」句(D3 候选 A 实现后失真);「测试与质量域」S13 豁免登记段(D5 五键——批末按实测值落文) | 设计权威文档——实现批禁直改;level_runner 句与豁免登记逐条列进 `reports/iter<N>-反哺点-<日期>.md`,用户裁决后归文档批落文 |
| game/tests/README.md | 「GP 钱包/escrow/商店收口/共享池」行真实入口列——已随本卡 v2.0 改为 ShopSession 直连(计划入口,闸双通道认卡);「棋盘悬停反馈」行已随 v1.0 入表(能力独立成立,不随 D2 裁定变化) | 机械面文档——实现批禁写;批末收尾核对表上名字与实际落地一致(改名则回表) |
| design/gdd/systems/(全树) | 无关联文档——结构整改不改游戏规则与玩家可见行为(审计报告 §六.5 判定口径沿用) | 无修订预期;若实现中发现契约句被触碰,停卡呈报走反哺点通道 |
## 9. 卡前文件结构图
| 文件 | 职责 | 改动性质 |
|---|---|---|
| game/presentation/views/board_view.gd | 棋盘视图壳层(格态渲染入口、入站事件、探针出口) | 修改(非行为改动——_paint_cell 拆分层函数族、rebuild 构建器;验收=对照表行 V1) |
| game/core/level_runner.gd | 关卡域生命周期编排者 | 修改(非行为改动——仅 _enter_node 载荷合并〔载荷逐字保持〕;商店委托壳区段归并排卡,本卡不触——验收=对照表行 V3) |
| game/core/battle_orchestrator.gd | 战斗结算编排中枢 | 修改(非行为改动——装板域六函数迁出、宿主薄壳承接复位块、`_board_generator` 成员退役、三调用点改址;验收=对照表行 V4) |
| game/core/board_installer.gd | 棋盘安装与变异布置域件(新增,architecture v1.24 功能层在册) | 新增(非行为改动;验收=对照表行 V4) |
| game/tests/unit/core/level_shop_economy_test.gd | 关卡图与商店宿主套件 | 修改(仅新增断言 _test_shop_node_entered_payload〔尾波 V1〕;委托面调用点改址归并排卡) |
| game/tests/static_scan.sh | 静态扫描域承载 | 修改(S7 B1 域文件集与 S8 await 文件集增行 board_installer——工程随动,architecture 拆件定稿节 S7/S8 随动条在册) |
新增件「为什么不能复用」理由与唯一落点核查见第 10 节;对不上即卡片不成立。
## 10. 落位与可复用性预检
- **board_installer.gd(新增)**:复用面核查——装板与变异布置逻辑现行只存在于 battle_orchestrator(全仓 grep 六函数名仅本文件三处调用,无第二实现),无既有件可复用;不能并入 BattleFxHost(效果引擎宿主域,变化原因不同)也不能并入 RewardGrant(授予管线域)。落点唯一:B1 域新文件,`game/core/`。分层:core 层 RefCounted,零 UI、零 await,经引用束注入取协作对象,禁裸引用 autoload(S5);BoardGenerator 实例件内自建并接线 fatal 回调。
- **新增函数**:_build_cell_unit、_paint_cell_* 段函数族、宿主薄壳 `_install_and_reset`——全部是既有函数体的逐字搬运或循环体内联体出函,无新增逻辑;迁移函数对账见第 11 节。**零新增数据件、零新增常量表**。
- **调用路径显式声明**:board_installer 只被 battle_orchestrator 三调用点(经宿主薄壳)调用——禁反向持有宿主引用(注入回调单向,先例=BattleFxHost 兑付回调)。
## 11. 实现规范预算(新件对照 gdstyle 默认阈值;写码前对账,超预算面=停卡退回写卡段)
| 新件 | 公开方法 ≤20 | 类变量 ≤15 | 迁移函数对账(基线源函数键) |
|---|---|---|---|
| board_installer.gd | 预算 1(install) | 预算 7(引用束成员 rng/db/minesweeper/reward_economy/encounter 引用/fatal Callable+自建 BoardGenerator) | 六函数现行均无超标键(基线无其函数级键);`_install_new_board` 拆件体与宿主薄壳两段合计行数逐字=计数不变=新文件零新增键 |
| 新增私有函数(board_view/level_runner 内) | —(私有不计数) | 零新增成员(类变量只降不升) | _paint_cell 三键随拆分消除;段函数逐个低于默认阈值(超 8 分支的段沿绘制层轴下钻,见第 7 节键 1) |
存量触面一查:涉案存量文件 T0 读数写明「S13 键较登记值只降不升」预期——board_view 6 键(3 归绿+1 消除+2 维持不升)、level_runner `_enter_node` 键归绿(键 7/8 归并排卡与豁免通道,本卡不触)、battle_orchestrator 文件长归绿(类变量 38→37 只降);adventure_runner 本批零触碰(键 11 维持)。`_init` 一律引用束注入(字典束),防 max-parameters 新键(先例=迭代 40 卡①)。
## 12. 任务卡 T0..T7(估时为区间;工作量限制只属迭代层)
**T0 读数复核与对账(0.25–0.5 工作日)**
- [ ] 以当轮 master 重测 11 键实测值,与基线登记值逐键对账(审计报告 §四.6 纪律:禁按本卡行号直接施工);对不上即停卡呈报
- [ ] 复核涉案文件行数与第 3/7/9/11 节全部 file:line 引用;漂移处就地改卡并记录
**T1 board_view 拆分——4 键归绿(1–1.5 工作日)**(取用:T0 读数;产出:归绿键 1–4)
- [ ] `_paint_cell` 拆五段分层函数族 `_paint_cell_base/_paint_cell_ink/_paint_cell_facade/_paint_cell_overlays/_paint_cell_fog`(骨架见第 7 节键 1;主函数留格态读取与终写),函数体逐字搬运、段内超 8 分支沿绘制层轴下钻
- [ ] rebuild() 建格循环抽 `_build_cell_unit(c: Vector2i, stride: float) -> Node2D`,`.new()` 与属性装配逐字迁入
- [ ] 孤立验证:`godot --headless --script res://tests/run_all.gd -- --only=type_glow,theme_projection,player_copy` 全绿+`tools/probe_gate.sh --click` 全绿(出红只跑红件最小单元)
- [ ] `tools/render_gate.sh` 棋盘 golden 三张对拍差异 0.00%(非零即停机呈报)
**T2 level_runner _enter_node 载荷合并——键 9 归绿(0.25 工作日)**
- [ ] shop 分支 pool/legacy 双 return 合并为载荷变量单 return,两 payload 字典逐字保持(pool `{"gp": gp, "shelf": true}`/legacy `{"gp": gp}`)
- [ ] 新增断言 `level_shop_economy_test.gd:_test_shop_node_entered_payload`(规格见对照表 V3)
- [ ] 孤立验证:`-- --only=level_shop_economy` 全绿
**T3 battle_orchestrator 装板域拆件——键 10 归绿(1–1.5 工作日)**(前置:architecture v1.24 轴丙在册)
- [ ] 建 `game/core/board_installer.gd`(引用束注入,第 10 节落位预检),装板逻辑六函数迁件;`_install_new_board` 拆件体=生成→决议→装板→摇号→布置(415-441 行段),宿主薄壳 `_install_and_reset` 承载复位块(442-451 行段:五处计数清零+板序推进+board_changed 发射+秘宝叠加层退役),时序逐字
- [ ] 宿主三调用点(:271/:801/:835)改调薄壳;删宿主 `_board_generator` 成员(38→37,禁无效成员)
- [ ] static_scan.sh S7 B1 域文件集与 S8 await 文件集增行
- [ ] 孤立验证:`-- --only=variant_placement,round_flow,seal_polarity` 全绿+`python tools/run_smoke.py`
- [ ] 文件长实测 ≤1000(键归绿);类变量实测 37
**T6 批末对账与收口(0.5 工作日)**
- [ ] 全量闸:`bash tools/run_all_gates.sh --code`(改动全为代码面)全绿;闸 11 触面按第 4 节 PARITY 条款由用户裁决
- [ ] 键账对账:11 键终值对照第 7 节预期逐键核(归绿 6/豁免 5;键 7 终值视并排卡落地时序——同批编队即见 33);重锚由用户发令人工 `--refresh`(第 13 节);豁免登记 5 条+既有讨论 5 键按实测值落 architecture(反哺点通道)
- [ ] 产出物落位卡:`reports/iter<N>-实现报告-<日期>.md`(各卡状态+实机目检总清单+实现决定记录+逐拆出单元职责语句一行)与 `reports/iter<N>-反哺点-<日期>.md`(第 8 节同步面逐条落点)写齐方算收口
合计估时约 3–4.25 工作日(切分预案:T3 拆件裁决被证伪时按留债口径 −1~1.5 并回写第 7 节预期;并排卡 v1.1 的 ≈1.5 工作日不占本卡估时,编队总量由编排段合并计)。
## 13. 验证闸——改动—验收对照表
域口径:本批改动全为 `game/` 下非 .md 行为载体——批末跑代码域闸(`tools/run_all_gates.sh --code`);里程碑步(T 段)出口跑一次全套闸,波内修复只做出红最小单元的最窄确认,新增测试先行孤立验证再并入全套。测试执行两入口铁规不破。总判据=每行改动以「既有断言面全绿+golden 零差异+(V4)同种子对拍」证明零行为漂移,无新增玩家可见面;全量闸电池=冒烟 run_smoke.py+系统测试 run_system_test.py+`run_all.gd` 全量+probe_gate --click --full+render_gate+static_scan(含 S13 对账)。
| 行 | 需求点(键) | 改动(文件:行) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 能力行 |
|---|---|---|---|---|---|---|---|
| V1 | 键 1/2/3/4——_paint_cell 拆分层函数族+rebuild 构建器 | board_view.gd:601-739 拆五段、:246-262 抽 _build_cell_unit | 复用 theme_projection_test.gd:_smoke_type_glow、type_glow_test.gd、player_copy_sweep_test.gd:_sweep_board_glyph(既有断言零改动);新增断言零条;render_gate 棋盘 golden 三张 | 单元+探针+golden | BoardView.rebuild()→refresh()→type_glow_probe();board_shot_probe 真实场景帧 | 既有套件断言面+golden 逐像素(差 ≤16/changed ≤2%)——**既有三张基线即改前基线,对拍差异非零即红;重建基线须用户批准** | 既有:未开格类型微光(8 邻作用集/五组色相/零玩法写入);既有:玩家面文案与危险格格面字形 |
| V3 | 键 9——_enter_node 载荷合并 | level_runner.gd:503-511 双 return 并单 return | 新增 level_shop_economy_test.gd:_test_shop_node_entered_payload〔尾波 V1〕:pool 态 node_entered 载荷含 `shelf: true` 且 gp 等于进店时钱包现值;legacy 态载荷无 `shelf` 键。复用该套件商店房段既有断言 | 单元 | LevelRunner.start_level_external→pick_next_node 进 shop 节点 | node_entered 信号载荷(两 payload 字典逐字保持是本改动判据本体) | 既有:GP 钱包/escrow/商店收口/共享池 |
| V4 | 键 10——装板域拆件 | board_installer.gd 新建;battle_orchestrator.gd:414-549 迁出、:442-451 复位块留薄壳、:271/:801/:835 改址 | 复用 variant_placement_test.gd:_smoke_variant_placement/_smoke_global_variant、round_flow_skill_pool_test.gd:_smoke_full_battle、seal_polarity_test.gd 全套;新增断言零条;PARITY 对拍按第 4 节条款 | 单元+集成+MC 对拍(须批准) | BattleOrchestrator.begin_battle()/player_request_refresh() | 同种子变异布置逐位断言面+全战斗步骤流逐行回放断言;board_gen/variant_place 取号序零漂移由同种子断言钉住 | 既有:变异布置门控;既有:轮内流与结算五段全序 |
反向核对:diff 中每个行为改动均有行——本批申报行为改动仅 V3 一处(载荷合并,行为保持);其余行均为非行为改动(结构整改,判据=零漂移证明)。孤儿断言:无(新增断言仅一条:V3 行〔尾波 V1〕,已挂行)。
## 14. 基线纪律(S13)
1. 禁为过闸 refresh 换绿:基线重生成只由用户发令,批末键账对账后人工 `python tools/gdstyle_gate.py --refresh` 一次执行(禁闸内自动重生成、禁实现批中途 refresh)。
2. 还债=数值下降自然绿:拆件后同键数值下降不判红,无需动基线;归绿键在下批重锚后自然消失。
3. 出现恶化(同键数值较登记值上升、基线外新键)=停卡呈报,禁自行消化。
4. 重锚守卫:新键数较旧基线少逾 10% 须 `--force` 显式确认——本批预期归绿 6 键(16→约 10,降幅约 37%;键 7 随并排卡降值 50→33,同批编队时同一次重锚即含),须带 `--force` 由用户执行。
5. 豁免键范围:既有豁免 5 键(main.gd 类变量等——不在本批对账范围;battle_orchestrator 类变量 38→37 为已登记豁免键,只降不升)+本批 D5 裁定 5 键(批末按实测值落文——键 7 实测值视并排卡落地时序;落文前照常按基线对账)。
## 15. MC/数值与 RNG 零触碰声明
本批零数值改动、零数据表改动、零 RNG 流词表改动、零玩法字面量引入——结构整改不改数值与 RNG 消费序。board_gen/variant_place 封闭流随件迁移取号序不变(流名、开流时点、消费序零漂移——同种子逐位一致)。数值与 RNG 零触碰则 MC 对拍不补跑;battle_orchestrator.gd 触 PARITY_TOUCH_FACES——实跑对拍须经用户批准、不得借机重锚(批末闸 11 判 NEED-APPROVAL 时由用户裁决:批准实跑或裁跳过,卡内不定案)。
## 16. 异常分级
- **L1 自救**:迁移函数编译错、探针断言调用面改址漏改——就地修复重跑最窄确认。
- **L2 记录继续**:实测值与第 7 节预期区间偏离但在同档位内(如降值幅度略小)——记入实现报告「实现决定记录」。
- **L3 停机写报告等用户**:①拆分边界纪律④触发——按职责轴拆不出内聚单元(T1 五段/T3 装板域形态被证伪);②机检预算互斥——新件实数超第 11 节预算或迁移函数现出超标键=上游缺陷停卡退回写卡段;③golden 对拍差异非零;④S13 键恶化或基线外新键;⑤PARITY 触面处置分歧;⑥断言面被下游兜底吸收致判据绑不上(按 test-conventions §8.4 出口呈报)。
## 17. 测试削弱申报(允许改动的旧断言及理由)
零申报——本批不删任何断言、不改判据阈值、不缩扫描面、不把真实入口换成下层直调;唯一新增断言=V3 行〔尾波 V1〕(既有断言零改动)。商店委托面的测试改址归并排卡 v1.1 管辖(其自报零申报——只改调用地址,断言条件与期望值零变化)。未申报的削弱由独立验收审计判红。
## 18. 独立验收审计
合并前由未参与实现的会话或子代理执行;报告落 `reports/iter<N>-验收审计-<日期>.md`。审计至少做四件事(test-conventions §8.7):逐行核对第 13 节对照表(测试存在、真执行、尾缀可 grep、真实入口未绕过改动点);抽查判据绑定面;检查测试削弱(对照第 17 节申报);核对 V4 行「非行为改动」判定依据(零漂移证明是否在当轮真实跑批中成立)。未过不得合并。
## 19. 代拍板白名单(允许 agent 自行拍板的微决策)
1. 迁移函数头注注释的指址微调(词表与语义不动——「指址须真」纪律)。
2. 新件私有 helper 命名在职责语义不变前提下的微调。
3. 断言失败消息尾缀编号分配与措辞(〔尾波 Vn〕系列)。
4. 调用面改址的机械改写(import/preload 顺序、空行排版)。
5. 第 7 节键 1 段函数在五段骨架内的下钻粒度(沿绘制层轴)。
超清单决策记入实现报告「实现决定记录」节。
## 20. 裁决记录重申
D1–D6 已全部裁决闭合(2026-10-29 用户裁,第 6 节):装板域拆新件(两修正)/悬停域不拆/商店域候选 A 并排编队(执行合同=并排卡 v1.1)/断点域不拆/五降值键豁免终局——**零待裁点,卡片可编排**。豁免登记 5 条与既有讨论 5 键合并,批末按实测值经反哺点通道一次落文 architecture。
@@ -1,185 +0,0 @@
# 需求卡片——level_runner 商店委托层拆除批
- 版本:v1.1(2026-10-29,grilling 复核门通过——修正四项:跨类同名扫描判类归属、`_shop` 词级归零判据、set_shop_pool 陈旧调用方陈述订正、测试面精确计数)
- 来源:待办清单「level_runner 商店委托层拆除——迭代 37 冻结契约期过渡件」(2026-10-26 登记,来源=批次移交;需求性工程条目,改动面=游戏代码)
- 执行通道:需求改动——经迭代编排编入迭代后在独立 worktree 施工(仪式归 /iter);本卡估时合计 ≈1.5 个工作日(区间 1.5–2,超 2 停批呈报)
- 迭代号:由编排段落位(本卡 `iter<N>` 占位符届时代换)
## 1. 目标与完成定义
拆除 LevelRunner 的商店同签名委托层(19 个纯转传方法+「冻结公开面」契约注释),全部外部调用方直连 ShopSession;游戏行为零改动。
完成定义(全部满足才算完):
1. level_runner.gd 壳块(:722–728 区块注释涉委托句+:751–828 十九个委托)归零;宿主商店成员以公开名 `shop` 承载取件。
2. 产品码、sim、测试面全部调用点改为 `<runner 表达式>.shop.<方法>` 直连,全仓无旧委托调用残留。
3. 全部单元套件、冒烟、代码域闸绿;现有断言读数逐位不变(零新增断言、零断言删除)。
4. 架构文档同步点入反哺点清单;实现报告与验收审计报告落 `reports/`。
## 2. 设计事实源
跳过设计线的证据指认(需求性工程条目,第〇步核验放行 2026-10-29):
- 待办清单条目实况:委托壳=迭代 37 卡④ ShopSession 拆分时立的「冻结公开面」契约过渡件;架构登记=`design/engineering/architecture/architecture.md:109`「外部调用面由 LevelRunner 同签名委托承载(外部零变化)」。
- 拆除形态权威=`architecture.md:152`「调用直连:不留中间人委托壳(纯转传=Middle Man 反模式,2026-10-26 用户裁定)」。
- 2026-10-29 写卡会话裁决:改址定式四条(公开成员取件/零调用死壳删/测试直连私有面/契约注释随壳消失)+ spend_gp 归属确认非问题(共享冒险钱包的校验扣款原语,宿主 ShopSession 语义自洽、不迁移)。
## 3. 现状事实(代码级,开工自行复核)
委托壳块:
1. level_runner.gd:722–724 区块注释(「本类保留同签名委托」句)+:725(「白盒入口——外部调用面与断点取数面零变化」)+:751–828 共 19 个同签名转传方法:shop_products:751/current_price:755/purchased_tier:759/buy_product:763/player_request_refresh:767/_spend_discounted:772/set_shop_pool:778/_shop_draw_shelf:783/pool_stock:787/pool_price:791/shop_shelf:795/shop_refresh_free_left:799/shop_refresh_cost:803/shop_refresh:807/spend_gp:811/buy_pool_item:815/is_shop_pool:819/shop_bp_shelf:823/shop_bp_visit_buys:827。
2. 零外部调用方死壳 2 枚:`_spend_discounted`(全库仅 tree_rules_v11_test.gd:6 注释提及)、`pool_stock`(外部同名调用全为 CurioDeck.pool_stock,如 adventure_feats_test.gd:545——同名不同件)。
外部调用面(产品码与 sim):
3. 呈现层:camp_shop_presenter.gd:173–203(12 处)/main_adventure_flow.gd:81–86(6 处)/main_overlay_flow.gd:135/138/main.gd:773(player_request_refresh)/curio_interact_controller.gd:163(spend_gp——奇物三选一付费刷新出账)。
4. sim:level_autoplay.gd:341/350/363/372/418/421(6 处)+:335 注释。
5. 场景 .tscn 零引用、tests/system_test.gd 零引用(2026-10-29 全仓扫描)。**跨类同名方法不属本批面**(残留扫描判类归属的依据):BattleOrchestrator.player_request_refresh(round_flow_skill_pool_test ×5、seal_polarity_test:185、main.gd:775)与 CurioDeck.pool_stock(adventure_feats_test ×3、feat_vouchers_test ×2、pool_case_d_test ×3、level_shop_economy_test:1360/1361)——17 公开名 grep 命中须按接收方类归属判别,禁误改这两处同名面。
外部调用面(测试):
6. level_shop_economy_test.gd 实改址 25+白盒 `_shop_draw_shelf` :1344/:1355(公开名 grep 命中 27,内含跨类同名 `deck().pool_stock` :1360/:1361 非本批面)/hero_ledger_test.gd:60/66/adventure_feats_test.gd:364/pool_case_d_test.gd:45–47(:47=shop_bp_shelf)/treasure_effects_test.gd:229/233/curio_refresh_paid_test.gd:52–54/meta_progression_forge_test.gd:735–740。
保留面(非委托,仅随成员更名):
7. 宿主内部使用:level_runner.gd:99/102/104/305–306/506–507/704/729(建件、pool 注入、断点取数、进离店编排);钱包生命周期 `close_wallet`:718 归宿主自有(写 gp+发 clear 事件,不经商店)。
8. 商店件本体 shop_session.gd 公开面现成(spend_gp:262 等),测试直连其私有 `_draw_shelf`:133/`_spend_discounted`:59 属测试侧改址。
登记与账面:
9. 架构:architecture.md:109(委托承载句+公开面格)/:152(直连原则);`design/` 全目录仅此一处委托登记(grep 考证 2026-10-29)。
10. gdstyle 基线:tools/gdstyle_baseline.txt:11–13——level_runner 三键 max-class-variables=30/max-public-methods=50/max-returns|_enter_node=7。
11. 能力清单:game/tests/README.md 旅程段四「GP 钱包/escrow/商店收口/共享池」行真实入口列引用委托名——已随本卡写作更新为直连形态(写卡段义务,实现批禁写该文件)。
12. shop_session.gd:4 头注「公开面由 LevelRunner 同签名委托承载」句随拆除失效(商店件唯一注释级 diff,见契约 3)。
## 4. 设计契约(违反即返工;🔴=触碰停机呈报)
1. 🔴 直连不留壳:19 个委托全删(含死壳 2 枚),禁保留任何同签名别名/转发/兼容壳;外部取件唯一形态=公开成员 `LevelRunner.shop`(level_runner.gd:44 私有 `_shop` 更名,公开面=成员本身,不设访问器方法)。
2. 🔴 行为零改动:商店双模式定价、货架、RNG 取号序列、钱包账面、事件发射逐项同前;全部现有断言同种子逐位不变;`data/tables`、RngService、ConfigDB 消费零 diff;无数值改动禁跑 MC。
3. ShopSession 行为零改动:shop_session.gd 不改任何可执行行;唯一允许 diff=头注 :4 委托承载句随批更新(非行为)。
4. 测试直连私有面仅限白盒两入口(shop_session `_draw_shelf`/`_spend_discounted`),依据=「测试/探针直连件读写」(architecture.md:152 裁定段);产品码禁直连任何 `_` 前缀私有成员。
5. 拆分边界纪律(判据权威=code-conventions §1.1,本批为拆除/收敛向):本批零拆出新件——结构变化仅壳删除与成员公开化两项;保留单元(LevelRunner 商店房编排段、ShopSession、呈现控制器)职责语句不变;凡「为达标保留的转发别名」按凑计数袋处理,禁。
6. 🔴 禁为机检文本保留无效成员(code-conventions §1.1 同款):机检键与本卡壳删除契约互斥时(如任何键要求公开方法数不得下降),实现段一律停卡呈报,不得自造第三条路。
7. 测试削弱申报:零申报——不删任何断言、不改判据阈值、不缩扫描面;测试只改调用地址。实现期发现必须改判的断言=上游缺陷,停卡退回写卡段。
## 5. 范围边界
做:壳块与死壳删除;`_shop`→`shop` 公开化(含内部引用点);产品码/sim/测试调用点改址;注释同步六处(shop_session.gd:4、level_runner.gd:722–724 区块句、level_autoplay.gd:335、shop_overlay_view.gd:88/152、tree_rules_v11_test.gd:6);level_runner 头注涉委托表述随批核查同步。
不做(去向):
1. spend_gp 迁 GpWallet 与三处内联扣账去重——2026-10-29 用户裁「可选微整理、不进本批」;日后要做属游戏代码重构,另立卡走流水线。
2. gdstyle 基线重锚(`python tools/gdstyle_gate.py --refresh` 人工专用)——本批公开方法数自然下降不触发重锚;基线死键清账时机归用户发令(待办清单在册)。
3. architecture.md:109 改写——实现批禁写 `design/`,入反哺点清单(§6)。
## 6. 关联文档同步面
| 文档 | 关联点(本批实现会改变它的哪条陈述) | 同步义务 |
|---|---|---|
| design/engineering/architecture/architecture.md | :109 ShopSession 行——「外部调用面由 LevelRunner 同签名委托承载(外部零变化)」句失效;公开面格需随改址后实际对外面重写:补 spend_gp(奇物付费刷新对外出账口)与 current_price/pool_price/purchased_tier/shop_refresh_free_left/is_shop_pool/shop_bp_shelf/shop_bp_visit_buys 等实际被直连方法;set_shop_pool 调用面改述:pool 注入经 LevelRunner 构造 ctx 供给(level_runner.gd:101–102 内部直调 ShopSession.set_shop_pool;原委托块注释「AdventureRunner._start_current_level 唯一调用方」系陈旧表述——全仓 grep 证实产品码零调用方,随壳删除作废),测试面直连 shop.set_shop_pool | 反哺点清单(`reports/iter<N>-反哺点-<日期>.md`),用户裁决后归文档批落文;实现批禁直改 |
其余文档无关联点:GDD 商店/秘宝/天赋树各节机制陈述(扣账口唯一、刷新双源、折价边界、爬价公式)不因调用面搬家而变(grep 考证 2026-10-29)。
## 7. 落位与可复用性预检
- **新增件清点**:零新增文件、零新增函数、零新增数据件。唯一「新增面」=LevelRunner 公开成员 `shop`——更名既有私有成员 `_shop`(level_runner.gd:44),复用既有 ShopSession 实例与 `_bind_shop` 装配(:728),零新装配、零新引用、零新职责。
- **唯一面核查**:grep 19 个方法名全仓——委托职责拆除后调用面唯一落点=ShopSession(第二承载不存在);`_shop_draw_shelf`/`_spend_discounted` 转发壳删除后,白盒入口唯一落点=shop_session 私有方法本体。
- **复用面核查**:不新增任何 helper/访问器——「更名公开」即全部;呈现层持件沿用既有 `_runner_get` 取值回调(camp_shop_presenter.gd:170 现形态),不新引入注入面。
- **分层与交互面**:调用方向不变——views→(信号)→组合根→core;呈现控制器经取值回调持 runner 后以 `runner.shop` 取件(组合根拆件直连=架构「模块归属·呈现层」既有口径);sim 属测量面直构既有惯例;测试直连 core 公开成员+白盒两私有入口(契约 4)。
## 8. 实现规范预算
- **新件三查**:零新增文件——空转。
- **存量触面一查**(level_runner.gd,基线 tools/gdstyle_baseline.txt:11–13):max-public-methods 预期 50→33(删 17 公开委托、增 0;私有委托 2 枚同删不入此数);max-class-variables=30 不变(成员更名不改数);max-returns|_enter_node=7 不变(`_enter_node` 零触碰)。T0 读数步核对「S13 键较登记值只降不升」,基线值本批不动。
- **shop_session.gd**:零可执行行 diff,键面不变。
- **指标例外申报**:无。
## 9. 文件结构图(卡前;全部「复用既有」——零新增件,与 §7 互指)
```
game/core/level_runner.gd 壳块删除+成员公开化+内部引用更名+区块注释【非行为——R2/R3/R4】
game/core/shop_session.gd 仅头注 :4 委托句更新(零可执行行 diff)【非行为——R4】
game/presentation/camp_shop_presenter.gd 调用点改址 12 处【非行为——R1】
game/presentation/main_adventure_flow.gd 调用点改址 6 处【非行为——R1】
game/presentation/main_overlay_flow.gd 调用点改址 2 处【非行为——R1】
game/presentation/main.gd 调用点改址 1 处【非行为——R1】
game/presentation/curio_interact_controller.gd 调用点改址 1 处【非行为——R1】
game/presentation/views/shop_overlay_view.gd 注释同步 2 处【非行为——R4】
game/sim/level_autoplay.gd 调用点改址 6 处+注释 1 处【非行为——R1/R4】
game/tests/unit/core/level_shop_economy_test.gd 改址 25+白盒 2(grep 全量核对)【非行为——R1】
game/tests/unit/core/hero_ledger_test.gd 改址 2 处【非行为——R1】
game/tests/unit/core/adventure_feats_test.gd 改址 1 处【非行为——R1】
game/tests/unit/core/pool_case_d_test.gd 改址 3 处【非行为——R1】
game/tests/unit/core/treasure_effects_test.gd 改址 2 处【非行为——R1】
game/tests/unit/core/curio_refresh_paid_test.gd 改址 3 处【非行为——R1】
game/tests/unit/core/meta_progression_forge_test.gd 改址 3 处【非行为——R1】
game/tests/unit/core/tree_rules_v11_test.gd 头注同步 1 处【非行为——R4】
```
## 10. 任务卡 T0..T2
施工遵循闸机轮次纪律:里程碑步(T1)出口跑一次全套闸;波内修复只做出红最小单元的最窄确认;新增测试/工具先行孤立验证再并入全套。
### T0 调用面直连(壳仍在位——中间态全绿) 估时 0.5–1 个工作日
- 取用:—(首批)。
- 产出:全部调用点 `*.shop.*` 直连形态(壳在位,行为零改动的中间态);T1 依赖「删壳后零编译错」以本产出为完整性基线。
- [ ] level_runner.gd:44 成员更名 `var shop: ShopSession`(行注释同步),内部引用 :99/:102/:104/:305–306/:506–507/:704/:729 全部随更名;核查=grep `_shop`(词级)于 level_runner.gd 仅剩保留件 `_bind_shop`(:65/:104/:728——宿主回调装配面,非委托)
- [ ] 产品码改址六件(形态=`<runner 表达式>.shop.<方法>`,方法名与实参逐字不变):camp_shop_presenter.gd:173–203/main_adventure_flow.gd:81–86/main_overlay_flow.gd:135/138/main.gd:773/curio_interact_controller.gd:163/shop_overlay_view.gd:88/152 仅注释
- [ ] sim 改址:level_autoplay.gd:341/350/363/372/418/421+:335 注释
- [ ] 测试面改址七件:level_shop_economy_test.gd(grep 全量核对;白盒 :1344/:1355 改 `r8.shop._draw_shelf()`)/hero_ledger_test.gd:60/66/adventure_feats_test.gd:364/pool_case_d_test.gd:45–47(:46 改 `lr.shop._draw_shelf()`、:47 改 `lr.shop.shop_bp_shelf()`)/treasure_effects_test.gd:229/233/curio_refresh_paid_test.gd:52–54(`lr.shop.spend_gp(...)`)/meta_progression_forge_test.gd:735–740(`lr.shop.current_price(...)`)
- [ ] 注释同步:tree_rules_v11_test.gd:6 头注 `level_runner._spend_discounted`→`shop_session._spend_discounted`;level_autoplay.gd:335 注释调用名同步
- [ ] 验证:`python tools/run_smoke.py` 绿;单元与集成 `--script res://tests/run_all.gd` 全绿(中间态=直连可编译可过+行为零改动初证);level_runner 三键读数核对只降不升(§8 预期值)
### T1 壳拆除与完整性证明 估时 0.5 个工作日
- 取用:T0 全调用点直连形态在位。
- 产出:终态 level_runner(壳归零);T2 的闸与审计对象。
- [ ] 删除 level_runner.gd:722–724 区块注释涉委托句+:725 契约注释(保留「商店房兑付」节头与件注入说明)+:751–828 十九个委托全量(含死壳 `_spend_discounted`:772/`pool_stock`:787)
- [ ] shop_session.gd:4 头注委托承载句更新(「外部调用面=调用方经宿主公开成员 `shop` 直连」表述)
- [ ] 验证:`python tools/run_smoke.py` 绿+全单元套件绿(壳删后绿=T0 改址无遗漏的编译级证明)
- [ ] 残留扫描:grep 19 个方法名于 `game/`(排除 shop_session.gd 本体定义、level_runner.gd 内部 `shop.` 使用点、跨类同名方法——BattleOrchestrator.player_request_refresh 与 CurioDeck.pool_stock 按现状事实 5 清单判接收方类归属)——产品码与测试面零命中,输出留实现报告
- [ ] 注释收尾核查:level_runner 头注涉委托表述已同步(§5 做项)
- [ ] 里程碑出口:`tools/run_all_gates.sh --code` 全绿、回执落 `game/tests/.gates/receipt.json`
### T2 收口(产出物落位卡) 估时 0.5 个工作日
- 取用:T1 终态(壳归零、套件绿、回执在盘)。
- 产出:三份过程件+合回呈报。
- [ ] 反哺点清单 `reports/iter<N>-反哺点-<日期>.md`:§6 表逐条落点——architecture.md:109 ShopSession 行改写内容全文随单;无其他落点
- [ ] 实现报告 `reports/iter<N>-实现报告-<日期>.md`:各卡状态+实现决定记录+实机目检总清单(本批零玩家可观测改动——写明「无新增目检项」)+遗留读数义务(无)+残留扫描输出留痕
- [ ] 独立验收审计 `reports/iter<N>-验收审计-<日期>.md`:未参与实现的会话或子代理执行(test-conventions §8.7 四事);审计在迭代 loop 内启动的子代理算 loop
- [ ] 审计通过后呈报用户合回(worktree-guard 按 diff 文件性质推域校验回执;审计判红→修复→出红最小单元最窄确认→闸重跑)
- [ ] 批后收尾(待办条目整条删除、roadmap 换代)归 batch-closure——不在本卡任务内,此处仅登记去向
## 11. 验证闸(改动—验收对照表)
本卡零新增断言——对照表全行由既有断言与静态扫描承接,无〔卡短名 Vn〕尾缀需求。
| 需求点 | 改动(文件:行/行为) | 验收测试(名称+落点) | 层级 | 真实入口 | 判据绑定面 | 能力行 |
|---|---|---|---|---|---|---|
| R1 调用面直连改址(非行为改动——调用目标/实参/返回/副作用逐项同前,仅取件路径文本变化) | §9 结构图产品码 6 件+sim 1 件+测试 7 件全部调用点 | 既有套件回归两轮全绿:T0 中间态轮+T1 壳删后轮(level_shop_economy/hero_ledger/adventure_feats/pool_case_d/treasure_effects/curio_refresh_paid/meta_progression_forge 全套+冒烟) | 单元+接口 | 各套件既有真实入口(直构 LevelRunner/AdventureRunner 链) | 既有断言逐位不变(行为零改动正向证据)+壳删后编译通过(改址无遗漏) | 既有:GP 钱包/escrow/商店收口/共享池;既有:奖励三选一候选/刷新付费/呈现 |
| R2 壳块拆除(非行为改动——纯转传方法与契约注释删除,行为承载在 ShopSession 不变) | level_runner.gd:722–724 句+:725+:751–828(含死壳 `_spend_discounted`:772/`pool_stock`:787) | 残留扫描零命中(T1 静态步)+T1 套件绿 | 静态+单元 | grep 全仓(19 方法名清单固定)+套件真实入口 | 扫描面>0 前置(grep 命令对 19 名单非空可执行)+壳删后零引用可编译;命中行按接收方类归属判别(现状事实 5 跨类同名清单) | 既有:GP 钱包/escrow/商店收口/共享池 |
| R3 宿主成员公开化(非行为改动——私有 `_shop` 更名公开 `shop`,装配与读写路径不变) | level_runner.gd:44+内部引用 :99/:102/:104/:305–306/:506–507/:704/:729 | 随 R1 两轮套件绿 | 单元 | 套件真实入口 | 同 R1(内部使用点行为不变) | 既有:GP 钱包/escrow/商店收口/共享池 |
| R4 注释同步六处(非行为改动——注释文本,无运行时可观测面) | shop_session.gd:4/level_runner.gd:722–724/level_autoplay.gd:335/shop_overlay_view.gd:88/152/tree_rules_v11_test.gd:6 | 随批代码域闸绿(静态扫描覆盖文件头纪律面) | 静态 | —(静态判据) | 观测面局限申报:注释无运行时提交面,判据=静态扫描+评审注记 | 既有:GP 钱包/escrow/商店收口/共享池 |
## 12. 异常分级
- **L1 自救**:更名/改址漏点编译错→就地补改后重跑最窄确认(冒烟或单套件)。
- **L2 记录继续**:套件出红且根因属本批改址笔误→出红最小单元最窄确认修复+实现报告记录;连续 3 次同红灯升级 L3。
- **L3 停机呈报**:①出红根因=上游缺陷(非本批改动所致)→停卡退回写卡段;②机检键与本卡契约互斥(如需保留壳/别名才达标)→停机呈报,禁自造第三条路(机检预算互斥条款);③改址中发现任何行为差异(同种子读数漂移)→立即停批(行为零改动契约被打破);④ShopSession 需要可执行行改动才能达成改址→停机呈报(契约 3 被打破)。
## 13. 代拍板白名单
1. 注释措辞与所在行随改址微调(不改语义)。
2. 测试内局部变量随调用链展开的机械适配(不改断言判据)。
3. 残留扫描的 grep 命令形态与输出留痕方式(实现报告正文承载)。
4. level_runner 头注与区块注释涉委托表述的同步措辞。
5. 单元套件运行的分批执行方式(闸机轮次纪律内的最窄确认选择)。
超白名单的微决策记入实现报告「实现决定记录」节。
## 14. 待裁点
零待裁点,开工门=U0。(改址定式与 spend_gp 归属均经 2026-10-29 会话裁决,见 §2。)
@@ -1,336 +0,0 @@
# 需求卡片:战斗一屏样张批(整屏样张判定物首跑 · 四屏样张排程 · 防不满意工序重排)
- 版本:**v1.4**(2026-10-17 开工前预授权复查:本卡**不适用** U1 预授权——两处人闸(T1 方向稿选划/T3 气质终审)是防不满意机制本体,取消即拆机制;四项 L3 经分类**全部属「触既有裁决面/超出本批面」类,维持停机呈报**,详见 §6 分类结论注);**v1.3**(2026-10-17 文档同步闸 v3 口径修订,纯文档批〔分支 carddoc-20261017〕按用户六条裁定执行——①本批(代码实现批,此处含样张工具面)禁写 `design/` 整目录与根规则文件,§8 同步面的同步义务一律执行为「列进 `reports/iter30-反哺点-<完成日>.md`(哪个文件哪一节、写成什么、依据),用户会话裁决后归文档批落文」;冻结记录段全文、目标规格清单、H1–H11 逐项处置、徽章明度档复核转正记录等**成文内容本体随反哺点条目交付**;②产出物落位换代:实现报告/代拍板追认单/反哺点清单三份落 `reports/`,不再 `design/qa/`;③T5 末卡改「产出物落位卡」(三份 reports/ 文件写齐才算收口);④闸口径按域选档:重闸 = `tools/run_all_gates.sh` 按改动域选档(本批触碰 `tools/` 样张面 → `--code`),卡内不再用「全量档全绿」类字样;⑤实机目检项写进 `reports/iter30-实现报告-<完成日>.md` 目检总清单,「入册 07」转反哺点;§7 裁决留痕原文不动,其落文类表述的执行口径随本版换代);**v1.2**(2026-10-16 用户裁决回写:Q1–Q6 六项全部已裁——裁决来源统一措辞「2026-10-16 用户令:三卡全部待裁项按推荐方案裁决(用户将裁权授出,由主线会话代拍板留痕)」,§7 逐条留痕;本版改动:§7 由「待裁点」改为「裁决记录」(选项原文保留+裁定案+一句理由),排期改实(迭代 30 已编入、roadmap 同步在批;三卡串行位第 2=徽章→样张→观感),§0/§1/§2/§3/§6/§7.5/§8 按裁定口径改实,T0 承接 Q5 甲案死指涉处置(指代稿候选 v31/v32/v33-draft 实查拍板),T1 新增 Q3 乙案试验件前闸(半天,不过闸退甲案预设退路),T2/T3 承接徽章卡 Q2 甲案之明度档复核转正义务(§7.5 D1 改实),T4 随 Q2 甲案退役为登记行(06 三屏排队行=本批交付义务,写入文档时点=批次出口),估时随 Q3 乙案重算为 **≈5–6d** 并改软盒拆批线,全卡不留已裁 Q 项的「待裁」字样)。v1.1(2026-10-16 grilling 对抗审查修订;v1.0 同日成文)v1.0→v1.1 改动八项,逐条证据见文末「grilling 审查记录」:①**事实面订正七处**——整屏四项的机检代码并非「从无」(`tools/shot_checker.py` 头注自述承载 §1.4 整屏闸机检,迭代 9 T8 已首跑实测 PASS,git 053baeee;缺的是样张判定物与③焦点预算);死指涉行号 527→**562**;v1 四否决原因补全四条(②封印色冲突、④微光格只留微光此前漏列);playnote 引文改逐字;P5「worktree 不可直接跑」证伪(两级回退+自动建目录);旧锚 §5.1 另存于 06:41(此前只登记看板一处);战斗表现内容清单仍写「四区」(现行三区 B/C/D)。②新增 **P10**(关卡屏样张 v1 已冻结 2026-09-28,系立闸前产物)与 **P11**(§1.4 的执行时机已被屏级落码批越过,本批性质=闸后补跑)。③新增 **§0.5 样张批继承的既有待裁义务清单**(全库检索「待样张批冻结/终审/追认」得十项,本批禁止静默丢弃)。④新增 **§0.6 已裁项核收表**(Q 清单据此收敛,已裁面不复问;同时关闭本卡三处隐性重问)。⑤Q1 补乙案连带(A4 尺寸基线须显式解冻重校)、Q2 补关卡屏既有件、目标句与完成定义去掉「并集单闸」预设(合体方式归 Q6 未裁)。⑥新增 **§7.5 跨卡依赖**(与迭代 30 并行两卡的次序耦合+触碰面重叠禁三线并行)。⑦估时加总订正(T4 上限与合计重算)+补先例出处。⑧验收 V 条逐条改挂实存工具,判不了项降级并注明手段。
- **指路订正(2026-10-20)**:徽章卡(迭代 30 卡①)与测试分层卡(迭代 29)已按 doc-conventions §11「吸收即死」退役,本卡对两卡的三处指路句改为结论内联+提交号(`7ffd1dc0`/`fff4f120`),裁决内容零变更。
- 来源:[06-待办清单](../design/gdd/06-待办清单.md)「战斗一屏样张 P0 未开工——界面成品是否过审的那把尺子还没建(2026-10-13 登记,UI 美化三层诊断 · 第二层)」条(:37-43;注:该条「尺子到现在还没造出来」的措辞与 P1 读数有差——机检代码在位,缺的是样张判定物,T0 订正面连带此句);依赖素材取自同清单相邻条「UI 贴图重构批」(:21-27)「UI 字体补完」(:29-35)「资产升档重产批」(:465-471)「立绘清晰度回退评估」(:473 起)「迭代 26 反哺点④」(:78)「美术资产链路审计两项 P0」(:609-615)
- 归属:**美术线**(06 原条归属栏:战斗屏样张的验收标准归[战斗表现](../design/gdd/systems/战斗循环/战斗表现.md)与[美术圣经](../design/art/art-bible.md)共裁);样张生产脚本与机检面归呈现层工具面(工程)
- 排期:**迭代 30 已编入**(2026-10-16 用户令:三卡全部待裁项按推荐方案裁决(用户将裁权授出,由主线会话代拍板留痕),[roadmap](../roadmap.md) 编入行已在案、批内状态刷新列反哺点);三卡串行序位第 2(徽章→样张→观感,§7.5 D3 改实);开工门 = U0〔用户 `/iter` 发令——Q1–Q6 已全部裁决(见 §7),无遗留待裁项〕;v1.1 之「Q1 必须先裁」前提随其裁决(丙案)关闭,本批依赖链与任务顺序已按丙案口径改实(见 §0/§3)
- **立场前提(用户原话,本卡每环节据此设计)**:「现在做优化就是因为它丑;如果还是用原有方式产出,很可能还是不满意。」⇒ 本卡对现状的改动不是「再跑一次程序化产图」,而是**工序重排 + 选型交用户裁决 + 否决成本前移**;每张任务卡自带「相对现状新增什么」与「防重蹈不满意怎么防」两栏。
---
## 0. 前置依赖与并行性
**串行位**:按 roadmap 执行纪律「一迭代一 loop、单迭代串行」(roadmap.md:93),本批**已编入迭代 30**(2026-10-16 用户令,roadmap 编入行已在案、状态刷新列反哺点),前置=迭代 29(需求卡片-测试分层与门禁语义修正批(迭代 29,已入库合并 fff4f120,卡片已退役))完成后开工;三卡串行序位第 2(徽章→样张→观感,2026-10-16 用户令裁定,§7.5 D3 改实)。**两处限定(v1.1 补)**:①迭代 29 已于 2026-10-16 合入 master(合并提交 fff4f120,roadmap.md:88 状态=✅ 已入库;本卡原「迭代 29 U0 未解」读数随之作废,见 §7.5 D4),该前置已满足;本批开工时点仍取决于用户 `/iter` 发令,禁把「迭代 29 完成后」读成排期承诺;②触碰面重叠除 iter29(`tools/`、06、08、07、roadmap、active.md)外,**还包括另两张迭代 30 卡**(徽章卡动 `presentation/views`+06/08/07+art-bible 修订行;棋盘观感卡动 `tools/recolor_terrain_tiles.py`+golden 基线+art-bible §4.1/§4.3+08 表现行)——三卡各写 06/08/07/art-bible/roadmap/active.md,序位既裁则「禁三线并行」转为逐批串行执行,详见 §7.5。**v1.3 注**:三卡文档直写义务已按 2026-10-17 文档同步闸 v3 全部转为反哺点(落 `reports/`,见 §8),上列汇聚文件重叠随之消解,但 2026-10-16 串行序位裁定不因此失效——序内仍有 golden 基线先后(§4B/§7.5 D2)与 art-bible 冻结内容先后(D1)依赖,禁三线并行维持。
**闸已立、机检面已跑过实机帧、样张判定物从未出**:整屏样张闸条款已于 art-bible v3.0(2026-09-29)写入 §1.4(L64)——四屏各出一张整屏样张、验收四项、闸形「方向稿目检先行」、执行时机 = 屏级视觉落码开工前;§7.1 占位完成条款同步在位(L411,程序化占位 = 临时态,终稿闸 = §1.4 整屏样张过闸,占位态禁止跨里程碑)。**机检面并非空白**:`tools/shot_checker.py` 头注自述「整屏样张闸机检(迭代 9 卡⑤/T8 2026-09-30)」,四项中 ①文字对比 ②明度三档带 ④文字禁压纹理 已有实现(B/C/D 段,另 F 段补「文字在判据」),③整屏焦点预算明文「语义面无法机检 → 归实机目检人闸」(E 段);该断言集 2026-09-30 已对**实机渲染帧**首跑实测 PASS(git 提交 053baeee:「实测 PASS——WCAG 5.79-13.14/三档 204>79>55/骨架四屏过;焦点预算=目检项归段闸人闸」),随闸三裁决亦已裁毕(见 §0.6;呈报单原文件 design/qa/m3-iter9-t8-rulings-2026-09-30.md 已按吸收即死纪律在提交 8e02dcf0 删除,结论以各权威文档现行为准),现挂在 `tools/render_gate.sh` ①段周期跑。**本批要补的是剩下的半截**:出 §1.4 意义上的整屏**样张判定物**(目标图,非现状截图)、走「方向稿目检先行→精制→冻结」的闸形、补 ③焦点预算的判定、按屏扩取样点位、立四屏冻结记录(**v1.2 按已裁范围收窄:Q2 裁甲案,本批只出战斗屏判定物与其冻结记录,余三屏登记 06 排队行归后续批**)。**不新立闸条款。**
**前置事实(2026-10-16 实读)**:
| # | 事实 | 出处 |
|---|---|---|
| P1 | 格子样张 v3 已冻结(2026-09-30,机检 17/17 全绿,四冻结①④重冻生效)。整屏四项的**机检代码在位且已对实机渲染帧跑过**(B 文字对比/C 明度三档带/D 文字禁压纹理+F 文字在判据),**唯 ③整屏焦点预算明文不可机检(E 段归人闸)**;§1.4 意义上的「整屏样张判定物」与四屏冻结记录两件从未产出 | [art-bible](../design/art/art-bible.md) §1.4(L64 闸条款/L68 v3 冻结记录);[shot_checker.py](../tools/shot_checker.py) 头注 L1-25;git 提交 053baeee(iter9-T8 卡⑤ 首跑实测 PASS);[render_gate.sh](../tools/render_gate.sh) ①段 L11-13 |
| P2 | 现行 v3 脚本头注自称「战斗一屏样张方向稿生成脚本 v3-draft」,实际出的是 B/C/D 三区 1280×800 全内容合成图 + S2/S3/S4 附录 + A1–A6 程序化审计——但**冻结记录登记在格子样张闸名下**(圣经 §1.4 v3 段),且它验的是「格+一张合成 mockup」,不是「屏级气质判定物 + 四项逐项断言 + 冻结记录」三件齐 | [render_sample_sheet_v3.py](../tools/render_sample_sheet_v3.py) 头注 L2、L28「一屏样张 = B/C/D 三区全内容(1280×800 1:1 实比)」、L1100-1144 版式与断言清单;art-bible §1.4 L68 |
| P3 | 看板十五件:✅4 / ◐8 / ⬜3(实点计数核过);「战斗一屏样张」行 = ⬜ 未开始,需求文档指针为战斗表现 + art-bible §5.1。**注:§5 已抽离至 [character-design.md](../design/art/character-design.md)(v1.7 批),指针系旧锚;同型旧锚另有 06:41「归属」栏一处**(写作「验收标准归战斗表现与美术圣经 §5.1 共裁」——正确锚应为 §1.4 整屏闸+§7.2 文字硬指标,§5.1 只管剪影轮廓),T0 两处一并订正 | [art-status-checklist.md](../design/art/art-status-checklist.md) L5/L13-14;06:41 |
| P4 | 旧冻结样张产物 `sample-batch-v3-final-2026-09-30.png` 磁盘实测不在盘(`artwork/mockups/` 现存 v0/v2/v31/v32/v33/v34/v35-draft 等同系列图与 level-map-sheet-v1),06 在册 P0①(死指涉条,其原记状态至 2026-10-16 前悬而未决)**已由 Q5 裁甲案(2026-10-16 用户令),处置落 T0**(2026-10-14 审计会话存档 :290/:316 仍记「未读到」)。**订正:06:611 原文写「art-bible 第 68/527 行」,现行为 68 与 562**(527 行实为 §9.1 正面参考表,不含该指涉;562=v3.2 登记行)——T0 订正面对 art-bible:68/:562 与 06:611 的行号自身一并核。**同类死指涉另检出一处(v1.1 新增)**:art-bible §8.2 L490 指路 `design/qa/m3-iter9-t8-rulings-2026-09-30.md`(迭代 9 三裁决呈报单)该文件已在提交 8e02dcf0「存量超期清理批」删除,盘上不存在——裁决结论本身已由 art-bible §8.2/§8.4 与 06:465-471 承载,指路须改指现行文档或改「描述性出处」 | 2026-10-16 目录实查;06:609-615;reports/美术审计-cursor会话存档-2026-10-14.md:290,:316;git 只读查询(053baeee 建件/8e02dcf0 删件) |
| P5 | 素材不入 git(归档纪律 art-bible §8.3 L500),v3 脚本 `asset_root()` 做「本仓库→主检出 D:/code/g-plan2」两级回退,且 OUT_PNG 落笔前 `os.makedirs(..., exist_ok=True)`——**故 worktree 分支内可直接跑(v1.0 的「不可直接跑」判断证伪)**;真实约束是**产物落进该 worktree 的 `artwork/mockups/`,而 `artwork/` 整目录不入库**,冻结件不随合并进 master,须按 §8.3 归档纪律打包(这亦是 P4 丢件的成因)。素材在盘实测:board 瓦片 64 件、character 立绘/小人、`game/assets/ui/icons` 状态图标 18 件、fusion-pixel 字体齐 | 脚本头注 L31 + `asset_root` L50-56 + L62/L1147-1149;art-bible §8.3 L500;2026-10-16 目录实查 |
| P6 | 样张 v1 曾被用户**四原因整体否决**(2026-09-28),四条已全部断言化。**权威映射表在脚本写报告的那段代码里(L1156-1160),不在头注**——头注只逐条标了 ①③ 的编号,②④ 需从映射表补:①**显示太丑(总纲)**→ A6 观感密度+装饰变体 ≥2 种;②**封印色冲突**→ 封印黑 #000000(A1a 色板对表自动覆盖);③**立体感不够**→ A3 图底分离阈值上调+F6 顶面/立面双明度带 Δ≥18+F7 整板悬崖 DEPTH_PX 与游戏渲染常量同源;④**微光格只留微光**→ F5c 微光段零 draw_inlay(另见 art-bible v2.12 角嵌退役批)。注:脚本头注 L23 仍写 A3 Δ≥55、L1157 仍写「整板明度 std ≥50.9」,两处均是**旧口径残留文本**,现行定值以圣经 §1.4 v3 记录为准(A3b Δ≥30、A6 明度梯度均值 ≥4.70) | 脚本 L12-26(F1–F7/A1–A6 头注)+L1156-1160(四否决映射表);art-bible L68(Δ≥30 定值)/L570(v2.13「v1 四否决原因断言化收官」)/L569(v2.12 ④) |
| P7 | 用户气质否决另有两例点名先例:①站桩排版旧格式「盒顶固定 y 与盒内居中在高盒下拉开血条-立绘空洞」2026-10-06 用户点名否决,由 art-bible §8.2「站桩立绘排版 = 方案二」代替(L496);②playnote 立绘批评,**逐字原文**:「现在的立绘太丑了,像素化的粒度太低了,导致根本看不清立绘;看是否可以改回用较为高清的图片?」(playnote.md:9,文件本身无日期;日期系 06「资产升档重产批」条 2026-10-06 注所记「同日 playnote 立绘批评」,裁定日为 2026-10-13 归美术线) | [playnote.md](../playnote.md):9;art-bible §8.2 L496;06:465-471/06「立绘清晰度回退评估」条 |
| P8 | 氛围层两派并存(平滑渐变件 vs Bayer 有序抖动)——**已裁半**:vignette/gradient_base/beams 三件保平滑连续 alpha(2026-10-13 grilling Q5 裁,已入 §7.6(三)+§8.2 LINEAR 豁免清单);**未裁半** = 两派是否统一,明文「留整屏样张批统一裁决」——本批承接的正是这未裁半(**Q3 已裁乙为主**:统一结论在引擎实拍图上呈报、落 T3 裁决记录,见 §7) | art-bible §7.6(三) L465、§8.2 L486;06:78(迭代 26 反哺点④)/06:82(用户裁「全部按呈报写入文档」) |
| P9 | 渲染手段现状:**全仓 `.gdshader` 实测仅一枚**(`game/presentation/views/desaturation.gdshader`,结算去饱和),`ShaderMaterial` 消费面实测仅 `board_view.gd` 一处(:111 声明/:170-172 创建/:194 tween 参数),`.tscn` 内无 shader 节点;样张面全部为 PIL 代码绘制;「Godot shader 能否胜任特效」是用户已提出的未验证疑问(已由 Q3 裁定乙案承接:开工先半天试验件坐实,见 §7;另卡亦载,见 §7.5) | 2026-10-16 全仓检索(`find *.gdshader`+grep `ShaderMaterial`);徽章卡(迭代 30 卡①,已入库合并 7ffd1dc0,卡片已退役)F5/用户立场前提行 |
| P10 | **关卡屏并非零样张**:关卡画面样张 v1 已于 2026-09-28 冻结(机检 11/11 全绿,闸形 A=机检全绿即冻结,气质终审走 M-2 出口闸真人目检八项过),产物 `artwork/mockups/level-map-sheet-v1-2026-09-28.png` **在盘**。它立闸于整屏样张闸条款(v3.0,2026-09-29)之前,**从未按整屏四项判定过**——Q2 若裁含关卡屏,动作是「按四项补判既有样张」而非从零出图 | art-bible v2.14 L565、§7.2 L418;[level-map.md](../design/gdd/ux/level-map.md):3/:222/:244;roadmap.md:21(M-2 出口闸目检通过);2026-10-16 目录实查 |
| P11 | **闸的执行时机已被越过,本批性质=闸后补跑**:§1.4 明文执行时机 =「屏级视觉落码开工前」,而屏级落码已发生四轮(迭代 9 视觉落码段、迭代 16 UI 设计对齐、迭代 25 全局主题、迭代 26 交互与观感),Alpha 亦已于 2026-10-03 到手(M-3 关闭);08「三 · 界面」多行已标在役、五 · 表现仍载「战斗一屏样张=待做」——即 §7.1「占位态禁止跨里程碑」在事实上已被跨。此定性直接影响完成定义第 1/3 条的语义(过闸≠事前定方向,而是对已在役形态出目标图判定物)与 T3 回炉半径 | art-bible §1.4 L64、§7.1 L411;roadmap.md:53/:68/:84-85;08:100-118/:158;美术审计-cursor会话存档-2026-10-14.md:168 |
**与相邻 06 条的依赖关系(Q1 已裁丙案——样张即设计目标图,本节按丙案口径改实,Q1 原文见 §7)**:
- 「UI 贴图重构批」(营地与元进度件出终稿,08 标待做 08:156,06:21-27 状态待办)——判例前置件;**Q1 丙案裁定下不构成本批前置**:样张不待其终稿,反之本卡目标规格清单(尺寸档/色板/明度档逐件成文)反向给其重产/终稿提要求;
- 「资产升档重产批」(06:465-471:**已裁案乙**〔2026-09-30 迭代 9 三裁决,本迭代不升档〕+2026-10-04 用户令暂不做、重启=用户点名+2026-10-06 优先级再降;v1.0 的「档位上调方向候选」已被 v3.7 八敌逐键定档实质完成,剩余挂起项仅「提灯点亮解禁候选」)与「立绘清晰度回退评估」(08「五 · 表现」L146 待裁+08 §7 未决项第 7 行 L210,方向待裁)——素材线两个闸口,**性质不同**:前者=已裁且挂起(本批禁代裁、禁重开),后者=真未裁;**Q1 丙案裁定下两者均不再是样张前置**,本卡反提规格反而是其重启时的输入;
- 「UI 字体补完」(08:157 标待做,06:29-35 归属栏载明须先裁「补不补拉丁专件、要不要上第二个字重」)——**Q4 已裁丙案**:样张按「目标字重效果」示意出图、本批按样张补真件;「补不补拉丁专件、上不上第二字重」仍归该条自身未裁点,本卡不代裁(见 §7)。
### 0.5 样张批继承的既有待裁义务清单(v1.1 新增——全库检索「待样张批冻结/终审/追认/重议」所得)
**为什么要这一节**:这些事项不是本卡新造的议题,而是各权威文档**明文把裁决动作挂名到「样张批」**的既有欠账——本卡若不受理,就会在「本批做完了」的读数里静默丢掉十件在册义务;若受理,就必须逐项写清「本批冻结/本批不冻结并说明去向」。**受理方式已随 §7 已裁 Q 项改实(v1.2)**:Q1 丙案——H1–H4 的尺寸欠账由「目标规格清单随样张冻结产出」承接;Q2 甲案——本批只判战斗屏;Q3 乙案——H10 之氛围统一结论在实拍图上呈报;Q6 乙案——两闸分跑。**本表对 H 项内容本身仍不预设结论——逐项最终处置(冻结/不冻结+去向批次)按契约 7 落 T3 冻结记录。**
| # | 欠账项 | 现行状态与出处 | 与本批的关系 |
|---|---|---|---|
| H1 | 19 枚格面奖励徽章**尺寸**(源 40×40;显示 20/40 双档候选) | art-bible §4.6.2 L309「尺寸候选,样张批冻结」 | 整屏样张要放徽章位即触到该尺寸;未放则显式写「本批不冻结,归贴图批」 |
| H2 | 地形变异纹带族**尺寸**(源 64×64 无缝) | art-bible §4.6.3 L328 同口径 | 同上 |
| H3 | B 区舞台背景 `stage_bg.png` **尺寸**(源 1280×270 基准一张) | art-bible §6.1 L364 同口径;§6.1 L361 地窟看台「规格与终值归整屏样张闸冻结」 | 战斗屏样张的 B 区背景件,正面触到 |
| H4 | 护甲类型(light/ward/heavy)视觉区分方案 | art-bible §4.6.4 L335/§5 存根 L350/[character-design.md](../design/art/character-design.md) §5.3 L27「候选方案,样张批冻结」 | 站桩名行/徽标呈现触到;是否本批裁归用户 |
| H5 | 类型微光**组间互辨抽测**义务(含明度档) | 06:431 裁决④「样张批保留组间互辨抽测义务」;§4.2 L218 组间明度档值从未定义 | **与徽章卡 Q2 直接耦合**(见 §7.5 跨卡 D1) |
| H6 | 面板间距/内边距 4px 步进档**终值** | art-bible §7.3 L426「终值归样张批」 | 整屏构图量取即触到 |
| H7 | 棋盘小人源档与格边长占比摩擦终值(36px 板 × 32px 源超 ≤0.75 红线) | art-bible §8.2 L494、v1.9 待决注记 L581 | 格边长三方取值不一致另条在册(06「棋盘格边长三方取值不一致」),本批禁代裁,只登记 |
| H8 | 镜像与「旗面恒画右」纪律相容口径(代拟待追认) | art-bible §8.4 L508「代拟口径,样张批追认」 | 冻结记录逐条写明已追认/未追认 |
| H9 | 钩爪恐魔「宽≥高」豁免(待样张批终审) | character-design L71 | 站桩样张含该敌种即触到终审位 |
| H10 | 记忆态/明度带随深度微移/提灯点亮形制/滑轨两段可辨 等 provisional 读数 | art-bible §6.5 L392(「样张/实机自检锚」)、§2.7 L129(「候选,整屏样张冻结」)、§7.6(二) L456(滑轨归样张目检)、character-design L45(提灯点亮形制归整屏样张批重议) | 整屏样张即其判定物;本批须逐项在冻结记录写「裁了/留哪一批」 |
| H11 | 「圣经口号型条款机检化批」前置整备(约 55% 条款属口号型无可机检值,分章推进) | 06:513-515「随整屏样张闸前整备批启动」(历史梳理稿 I15 行同义在册,该稿已按吸收即死退役,沿革查 git 提交史) | 跨卡次序已改实(§7.5 D3 已裁三卡串行序位=徽章→样张→观感,本卡位第 2);本整备批是否纳入本批开工门仍属排期题,归用户呈报,本卡不自裁 |
### 0.6 已裁项核收表(v1.1 新增——铁律「禁再问已裁项」的检索留档)
下列事项**已被用户明文裁决**,本卡全部 Q 项一律**禁止重开**;表中同时是本卡相关表述的执法依据。
| 事项 | 现行裁决 | 出处 |
|---|---|---|
| 格子样张 v2 四冻结(砖面/色板/动画/数字形制) | v2 冻结(2026-09-28)→ v3.0 解冻①④ → **v3 重冻生效(2026-09-30,机检 17/17)**;②色板③动画全程未动 | art-bible §1.4 L66/L68、v3.2 L562 |
| 类型微光五组**色相** | **样张 v2 定稿(2026-09-28)**;HUD框架:140 同记「五组色相经样张 v2 冻结」——06:431「候选值,样张批冻结」系滞后措辞,**冻结为真实现状**;本批不重裁色相值 | art-bible §4.2 L218;[HUD框架](../design/gdd/systems/表现与支撑/HUD框架.md)「未开格类型微光」节实装状态行;详见 §7.5 D1 |
| 微光组粒度 | v3.12 批已裁:组间五色相、组内不细分 | art-bible v3.12 L551、§4.2 L218 |
| 未开格类型微光作用圈 | 英雄周围 8 格(非全板常显),2026-09-30 用户裁;不随灯光区扩 | 06:431 口径校正;HUD框架同节;art-bible §6.5 L390 两圈分立注 |
| 站桩形态(卡片式退役) | 2026-09-20 用户裁决全站桩式,对照义务解除 | [战斗表现](../design/gdd/systems/战斗循环/战斗表现.md):77/:83/:93;[battle.md](../design/gdd/ux/battle.md):395/:437 |
| 站桩排版 | 2026-10-06 用户拍板方案二(旧「盒顶固定 y+盒内居中」点名否决退役) | art-bible §8.2 L496 |
| 八敌立绘源档 | 2026-10-06 逐键定档批完成(36–94,显示 ×2);「48/64/96 三档候选」一并完成 | art-bible §8.2 L495、v3.7 L556 |
| 资产升档重产 | 已裁案乙(本迭代不升档)+2026-10-04 令暂不做+2026-10-06 优先级再降;重启=用户点名 | 06:465-471 |
| 1920 档缩放策略/动画帧数规格/资产升档与否(迭代 9「随闸三裁决」) | **三件全部已裁**:1920=案甲全局等比(v3.2);帧数=样张 v2 ③冻结(§8.4 L505-507);升档=案乙 Alpha 后(06 已裁) | art-bible §8.2 L490、v3.2 L562、§8.4 L505;git 提交 053baeee(呈报单原文件已按吸收即死纪律删除,见 P4 附注);本卡禁以「随样张裁」名义重列 |
| 氛围层三件保平滑 | 2026-10-13 grilling Q5 已裁(统一与否另裁,见 P8) | art-bible §7.6(三) L465、§8.2 L486 |
| 封印色 | 封印黑 #000000(2026-09-28 用户拍板;紫→黑因色相冲突) | art-bible §4.1 L196、v2.11 L568 |
| 角嵌/微光格附加图形 | 2026-09-28 用户裁「微光格只留微光」,角嵌彻底退役 | art-bible v2.12 L569、§3.2 L168 |
| 关卡屏视觉终稿 | 样张 v1 机检 11/11 冻结(2026-09-28)+M-2 出口闸真人目检八项过 | art-bible v2.14 L565;level-map.md:3/:222;roadmap.md:21 |
| 按钮五态/全局 Nearest/主题资源 | 迭代 25/26 入库并经用户 2026-10-14 裁「全部按呈报写入文档」 | art-bible §7.6、v3.14 L549;08:117-118/:155 |
---
## 1. 目标与完成定义
**目标(一句话)**:把「判定界面美术算不算过关的那把尺子」补齐并按它跑通战斗屏——**尺子 = art-bible §1.4 整屏闸四项 + [战斗表现](../design/gdd/systems/战斗循环/战斗表现.md)「一屏样张的验收标准」节五项(:79-93)构成的断言集,两套口径的合体方式已由 Q6 裁定乙案——两闸分跑(见 §7);跑通 = 方向稿目检先行 → 精制 → 机检 → 用户终审 → 冻结记录写入文档**。
**相对现状,新增了什么(批次级;v1.1 按 P1 读数重写第 1/4 条)**:
1. 现状:**格子闸执行并冻结过;整屏闸的四项机检代码也已存在并对实机渲染帧跑过**(迭代 9 T8,shot_checker B/C/D+F),但**从未产出「整屏样张判定物」、从未立整屏冻结记录、③焦点预算从未判定**——本批新增=样张判定物本体+整屏冻结记录首条(战斗屏;四屏全套随 Q2 甲案余三屏登记 06 排队行归后续批)+③焦点项的目检判定+取样点位按屏扩面(T2);
2. 现状产图工序 = 脚本一次合成单路线成品直接呈报——本批新增**方向稿先行**硬工序(本批战斗屏先出 2–3 张低成本方向稿供用户选划,方向判完才准精制,T1;后续各屏各自立此闸);
3. 现状渲染手段默认锁死「PIL 程序化代码绘制」——本批把代码绘制 / shader / 新素材三路线的选型呈用户裁决(Q3),**已裁乙为主、预设甲案退路(2026-10-16 用户令)**:Godot 引擎实拍+shader 为目标路线,开工先半天试验件坐实,不过闸即退甲案纯代码管线(退路预裁定,见 §7);
4. 现状两套标准并存未合体(§1.4 整屏四项 / 战斗表现专项五项)——本批合成一张逐项比对表(T2),合体方式**已由 Q6 裁定乙案:两闸分跑**(专项第 5 项只产裁决记录)。
**防产出重蹈不满意(批次级,三道防线)**:
- **防线一(工序)**:否决成本前移——方向稿阶段否决只损失半天,精制阶段否决按战斗表现判定回退条款只回炉不过项(继承既有口径「已过项结果保留」,战斗表现:93);禁止跳过方向稿直接出精制版(V1:无目检记录即开工精制 = 违红线)。
- **防线二(判据)**:P6 四条否决原因(①显示太丑/②封印色冲突/③立体感不够/④微光格只留微光)+P7 两例(站桩排版空洞、立绘像素粒度)逐条映射为整屏断言集内的具名项;断言不过不出图(「生成即验,不过闸不出图」纪律沿 v3 脚本原文 L12)。
- **防线三(终审)**:气质终审权恒归用户目检,机检全绿不等于过闸。先例:v3 冻结时气质终审走的是「验收豁免」(art-bible L68,2026-09-29 用户验收豁免,状态记录 281),本批**不申请豁免**——此为**本批自我从严的执行纪律,不是对用户裁决的改动**;用户若主动以豁免方式判定本批完成,从其裁决。
**完成定义**(全部满足才算本批完成):
1. 战斗屏样张过闸:方向稿用户选划记录在案 + **按 Q6 已裁乙案两闸分跑、两闸全绿**(含 ③焦点预算的目检判定记录;专项第 5 项只产裁决记录不产过/不过) + 用户终审「过」记录;
2. 冻结记录**成文**(内容 = art-bible §1.4 新增整屏样张冻结记录段全文——设计权威文档零直改,**仅过闸后成文**):同段逐条写明「本样张冻结了什么、没冻结什么」,并对 §0.5 的 H1–H11 **逐项给出处置**(本批冻结/本批不冻结并写明去向批次),禁留空;**同段另须产出两件交付物:① 目标规格清单(Q1 已裁丙案:尺寸档/色板/明度档逐件成文,反向给素材重产批提要求,承接 H1–H4 尺寸欠账);② 徽章卡 Q2 甲案 provisional 明度档之复核转正记录(H5 组间明度档复核+组间互辨抽测,复核转正义务按 §7.5 D1 落本卡冻结件,为本卡交付义务)**——全文随 `reports/iter30-反哺点-<完成日>.md` 对应条目交付(「改成什么」栏即全文),「写入 art-bible §1.4」列反哺点,落文归文档批;
3. 状态回写义务列反哺点(判据语义不变;**条款在 art-bible §7.1 L411**;08 侧的行是「五 · 表现」表「战斗一屏样张」行 L158,08 无 §7.1 小节——v1.0 此处指路写错已订正):
- 反哺点条目:08「五 · 表现」表「战斗一屏样张(界面美术的终稿闸)」行:待做 → 在役;
- 反哺点条目:[art-status-checklist](../design/art/art-status-checklist.md)「战斗一屏样张」行:⬜ → ✅;
- **边界条款**(写进反哺点条目本身):样张过闸只是**判据生效**,不是各屏占位素材的转正条件——看板其余 ◐ 行(营地件、格面徽章、演出类等)仍须各自素材交付后才可改 ✅,禁止把「样张过闸」扩解为「全部占位转在役」;样张同时按**已裁 Q1 丙案**冻结目标规格清单、反向给素材重产批提规格要求(尺寸档/色板/明度档逐件成文),转正仍逐件走闸;
- **P11 定性连带**(写进反哺点条目本身):本批属闸后补跑,回写禁写成「落码前定方向」口径,须如实注记「屏级落码已先行四轮,本样张为对已在役形态的目标图判定与冻结」;
4. **Q2 已裁甲案**:本批仅出战斗屏;关卡/菜单/营地三屏的 **06 排队行登记内容属本批交付义务**(排队行措辞含 P10 注记与营地屏「待目标规格清单反提要求」注记),随反哺点清单交付、落文归文档批;关卡屏既有 v1 冻结样张保留为注记(**其后续批是「对既有 v1 冻结样张补判整屏四项」,不是从零出图**,P10);
5. §8 同步面清单每条在反哺点清单有落点;权威文档与 06/08/看板零直改(v3 口径)。
---
## 2. 范围边界
**做**:战斗屏样张全流程(方向稿→精制→双口径机检→终审→冻结);四屏排程落地按 **Q2 已裁甲案**(本批仅战斗屏+余三屏 06 排队行登记内容,随反哺点清单交付);整屏断言集的**承载与扩面**——**优先复用 `tools/shot_checker.py`(已有 B/C/D/F)+ `game/smoke/shot_probe.gd` 的 shot_meta.json 坐标源**,缺口(③焦点判定、逐点位覆盖、色数上限计数)才新建,禁另起一套并行判据;§0.5 H1–H11 继承义务的**逐项受理与处置登记**(冻结或写明去向,不自裁内容);氛围层两派统一裁决的**裁决与登记**(**Q3 已裁乙为主**:统一结论在引擎实拍图上呈报、落 T3 裁决记录,写入文档列反哺点,落码归后续呈现批);P3 看板与 06:41 两处旧锚订正、P4 死指涉处置(**Q5 已裁甲案**:改指在盘最近稿 v31/v32/v33-draft 之一并注「指代稿」,具体指哪份 T0 实查盘面后定)+ 06:611 行号读数自身订正、战斗表现内容清单「四区」旧措辞订正——以上文档订正一律以反哺点条目交付(写清文件/节/改成什么/依据),零直改。
**不做**(明示去向):
- **不重产任何正式素材**——终稿素材出图归 UI 贴图重构批 / 资产升档批各自立项;
- **不改游戏运行时渲染代码**——样张是判定物不是实现物;**Q3 已裁乙案(shader/实拍路线)**,样张侧 shader 试验件与实拍管线只进样张管线,游戏侧落码另批;
- **不执行格子闸重跑**——格底明度档、数字承载面已随 v3 冻结(art-bible §1.4 L68),整屏样张直接在其上合成;
- **不重裁 §0.6 表内任何已裁项**——尤其微光五组色相(v2 已定稿)、1920 档策略(案甲)、动画帧数(v2 ③冻结)、资产升档(案乙+挂起)、站桩形态与排版(2026-09-20/2026-10-06 两裁);
- **不代裁 08 未决项**——「站桩帧动画进不进 Alpha」(08 §7 第 5 行 L208)、「素材升档重产进不进 Alpha」(第 6 行 L209)、「立绘清晰度回退评估」(第 7 行 L210)三行归各自条,本卡只登记连带,禁以「样张过闸时顺带」名义吃掉(v1.0 把站桩帧动画写成「Beta/后批事项」属定性失实,已订正:它是 08 在册待裁,不是已排 Beta);
- **不动玩法规则与数值**——纯呈现层判定批,MC 零跑(零数值零行为);
- 键盘输入/手柄(08:191 已明列「六 · Alpha 不含」,排 Beta)不涉。
---
## 3. 任务分解
> 估时按 Claude 工作日口径;每卡自带两栏:**相对现状新增什么 / 怎么防重蹈不满意**。**Q1–Q6 已全部裁决(2026-10-16 用户令,见 §7),开工门 U0(用户 `/iter` 发令)后即可按任务序开工;T0 事实坐实段零路线依赖,可先行**。
### T0 事实坐实与旧账处置(≈0.5d)
- 实跑现行 [render_sample_sheet_v3.py](../tools/render_sample_sheet_v3.py) 与 `tools/render_gate.sh`(worktree 内可直接跑,见 P5 两级回退+自动建目录;跑前须防**覆盖在盘产物与机检报告**——OUT_PNG/OUT_REPORT 是硬编码旧文件名 L62-63,开工日一律改出到临时区或加日期后缀,禁写回 `sample-batch-v3-final-2026-09-30.png` 与 `sample-v3-final-机检报告-2026-09-30.md`),确认管线今天仍可出图、A1–A6+F1–F7 全绿基线读数冻结进开工记录(A4 尺寸基线硬等在盘实测成立:立绘 46×48、棋盘小人 32×32);
- P4 死指涉按 **Q5 已裁甲案**处置:T0 实查盘面**定稿具体指哪份代稿**(v31/v32/v33-draft 择一,该实查拍板为本批动作);其落文动作——art-bible:68/:562 与看板:13 三处死指涉改指代稿并注「指代稿」、同族死指涉(§8.2 L490 呈报单死路径)按同案口径一并处置、P3 两处旧锚(art-status-checklist:14 与 06:41 的 §5.1 → 正确锚 art-bible §1.4/§7.2 或 character-design)订正、06:611 行号读数(68/527 → 68/562)订正、战斗表现:83/:116「四区」→「三区 B/C/D」措辞订正(事实源 battle.md:126/:184)——逐条列进 `reports/iter30-反哺点-<完成日>.md`(v3 口径零直改,归文档批落文);新冻结记录按 art-bible §8.3 描述性出处纪律成文;
- §0.5 H1–H11 继承义务清单**逐条复核是否仍然在册**(漂移即订正本表);Q 项已全部裁决(§7),不再随 Q 呈报,清单逐项处置落 T3 冻结记录(契约 7);
- 复核本卡 §0 全部行号与事实,漂移即订正卡片。
- **新增**:开工日可跑性证据+现有机检面读数基线(现状:shot_checker 周期在跑,样张脚本自 2026-09-30 后无跑动记录)。**防不满意**:先验证旧管线还活着、先量出现有机检代码到底判到哪一项,再决定它在新工序里的角色——避免「新工序设计了,旧工具已哑火」或「重复造一套已存在的判据」两种空转。
### T1 方向稿(战斗屏 2–3 张,目检先行)(≈1.5–2d 含 Q3 乙案试验件前闸 0.5d)
- **Q3 乙案前闸(开工第一个动作,半天)**:Godot 引擎实拍+shader 试验件坐实发光类特效能力边界(光带/泛光/呼吸感)——过闸即走实拍目标路线;**试验件不过闸即按 §7 Q3 预设退路退甲案纯代码管线重出(退路预裁定已写入,不回炉问用户,仅留痕)**;
- 战斗屏方向稿 2–3 张:内容骨架按 [battle.md](../design/gdd/ux/battle.md) 现行 B/C/D 三区布局(:126/:184;**战斗表现:83/:116 仍写「四区」系旧措辞,T0 订正**)与 [战斗表现](../design/gdd/systems/战斗循环/战斗表现.md) 一屏样张内容清单(:83——站桩单版〔卡片式已 2026-09-20 裁退,§0.6〕、三语境剪影同图、受击/退场/高光三组动画关键帧条、敌层遮挡对照组),**气质方向分岔**(Q3 已裁乙为主:A 引擎实拍目标形态〔主线〕/ B 实拍+氛围层统一后形态(两派差异在实拍图上直接可见,统一结论呈报落 T3)/ C 代码绘制甲案基线对照张〔试验件不过闸时 C 升主线〕);
- 方向稿张数与气质分岔在 2–3 内自定(2026-10-16 用户令已把三卡各 Q 项全数裁定,方向稿层无待裁项遗留);
- 方向稿允许低成本形态(构图级即可,细节参数不必全冻结),每张附「本方向解决了哪条否决先例」标注(P6/P7 逐条);
- **用户选划 = 人闸**:判完方向才准进 T2;选划记录(哪张、为什么、否决了哪张)登记开工单。
- **新增**:现状无此工序(v1–v3 均为单路线成品一次呈报)。**防不满意**:把「不满意」拦在最便宜的阶段;多路线并呈使用户从「验收者」变「选择者」,气质分歧在方向稿层解决而不是精制品层返工。
### T2 战斗屏精制版 + 双口径断言集(≈2–2.5d,含实拍静态定格工序新建:shot_probe 现出「现状图」,目标构图定格工序为本批新建——§7 Q3 乙案真实成本)
- 按选划方向精制战斗屏 1280×800 整屏样张(1:1 实比);
- 断言集扩展(**先复用后新建**:`tools/shot_checker.py` 的 B/C/D/F 段已承载整屏 ①②④,`game/smoke/shot_probe.gd` 已产 shot_meta.json 坐标源;本批缺口=①的逐点位覆盖(现行 B 段只判 meta 登记的已知文字框三处,禁把「三处」报成「全部文字」)、②的非战斗屏取样(现行 C 段只算战斗屏 C 区/棋盘/舞台三点——**Q2 已裁甲案,本批不动非战斗屏,取样面仅扩战斗屏逐点位**)、③焦点预算(明文归人闸,**禁伪装成机检**)、色板 48 色上限计数(现行 A1a 是「对表圣经冻结色值+禁入带零命中」,**不是 48 色计数**;全仓无 48 色工具,`tools/` 实查零命中,须本批新建,且与 art-bible §8.3 计划的入包色板检查器分工不同——本批只做全图色数计数,不建入包闸);落点扩旧脚本 vs 建新脚本按工程就近原则批内自定):
- **整屏四项**(art-bible §1.4 L64 闸条款,与 §7.2 L416 同源):①全部文字(含菜单/说明/tooltip/按钮)对直接背景 WCAG ≥4.5:1,大字〔≥18px 或加粗档〕底线 3:1、目标 4.5:1;②明度三档带成立(信息面层 > 世界中层 > 世界底层,层间灰度可分);③整屏焦点预算(§3.4 首焦唯一,无第二焦点竞争);④文字禁直接压纹理底(须垫容器或罩层);
- **A1–A6 口径上屏**(脚本 L21-26+L925-1031 现行义,**逐项按实义引,禁改名义**):A1 色板对表冻结色值+禁入带 300°–345° 全图扫描、**A2 拼缝周界闭合(墙↔地墨线带亮度)**、A3/A3b 图底分离 Δ≥30(L23 头注的 Δ≥55 与 L1157 的「std ≥50.9」是旧文本,现行以圣经 §1.4 L68 定值为准)、A4 混排相融(语义色 == ui_kit 常量+在库原件可载+**尺寸基线硬等 立绘 46×48/小人源 32×32**)、A5 画布四缘净空、A6 观感密度梯度均值 ≥4.70;
- **战斗屏专项五项**([战斗表现](../design/gdd/systems/战斗循环/战斗表现.md):85-93:棋盘区零打扰/剪影三语境灰度可辨/三组动画关键帧可读/双舞台战斗空间/敌层插入位置对照组可裁)——与整屏四项的**合体方式已裁乙案:两闸分跑**(专项闸与整屏闸各跑各退,:93 已成文);battle.md:152 另载一条挂在样张上的验收项「徽章不进数字区」,一并入表;
- **H5 复核项(v1.2 新增,承接 §7.5 D1 已裁义务)**:徽章卡 Q2 甲案 provisional 明度档的复核转正+组间互辨抽测(06:431 裁决④保留给样张批的义务),结论随 T3 终审一并呈报、写入冻结记录;
- 生成即验,不过闸不出图(沿用 v3 脚本纪律原文)。
- **新增**:整屏样张判定物本体+③焦点项的判定+逐点位取样覆盖(跨屏取样随 Q2 甲案归后续批)(v1.0 所称「该四项只存在于条款文字、无对应代码」经 v1.1 实查证伪,见 P1)。**防不满意**:机检只拦「可量化的丑」(对比度、抢焦、色板越限),拦不住的部分强制留给 T3 目检——机检绿不豁免终审。
### T3 气质终审(用户目检,闸形 A)(≈0.5d 含往返)
- 精制版 + 断言报告一并呈用户终审;「过」→ 冻结;「不过」→ 按战斗表现判定回退条款分面回炉(演出/构图面回 T1 或 T2 对应项,已过项保留);
- 过闸同时产出三笔裁决记录:①敌人层绘制优先级插入位置(战斗表现:75 登记原句「随美术测试一并裁决,不在本文提前定」保留,清偿时点经 Q6 裁定随本样张一并——T3 即其清偿点);②氛围层两派统一结论(Q3 已裁乙为主:在实拍图上呈报;若方向稿层未完成,此层完成即为最迟期限);③徽章明度档 provisional 复核转正结论(H5,承接 §7.5 D1 已裁义务)。**三笔记录本体写进 `reports/`(实现报告+追认单),其文档落位(战斗表现:75 登记、art-bible §7.6(三) 改写、冻结记录段)列反哺点**。
- **新增**:明确不申请验收豁免(v3 先例有豁免,本批关闭该通道)。**防不满意**:终审语义 = 用户说不过就是不过,无「机检全绿应视为过」的反向论证空间。
### T4 后续三屏排程(Q2 已裁甲案:本卡退役为登记行)(≈0d,06 排队行登记动作并入 T5)
- **执行 Q2 甲案裁定案**:本批仅出战斗屏;关卡/菜单/营地三屏的 **06 排队行登记内容属本批交付义务**(动作列入同步面 T5/§8,随反哺点清单交付、落文归文档批);
- 关卡屏既有件保留为注记(P10):v1 冻结样张已在盘(2026-09-28,机检 11/11),排队行措辞须注明「其后续批=对既有 v1 冻结样张按整屏四项补判,非从零出图」;
- 营地屏连带改实:Q1 已裁丙案(非乙案),「等贴图批终稿」串行分支不发生;营地排队行注明「待本卡目标规格清单反提要求,终稿后再出样」;
- **跨卡次序改实(§7.5 D2/D3)**:三卡串行序位已随 2026-10-16 用户令裁定=徽章→样张→观感,本卡位第 2——观感卡排本卡之后,其 L2/L3 落地对本卡冻结读数的复核归该卡义务,本批不为其预留参数域、不被其阻塞。
- **新增**:现状四屏闸只立了条款、机检面跑过实机帧,但四屏**样张**零张(唯关卡屏有立闸前的 v1 一张)。**防不满意**:逐屏目检逐屏冻结,禁止「一套断言四处套用」把首屏过闸经验未经验证直接外推(本批只判战斗屏,余屏按排队行各自立闸)。
### T5 产出物落位卡(≈0.5d,末卡——三份 reports/ 文件写齐才算收口)
轻闸全绿后、合并前,把本批三份 `reports/` 产出物写齐(权威文档与 06/08/看板零直改;`active.md` 状态记录照常更新):
1. `reports/iter30-实现报告-<完成日>.md`——各卡状态+**实机目检总清单**(T3 终审轮次记录、V1 方向稿三段记录表〔方向稿→选划→精制〕随附)+遗留读数义务(本批冻结基线读数供观感卡对表,§7.5 D2)+变异验证记录表(新建断言段的注入实验记录);
2. `reports/iter30-代拍板追认单-<完成日>.md`——冻结记录段系列编号口径等白名单内代拍项逐条留痕;
3. `reports/iter30-反哺点-<完成日>.md`——§8 同步面每条一落点(哪个文件哪一节、写成什么、依据),其中:
- **art-bible §1.4 冻结记录段全文**(新段,沿 v2/v3 记录格式;**段内系列编号口径属批内代拍**——格子样张序列已用到 v3,整屏样张是否续称 v4 还是自成一列〔关卡屏已用「样张 v1」名〕在冻结记录里一次说清,禁两处各按一种叫法);§0.5 H1–H11 逐项处置写入该段;**随段两件交付物(v1.2 新增):目标规格清单(Q1 丙案,尺寸档/色板/明度档逐件成文)+徽章明度档复核转正记录(H5/§7.5 D1)**——三件全文即反哺点条目的「写成什么」栏;
- art-bible §7.6(三) 氛围层并存条改写为已裁决口径(经 T3 记录);
- [art-status-checklist](../design/art/art-status-checklist.md) 战斗屏行 ✅+余三屏行刷新为「入 06 排队」态;08「五 · 表现」行待做→在役;06 对应条注销(去向登记当批状态记录);**06 为关卡/菜单/营地三屏新立排队行(Q2 甲案交付义务;措辞含 P10 注记「后续批=对既有 v1 冻结样张补判整屏四项,非从零出图」与营地屏「待目标规格清单反提要求」注记)**;
- roadmap 迭代 30 行状态刷新(编入笔已在案,只刷状态);完成定义第 3 条「转正边界条款」与 P11 定性连带注记随 08 行条目交付(防「样张过闸」被误读为占位素材全体转正)。
- **防不满意**:冻结记录段全文逐条写明「本样张冻结了什么、没冻结什么」,后续批对表施工,杜绝各自解释。
**估时合计 ≈5–6d(v1.2 按已裁口径重算)**:Q2 甲案 → T4 退役为登记行(0d);Q3 乙案 → 新增试验件 0.5d(T1 前闸)+实拍静态定格工序新建 ≈0.5d(T2 增量)。T0 0.5 + T1 1.5–2(含试验件 0.5)+ T2 2–2.5 + T3 0.5 + T4 0 + T5 0.5 = **下限 5d、上限 6d**;**若试验件不过闸退预设甲案退路,回退 ≈4.5–5.5d**(扣实拍新建增量,已耗 0.5d 试验件保留)。**软盒拆批线改实**:v1.1 的「四屏全推超 7d 拆两批」分支随 Q2 甲案不再发生——本批 5–6d 在 7d 软盒内单批跑完;若实拍路线超支逼近 7d,仍在 T3 末截断(战斗屏已过闸可单独交付见效),余三屏已列 06 排队行无批内拆分需求。
**估时构成依据(先例出处,v1.1 补、v1.2 沿用)**:整屏四项机检的建造+三裁决呈报=迭代 9 T8 实测 3–4h 量级(.zcode/plans/m3-iter9-loop-plan-2026-09-30.md:65)——故 T2 走「复用+扩面」而非重建,增量只花在实拍定格工序新建;同窗格子样张机检冻结=迭代 9 批 A T1 估 2–3h(同计划书:58);方向稿多轮呈报的先例工时见迭代 26(估 2–2.5d 实际单会话)、迭代 16(估 6.9d 实际单会话)、迭代 13(估 4.8–5.3d)、迭代 29(估 5.5–7d);本批估时主要吃在「方向稿+用户往返目检+实拍管线新建」,roadmap 迭代粒度界定已裁「迭代周期 ≈ 一周左右」(2026-10-11 用户裁定,roadmap.md:101),本批单屏口径恰在一迭代盒内。
天然切分点:T3 末(战斗屏已过闸可单独交付见效)。
---
## 4. 验收(逐任务对应,全绿 + 记录齐备才走 T5)
| # | 需求点 | 测什么 | 落点/判定手段(实存工具,2026-10-16 核) |
|---|---|---|---|
| V1 | 方向稿先行工序 | 每张精制样张有对应「方向稿 + 用户选划记录」在案;无记录直接精制 = 违红线 L3 呈报 | **人闸留痕,无工具可判**(降级为件核:开工单/实现报告附「方向稿 → 选划 → 精制」三段记录表;禁称机检化) |
| V2 | 整屏四项 | ①文字对底 ≥4.5:1 逐点位(大字档 3:1 底线)②三档明度带层间灰度可分 ③首焦唯一 ④无文字裸压纹理 | ①②④=`tools/shot_checker.py` B/C/D 段(+F 段),坐标源 `game/smoke/shot_probe.gd` 产的 shot_meta.json,**须先按屏扩取样点位**;**③=人闸**(shot_checker E 段明文「语义面无法机检」),本批以目检记录判定并留档 |
| V3 | A1–A6 上屏 | A1 色板对表+禁入带全图扫描、A2 拼缝周界闭合、A3b Δ≥30、A4 混排相融(含尺寸基线硬等)、A5 四缘净空、A6 密度 ≥4.70;**另新建 48 色计数**(现行无此工具,art-bible §8.3 入包色板检查器另属他批) | 同上脚本机检段(`render_sample_sheet_v3.py` L925-1031 复用扩展),生成即验 |
| V4 | 战斗屏专项五项 | 按 Q6 已裁乙案**两闸分跑**执行(专项第 5 项只产裁决记录);敌层插入位置随本样张一并清偿,产出裁决记录一条 | 第 1 项借 V2/V3 断言(数字占位区零侵入沿 F5 族);第 2 项三语境灰度可辨**须新建**(可仿 §1.4 验收 2 灰度分类法);第 3/4/5 项=图上元素存在性+判读,**归 T3 目检**;裁决记录(含 H5 徽章明度档复核转正结论)落 T3 |
| V5 | 否决先例映射表 | P6 四条 + P7 两例逐条对到具名断言项(映射底表=脚本 L1156-1160),表格随样张一并归档 | 人工产物,实现报告附件(T2 出表、T3 随终审一并呈) |
| V6 | 用户终审 | 「过」字记录;不过则回炉轮次计数留痕 | T3 人闸(active.md 开工单/状态记录留痕) |
| V7 | 产出物落位 | `reports/` 三份写齐且互一致:反哺点清单 §8 每条有落点(08 行/看板行/06 注销/art-bible 冻结段全文四处对应),边界条款与 §0.5 处置表、06 三屏排队行(Q2 甲案)与目标规格清单(Q1 丙案)内容在册;文档落文与链接/废止词复核归文档批(不进本批域) | 件核:反哺点清单逐条对表 §8;闸面由 `tools/run_all_gates.sh` 按改动域选档承载(唯一闸入口与唯一闸清单,卡内不另列名单) |
**闸口径**:本批零玩法零数值——MC 零跑;批末重闸 = `tools/run_all_gates.sh` 按本批改动域选档(本批触碰 `tools/` 样张面与断言扩面、零 `game/` 运行时改动 → `--code`)全绿才合并;**样张脚本不入门禁常设闸表**(周期件属性,本批零闸件增删),跑动记录留档。卡内不另列闸件名单(脚本即唯一清单)。
---
## 5. 设计契约(红线)
🔴 **契约 1 · 零游戏运行时改动**:本批只产图、只动样张工具面与文档;任何「要改 game/ 代码才能让样张成立」的发现 = L3 停机呈报,改动另批。
🔴 **契约 2 · 方向稿不可跳**:目检先行是闸形(art-bible §1.4 原文),跳过方向稿直接精制 = 本批最高级违例。
🔴 **契约 3 · 卡片不代拍路线**:渲染手段、素材输入级别、四屏范围、字重前提——全部以 Q 项呈用户,批内任何任务禁止预设某路线已成立。**v1.2 闭合**:六项已全部经用户令裁定(2026-10-16 用户令:三卡全部待裁项按推荐方案裁决〔用户将裁权授出,由主线会话代拍板留痕〕,见 §7),本契约转为执法面——各卡按已裁口径执行,批内禁止自行升档、改线或另开路线。
🔴 **契约 4 · 机检不豁免目检**:断言全绿不构成过闸;终审权恒归用户;本批不申请验收豁免。
🔴 **契约 5 · 权威文档零直改(v1.3 换代)**:art-bible §1.4 冻结段、08、06 等一切文档回写义务一律转为反哺点清单条目——**过闸后成文**(未过闸状态下清单内对应条目只许注记「进行中」),用户会话裁决后归文档批落文;实现批唯一可写目录 = `reports/`(`active.md` 唯一例外)。
🔴 **契约 6 · 已裁项不可重开(v1.1 新增,用户红线)**:§0.6 核收表所列事项一律禁止以「样张冻结须总览」「再确认一遍」名义重列备选或再出推荐;本批 Q 项若与 §0.6 任一行相撞,以 §0.6 为准,该 Q 项当场收缩或删除并登记删除依据。
🔴 **契约 7 · 继承义务禁静默丢弃(v1.1 新增)**:§0.5 H1–H11 必须在冻结记录里逐项给「冻结/不冻结+去向批次」;留空即视为本批未完成,禁止以「不在本批范围」一句带过。
---
## 6. 异常分级
- **L1(自救)**:脚本路径/字体回退(素材两级回退失效时改主检出跑)、断言阈值取数笔误、图片命名不合归档纪律、样张产物误写回旧硬编码文件名(T0 已禁)。
- **L2(记录继续)**:方向稿张数在 2–3 内自选、断言实现落点(扩 shot_checker/旧脚本 vs 建新脚本)、精制版像素级微调不触发重目检的边界(构图级 = 需重目检,参数级 = 免)、冻结记录段的样张系列编号口径(见 T5 代拍注记)、**Q3 试验件不过闸 → 按 §7 预设退路退甲案纯代码管线重出(预裁定已成立,留痕即可,禁回炉问用户)**。
- **L3(停机呈报)**:T0 实跑发现 v3 管线或 shot_checker 链已不可跑通且修复超出样张面;方向稿全部被否决(触发 Q1 口径复核呈报——Q1 已裁丙案,改动须重新裁决留痕,禁批内自开重裁);发现整屏断言与格子 v3 冻结参数数学冲突(先例:v3.0 时代 Δ≥55 与明度分档带数学冲突退役);**本卡目标规格清单(Q1 已裁丙案)反提后,素材重产批若发生档位换代,触 A4 尺寸基线(立绘 46×48/小人源 32×32 硬等)变更——圣经 §1.4 L68 明文「资产升档批换代须显式解冻重校」,解冻须走 §1.4 修订,禁批内自改,本批样张侧亦禁预先解冻**;(v1.2 修订:原「Q1 裁乙案但前置批逾期未出终稿」等待场景随丙案裁定不发生,条目删除);**§7.5 跨卡前提冲突**(任一后续卡的前提在执行中被否定——三卡串行序位已裁=徽章→样张→观感,本条保留为序内冲突兜底)。
- **L3 分类结论注(2026-10-17 用户裁 U1 预授权时一并复查,留痕防重复呈报)**:本卡四项 L3 **全部归入「触既有裁决面/超出本批面」类,一律维持停机呈报**——①v3 管线不可跑通且修复超样张面=超出本批面;②方向稿全否=回 Q1 口径复核(Q1 已裁丙案,重裁须用户);③A4 尺寸基线解冻=圣经 §1.4 L68 明文须走修订;④跨卡前提冲突=序位裁定面。本卡无一属于「可自动处置类」,故 U1 预授权对本卡零改动。**两处人闸(T1 方向稿选划/T3 气质终审)同属不可取消项**:契约 2「跳过方向稿=本批最高级违例」、契约 4「机检全绿不构成过闸且本批关闭豁免通道」,取消即拆掉防不满意机制本体。
---
## 7. 裁决记录(v1.2 改实:本卡 Q1–Q6 全部已裁。裁决来源统一措辞逐条留痕:「2026-10-16 用户令:三卡全部待裁项按推荐方案裁决(用户将裁权授出,由主线会话代拍板留痕)」;本节保留选项原文+裁定案+一句理由,v1.1 的「不带推荐案、逐条亲裁」呈报框架随用户授出裁权关闭,备选案文字仅存档供对表)
> **v1.3 执行口径注(2026-10-17 文档同步闸 v3)**:本节裁定文中「目标规格清单…落 §1 完成定义 2/T5/§8」「三屏登记 06 排队条的动作列入同步面,写入文档时点=批次出口(T5/§8)」等落文类表述,一律执行为「成文内容随 `reports/iter30-反哺点-<完成日>.md` 交付,用户会话裁决后归文档批落文」——裁定内容(做什么)不变,落文方式随 v3 换代。
| # | 事项 | 备选案与影响 |
|---|---|---|
| **Q1** | **素材输入级别(本卡核心裁点)**:样张用什么料出? | **甲 · 纯现有占位资产合成**:最快(T1 可直接开跑),零前置依赖;代价 = 量的是占位态的屏——素材重产后样张作废须重跑,且「丑」的一半源头恰是占位素材本身,用户不满可能原样复现。**乙 · 等贴图重构批与素材终稿出件后再出样张**:样张一次到位量真料;代价 = 串行依赖两批(UI 贴图重构批 06:21-27 待办;立绘清晰度回退评估 08 §7 第 7 行待裁——**注意:「资产升档重产」不是未裁方向,而是 2026-09-30 已裁案乙+2026-10-04 令暂不做、重启=用户点名,本案只能等其被点名,**禁止代裁****),周期以周计起步,期间「判定尺子没造出来」状态持续;**连带硬账**:素材换代即触 art-bible §1.4 L68「A4 尺寸基线…资产升档批换代须显式解冻重校」,样张的 A4 硬等断言(立绘 46×48/小人源 32×32)须先解冻(走 §1.4 修订),不是零成本串行。**丙 · 样张即设计目标图**:样张承担定稿目标职能,冻结的规格反向给素材重产批提要求(尺寸档/色板/明度档逐件成文,正对 §0.5 H1–H4 的尺寸欠账);代价 = 要求方向稿本身已接近目标气质,用户对既有程序化气质不满时,方向稿的路线选型(Q3)权重陡增;素材批按样张规格返工的工程量未估,**且本案出的「规格建议单」不改变 08 未决项 6/7 的待裁性质(升档与否、回退高清与否仍归各条用户裁)**。**三案可混合**(如战斗屏走甲/丙、营地屏走乙)——混合粒度亦请一并裁定 **〔已裁:丙——2026-10-16 用户令:三卡全部待裁项按推荐方案裁决(用户将裁权授出,由主线会话代拍板留痕)〕裁定案:样张即设计目标图,样张承担定稿目标职能,冻结规格反向给素材重产批提要求。理由:唯丙案直接服务立场前提(因丑而改),且消除对已挂起素材闸口的串行等待。任务面同步(v1.2):① 新增交付物「目标规格清单随样张冻结产出(尺寸档/色板/明度档逐件成文)」,落 §1 完成定义 2/T5/§8;② 「三案可混」语义关闭——战斗屏适用丙案,其余三屏按 Q2 裁定(甲案)不在本批,无混合对象;③ 乙案「A4 尺寸基线须显式解冻重校」连带仅于规格清单引发后续素材档位换代时在另批触发(§6 L3 留兜底)** |
| **Q2** | **四屏范围与次序**:本批做几屏? | 战斗屏第一优先的依据已成文(06:37-43「最高优先级」+ 战斗表现:73「一屏样张是硬前置」+ 看板 ⬜ 现状),不复议;剩余 = 关卡/菜单/营地三屏:**甲 · 本批仅战斗屏**,余三屏 06 立排队行;**乙 · 战斗+营地**(营地是全作最暖屏、贴图债最重,Q1 裁乙案时此组合依赖最重);**丙 · 四屏全本批**(按 v1.1 重算估时 ≈8.5–11d,超 7d 软盒,按 T4 拆分点强制拆两批)。**次序连带(v1.1 补,与 §7.5 D2 同读)**:关卡屏已有立闸前的 v1 冻结样张(P10),若排在本批之后且其 L2/L3 换色/重产落地,本批冻结的明度三档带与观感密度读数即作废须重跑——四屏的先后不是纯排期题,是作废范围题 **〔已裁:甲——2026-10-16 用户令:三卡全部待裁项按推荐方案裁决(用户将裁权授出,由主线会话代拍板留痕)〕裁定案:本批仅战斗屏;关卡/菜单/营地三屏登记 06 排队条的动作列入同步面,写入文档时点=批次出口(T5/§8)。理由:单批 5–6d 在迭代盒内跑透一屏一闸,胜过四屏平摊超盒。关卡屏已有 v1 冻结样张(P10)事实保留为注记;乙/丙案及其作废范围连带段仅存档不执行** |
| **Q3** | **渲染手段选型**:样张怎么画出来?(用户已质疑 Godot shader 做特效的能力边界——选项、影响、代价列全,已裁见右栏) | **甲 · 纯代码绘制(PIL,现行管线)**:零新依赖、A1–A6 断言体系现成;已知上限 = v1 四否决原因全部产自该路线,氛围类效果(光带/泛光/呼吸感)只能静态模拟。**乙 · Godot 引擎实拍 + shader**:样张=引擎真实渲染(截屏管线先例 **`game/smoke/shot_probe.gd`+`tools/shot_checker.py`**——v1.0 误写为 `tools/shot_probe.gd`,已按实存路径订正;`tools/render_gate.sh` ①段即为该通路),发光/抖动/去饱和与实机逐位一致,氛围层两派并存(§7.6(三) L465)可在实拍层直接看出统一效果;代价 = 全仓 shader 仅 desaturation.gdshader 一枚先例(P9 实测),发光类特效能力边界未验证,须先半天试验件坐实「能不能」再谈「好不好」,且实机依赖动画与光照时序、静态定格取样工序需新建(shot_probe 现按「真实按钮信号路径驱动+双帧抓视口纹理」出帧,无「目标构图」概念——它出的是现状图,样张要的是目标图,这层差距即本案的真实成本)。**丙 · 新素材合成**(贴图批/远程生图件入样张画布):质感上限最高;代价 = 即 Q1 乙案语义,本选项在 Q1 裁甲/丙时不可用。**丁 · 混合**(底板引擎实拍 + 标注层脚本,或方向稿用甲精制版换乙):灵活;代价 = 两套管线双份维护与断言口径分岔。〔氛围层两派统一(平滑 vs 抖动)随本案一并裁:选了乙,两派差异在实拍图上直接可见,裁决成本最低〕 **〔已裁:乙为主、预设甲案退路——2026-10-16 用户令:三卡全部待裁项按推荐方案裁决(用户将裁权授出,由主线会话代拍板留痕)〕裁定案:Godot 引擎实拍+shader 为目标路线;开工先半天试验件坐实发光类特效能力——试验件不过闸即退甲案纯代码管线重出。理由:目标图须与实机渲染同语系,而退路预裁定已写入本栏,失败面不再回炉问用户(留痕即可,见 §6 L2)。连带:氛围层两派统一结论在实拍图上呈报、落 T3 裁决记录;丙案即 Q1 乙案语义,随 Q1 裁丙不可用;估时已按本案重算(§3)** |
| **Q4** | **字重与拉丁专件是否入样张前提**:06:29-35「UI 字体补完」条载明现行单字重、层级只能靠字号档差一个手段(art-bible §7.4 L434 字重纪律定的口径是**字号档差〔相邻层级 ≥1.25×〕+墨色深浅+字重两档**三件齐用)——样张用现行单字重出,还是先补字重再出? | 甲 · 现行单字重出样张:零前置;风险 = 样张冻结的层级观感建立在缺一个手段的基座上,字重补齐后层级重排可能追改样张;**另一处连带读数**:§1.4/§7.2 的大字档判据写作「≥18px **或加粗档**」,现行单字重下「加粗档」这一腿在实机上不存在(FS_TITLE 24/FS_H1 34 只吃字号档,§7.4 L432 六档),故本案直接决定整屏①项能不能按原文全判。乙 · 字体补完先行:样张一次到位;代价 = 又添一条串行前置(字体条本身含「补不补拉丁专件、上不上第二字重」未裁点——该未裁点归字体条 06:33,本 Q 只裁先后次序,禁在本卡代裁)。丙 · 样张按「目标字重效果」示意出图(方向稿级),字体批按样张补真件(即 Q1 丙案语义在字体面的特化) **〔已裁:丙——2026-10-16 用户令:三卡全部待裁项按推荐方案裁决(用户将裁权授出,由主线会话代拍板留痕)〕裁定案:样张按「目标字重效果」示意出图,字体补完批按样张补真件——即 Q1 丙案语义在字体面的特化,两案口径一致。理由:样张即目标图口径下层级观感不必建在缺手段的基座上,且零串行前置。「补不补拉丁专件、上不上第二字重」仍归字体条自身裁点(06:33),本卡不代裁;甲/乙案文字仅存档** |
| **Q5** | **旧样张死指涉处置**(06:609-615 在册 P0①,本 Q5 已裁,处置落 T0):art-bible **L68 与 L562**(06 原文写「68/527 行」,527 行现为 §9.1 参考表——**行号读数已过期,T0 连 06 该行一并订正**)与看板 L13 仍引用磁盘已不在盘的 `sample-batch-v3-final-2026-09-30.png`;同系列 v0/v2/v31/v32/v33/v34/v35-draft 在盘。 | 甲 · 死指涉改指在盘最近稿并加注「冻结原件失管,指代稿」;乙 · 判过期——旧引用随本批整屏样张冻结记录一并换代重写;丙 · 随本批重跑后新产物自然替代(不做单独修复动作)。〔登记纪律注:art-bible §8.3 L500 归档纪律本就规定主文档禁引用 artwork/ 散文件路径——无论何案,新冻结记录按「描述性出处」写,不再挂散路径;**同条 P0②(主角棋盘小人未走像素管线,重产未在册)属 06 同条另一未裁事项,本卡禁把 P0② 一并吃掉**〕 **〔已裁:甲——2026-10-16 用户令:三卡全部待裁项按推荐方案裁决(用户将裁权授出,由主线会话代拍板留痕)〕裁定案:art-bible 68/562 行与看板 13 行三处死指涉改指在盘最近稿 v31/v32/v33-draft 之一并注「指代稿」,具体指哪份由 T0 实查盘面后定;新冻结记录按 §8.3 描述性出处纪律。理由:指代稿保留判定物演进链可追溯,优于判过期换代重写。P0② 与本裁分离,仍归 06 条;乙/丙案文字仅存档** |
| **Q6** | **双口径断言集合体方式**:整屏四项(art-bible §1.4 L64)与战斗屏专项五项([战斗表现](../design/gdd/systems/战斗循环/战斗表现.md):85-93)合并为一张断言集一次判定,还是两闸分跑(先专项后整屏,各自回退语义不同——专项第 5 项只产裁决记录不产过/不过,:93 已成文)? | 甲 · 并集单闸:一次出图一次判定,报告最简;乙 · 两闸分跑:与两文档各自 owner 语义对齐,回退定位更准;代价 = 双份跑批记录。**注(v1.1):本案只裁「两套既有口径怎么合体」,不重裁两套口径本身的内容——四项与五项均是既有条款。** 另附裁:敌层插入位置裁决义务是否随本样张一并清偿(战斗表现:75 登记「随美术测试一并裁决,不在本文提前定」——该义务本就挂在本样张上,本 Q 只裁清偿时点,不裁是否要做) **〔已裁:乙——2026-10-16 用户令:三卡全部待裁项按推荐方案裁决(用户将裁权授出,由主线会话代拍板留痕)〕裁定案:整屏四项与战斗表现专项五项两闸分跑,各自回退语义独立;专项第 5 项(敌层插入位置对照组可裁)只产裁决记录、不产过/不过。理由:两口径各有 owner 文档,分闸回退定位更准。敌层绘制优先级插入位置「随本样张一并清偿」的原句保留(战斗表现:75),清偿点=T3;甲案文字仅存档** |
**开工门(v1.2 改实)**:Q1–Q6 已全部裁决(2026-10-16 用户令:三卡全部待裁项按推荐方案裁决〔用户将裁权授出,由主线会话代拍板留痕〕;v1.1 检索结论——六项当时均未被既有条款裁过、且关闭三处隐性重问(①「微光五组色相是否重冻」归 §0.6/徽章卡 Q2,②「1920 档/帧数规格/资产升档」随闸三裁决已闭,③「敌层插入位置要不要裁」已登记为样张义务)——保留为本次裁决的对表依据)。U0 剩余条件 = 用户 `/iter` 发令。Q3 已裁乙为主:半天试验件置于 T1 最前作前闸,试验件不过闸即按 §7 预设退路退甲案,**不再回炉请裁**(v1.1 原文「复核亦归用户裁」随退路预裁定关闭)。T0 事实坐实段(含 Q5 指代稿的实查拍板)凭本卡即可先行(零路线依赖)。**排期状态**:本卡已编入迭代 30(2026-10-16 用户令,[roadmap](../roadmap.md) 编入行已在案、状态刷新列反哺点);迭代 29 已于 2026-10-16 合入 master(fff4f120,roadmap.md:88 状态=✅ 已入库,见 §7.5 D4),「迭代 29 完成后开工」的前置已满足,但仍不构成排期承诺(见 §0)。
### 7.5 跨卡依赖(v1.1 新增——三卡次序耦合;v1.2 改实:三卡已编入迭代 30,串行序位已随 2026-10-16 用户令裁定=徽章→样张→观感,禁各自假设对方状态)
| # | 耦合点 | 谁的前提 | 次序与理由 |
|---|---|---|---|
| D1 | **类型微光色值与明度档的冻结口径** | 徽章卡(迭代 30 卡①,已入库合并 7ffd1dc0,卡片已退役)Q2 与其 S6 行 | **已改实(2026-10-16 用户令:三卡全部待裁项按推荐方案裁决〔用户将裁权授出,由主线会话代拍板留痕〕——徽章卡 Q2 已裁甲案:明度档取 provisional 值)**:「徽章卡 Q2 先裁」义务随其裁定关闭;**其复核转正义务落本卡样张冻结件**——本卡 T2 新增 H5 组间明度档复核+组间互辨抽测、T3 呈转正结论、写入完成定义第 2 条交付义务(见 §1/§3)。事实核对保留为对表依据:五组**色相**确已随样张 v2 定稿(art-bible §4.2 L218「样张 v2 定稿(2026-09-28)」+HUD框架:140 同记——「样张 v2 已冻结」与 08「战斗一屏样张=待做」并不矛盾:前者是**格子样张**序列(§1.4 L66/L68),后者是**整屏样张**序列(§1.4 L64),两条序列各有其闸),本卡不承接色相重冻义务;「徽章卡若裁乙(等本卡总闸)」分支不再发生。**本卡禁把色相值列进 Q 项**(已按契约 6 关闭) |
| D2 | **棋盘瓦片重产 vs 换色**(素材线同一入口) | [二三关棋盘观感卡](需求卡片-二三关棋盘观感批-2026-10-16.md) Q2(甲 recolor 换色 / 乙 远程生图重产) | **已改实(2026-10-16 用户令)**:序位裁定=徽章→样张→观感、本卡位第 2——观感卡排本卡之后,「同窗呈报两案代价」义务关闭;该卡 L2/L3 落地对本卡整屏②明度三档带与 A6 观感密度读数的复核,属该卡开工时按其 V2(A6 ≥4.70 保持)自跑的复验项,本卡冻结记录注明本样张基线读数供其对表即可;本卡 Q1 已裁丙案(非乙案),与其 Q2 各案不再有工期串行耦合 |
| D3 | **触碰面重叠与合入次序** | 三张卡共同 | 三卡都写 06/08/07/art-bible 的 git 提交史/roadmap/active.md,徽章卡另动 `presentation/views`+冒烟,本卡另动 `tools/` 样张面。按 roadmap.md:93「单迭代串行」与 roadmap.md:101「一迭代 = 一次合并登记」⇒ 三卡禁止三线并行。**已改实(2026-10-16 用户令):三卡串行序位=徽章→样张→观感,本卡位第 2**;「一次合并、批内分波」组织方式随序位裁定确定,v1.1「不预设结论、归用户排期令」呈报框架关闭。**v1.3 注**:三卡文档直写义务已按文档同步闸 v3 全部转反哺点(落 `reports/`),汇聚文件重叠消解,但串行序位裁定不失效——golden 基线与冻结内容的先后依赖仍在(D1/D2) |
| D4 | **门禁语义改动的先后** | 迭代 29 测试分层与门禁语义修正批 | **已结案(2026-10-16 合入 master,合并提交 fff4f120;roadmap.md:88 状态=✅ 已入库)**:该批改的 `tools/run_all_gates.sh`/探针判定/退出码权威语义(其契约 2「三硬闸不可绕过」)已在现行闸内生效,本卡 V7 的闸口径直接按当期语义执行(本批 → `--code`)并按其当期读数采基线;原「该批先行/本卡先行」两种分支随之消解,本卡仍不改写任何判定脚本。(原两分支表述留档供沿革查 git log) |
| D5 | **08 表现表同一表的多行改写** | 三卡+08 v1.6 定稿纪律 | 08 头注 L5 明文「Alpha 内容范围冻结,增删条目须经用户裁决」——三卡都只有**改状态列**的权限,**无一行可新增/删除**;本卡 T5 回写仅动「战斗一屏样张」一行,若需要为新条目动集合,一律 L3 呈报。**v1.3 注**:该「回写」执行为反哺点条目(仅动该行的改写要求照录进清单),零直改 |
---
## 8. 关联文档同步面
> **v1.3 执行口径(2026-10-17 文档同步闸 v3)**:本批**禁写 `design/` 整目录与根规则文件**,唯一可写目录 = `reports/`(`active.md` 唯一例外)。下表「同步义务」列一律执行为「列进 `reports/iter30-反哺点-<完成日>.md`:哪个文件哪一节、写成什么、依据,用户会话裁决后归文档批落文」;成文内容本体(冻结记录段全文、目标规格清单、处置表等)随对应反哺点条目交付。
| 文档 | 关联点 | 同步义务(v1.3 全部=反哺点列清单,另注者除外) |
|---|---|---|
| [art-bible.md](../design/art/art-bible.md) §1.4 | 新增**整屏样张冻结记录段**(编号口径见 T5 代拍注记——格子序列已用 v2/v3、关卡屏自成一列,禁两处各叫各的);段内随附**目标规格清单(Q1 已裁丙案:尺寸档/色板/明度档逐件成文)+徽章明度档复核转正记录(H5/§7.5 D1)**两件交付物;§7.6(三) 氛围层并存条改写为已裁决口径(经 T3 实拍图记录,Q3 乙案);§0.5 H1–H11 逐项处置写进该段;死指涉三处按 Q5 甲案改指「指代稿」(T0) | 反哺点 · 仅 T3 过闸后成文(契约 5) |
| [art-status-checklist.md](../design/art/art-status-checklist.md) | 战斗一屏样张行 ⬜→✅;:14 指针旧锚 §5.1 订正;余屏行按 Q2 刷新 | 反哺点 · 过闸后列清单 |
| [06-待办清单.md](../design/gdd/06-待办清单.md) | 战斗一屏样张条(:37-43)注销(去向登记当批状态记录)+:41 归属栏旧锚订正+:39「尺子还没造出来」措辞按 P1 读数订正;**关卡/菜单/营地三屏新立排队行(Q2 已裁甲案之本批交付义务)**;P0① 死指涉条(:609-615)按 **Q5 已裁甲案**处置(改指指代稿并注)+该行「68/527」行号读数订正 | 反哺点 · 列清单待裁 |
| [08-Alpha版本标准.md](../design/gdd/08-Alpha版本标准.md) | 「五 · 表现」战斗一屏样张行(:158)待做→在役(过闸后);完成定义边界条款注记(样张过闸 ≠ 占位素材全体转正)。**禁动项**:头注 :5 已定稿、增删条目须用户裁;「七 · 未决项」:208-210 三行(站桩帧动画/素材升档/立绘回退)本批一律不改 | 反哺点 · 过闸后列清单 |
| [战斗表现.md](../design/gdd/systems/战斗循环/战斗表现.md) | 敌层插入位置裁决记录登记(Q6 附带,:75/:93 既有义务);「一屏样张的验收标准」节(:79-93)状态刷新;**:83/:116「四区」旧措辞订正为三区 B/C/D** | 反哺点 · 随裁决列清单 |
| [character-design.md](../design/art/character-design.md) | **Q1 已裁丙案**:样张反提的立绘/小人规格(目标规格清单)回写其档位节;H4(§5.3 护甲三档视觉区分)与 H9(钩爪恐魔宽≥高豁免)处置登记 | 反哺点 · 随冻结记录列清单 |
| [HUD框架.md](../design/gdd/systems/表现与支撑/HUD框架.md) | **D1 已改实(徽章卡 Q2 裁甲案)**:本卡承接微光组间明度档复核转正/互辨抽测(H5)义务,按其「唯一事实源」纪律回报结论,定义与口径零复述 | 反哺点 · 义务已成立,结论列清单 |
| `tools/shot_checker.py`/`tools/render_sample_sheet_v3.py`/`game/smoke/shot_probe.gd` | 取样点位与断言扩面(T2);**闸件集合本身不改**(增减只走 `tools/run_all_gates.sh`);样张脚本仍不入常设闸表 | 工程面 · 批内更新(可写) |
| [roadmap.md](../roadmap.md) | 迭代 30 行编入笔已发(2026-10-16 用户令,行已在案);批内状态刷新 | 反哺点 · 列清单待裁 |
| active.md | 状态快照与批次记录 | **本批更新(唯一例外 · 会话状态源)** |
| [07-体验验证清单.md](../design/gdd/07-体验验证清单.md) | T3 目检轮次与后续实机目检衔接项登记(**现行 07 无样张类条目,实查 2026-10-16 零命中——本批为首次登记,属新增条目须走 07 自身纪律**) | 反哺点 · 列清单待裁(目检项另进 reports/ 实现报告目检总清单) |
---
## 9. grilling 审查记录(v1.1,2026-10-16,对抗审查员;逐攻击面给结论/证据/处置)
**审查方法**:卡片每一处引用逐一开文件核行号与措辞;Q1–Q6 逐条全库检索既有条款与裁决(design/gdd/06、08 及其 git 提交史、design/art/art-bible.md、character-design.md、art-status-checklist.md、systems/战斗循环/战斗表现.md、systems/表现与支撑/HUD框架.md、ux/battle.md、playnote.md(仓库根)、roadmap.md、active 侧状态记录残留、`tools/` 与 `game/` 实码、git 只读历史);与迭代 30 另两张并行卡逐 Q 比对互斥面。**只编辑本卡,未动任何其他文件;git 仅只读查询。**
### G1 事实引用核实(攻击面 1)
| 引用 | 核实结论 | 证据 | v1.1 处置 |
|---|---|---|---|
| art-bible §1.4 整屏样张闸 | **真** | §1.4 L64 全文(四屏/四项/闸形方向稿先行/执行时机=屏级视觉落码开工前) | 保留并补 L64 行号 |
| art-bible §7.1 占位完成条款 | **真** | §7.1 L411 | 完成定义 3 原写「08 §7.1 指路」**错**(08 无 §7.1),已改指 art-bible §7.1 L411 + 08:158 行 |
| art-bible §7.4 字重三件口径 | **真** | §7.4 L434(字号档差 ≥1.25×+墨色深浅+字重两档)、L432 六档表 | 保留,补 L434;并新增连带读数:「加粗档」在单字重下不存在,直接决定整屏①项可判面(Q4) |
| v3 脚本 A1–A6 | **半真** | 头注 L21-26 确有 A1–A6;**A2 拼缝周界闭合被 v1.0 漏列**;A1 实义=色板对表冻结值+禁入带(L928-930),**不是 48 色计数**;A4 含尺寸基线硬等(L1029-1031);L23「Δ≥55」与 L1157「std ≥50.9」是旧文本(现行定值 A3b Δ≥30,L35 与圣经 L68) | V3 与 T2 逐名义重写,48 色单列为新建项 |
| 「v1 四否决原因」 | **v1.0 只列两条编号,漏 ②④** | 脚本 L1156-1160 映射表:①显示太丑→A6;②封印色冲突→封印黑(A1a);③立体感→A3/F6/F7;④微光格只留微光→F5c;头注 L12-26 仅标 ①③ | P6 补全四条+标注权威表位置;V5 底表指到 L1156-1160 |
| 08 当前行状态 | **真** | 08:158「战斗一屏样张=待做」;:156 UI 贴图重构批待做;:157 UI 字体补完待做;:146 立绘清晰度回退待裁;:154 素材升档待裁;§7 未决项 :208-210 | 保留;「站桩帧动画」原被本卡写作「Beta/后批事项」**定性失实**(实为 08 未决项第 5 行待裁),已订正并入「不做」禁代裁清单 |
| 看板 13 行/§5.1 旧锚 | **真,但 v1.0 漏了 06:41 同型旧锚** | art-status-checklist:14;06:41 | P3 补记;T0 订正面扩为两处 |
| 死指涉行号 | **v1.0 沿用 06:611 的过期读数** | 06:611 写「68/527」;art-bible grep 实测指涉在 **L68 与 L562**,L527 现为 §9.1 参考表 | P4/Q5 改为 68/562,并把 06:611 自身列入 T0 订正面 |
| 2026-10-06 playnote 引文 | **原文被压缩改写** | playnote.md:9 逐字:「现在的立绘太丑了,像素化的粒度太低了,导致根本看不清立绘;看是否可以改回用较为高清的图片?」;日期出处=06:465-471「同日 playnote 立绘批评」 | P7 改逐字引+注明文件本身无日期 |
### G2 「v2 已冻结/样张批冻结候选值/08 待做」三者是否矛盾(攻击面 2)
**结论:不矛盾,v1.0 与并行徽章卡都需把两条序列分开。**
- 格子样张序列:v2 冻结(2026-09-28,圣经 §1.4 L66,四冻结含②色板)→ v3.0 解冻①④ → **v3 重冻生效(2026-09-30,L68,机检 17/17)**;类型微光五组**色相**属②色板面,§4.2 L218 明文「样张 v2 定稿(2026-09-28)」,HUD框架:140 同记「五组色相经样张 v2 冻结」——**冻结为真实现状**。
- 整屏样张序列:条款 2026-09-29 v3.0 才立(L64),**从未出过一张整屏样张判定物,四屏冻结记录零条**——08:158「待做」与看板 ⬜ 因此为真。
- 「候选值,样张批冻结」的出处是 06:431 里 **2026-09-22 那次更新的历史文本**,滞后于 v2/v3 两次冻结;06:431 同时**明文保留**「样张批组间互辨抽测义务」,且**组间明度档值从未定义**——这才是真挂在样张批上的东西。
- **v1.1 处置**:①新增 §0.6 关闭「重裁色相」的可能;②新增 §0.5 H5 承接互辨抽测与明度档;③新增 §7.5 D1 写明徽章卡 Q2 与本卡的先后与理由。
- **另两处状态读数订正(本攻击面的真发现)**:(a) 整屏四项的**机检代码已存在并跑过实机渲染帧**——shot_checker.py 头注自述「整屏样张闸机检(迭代 9 卡⑤/T8 2026-09-30)」,git 053baeee 记「实测 PASS(WCAG 5.79-13.14/三档 204>79>55/骨架四屏过)」,现挂 render_gate.sh ①段周期跑;**③焦点预算 E 段明文「无法机检→人闸」**。v1.0 的「四项从未作为断言集执行」「只存在于条款文字、无对应代码」「本批=该闸首次执行」三句按此改写为「判定物与冻结记录从未出、③从未判」。(b) 新增 P11:§1.4 的执行时机(屏级落码开工前)已被迭代 9/16/25/26 四轮落码越过、Alpha 已于 2026-10-03 到手,§7.1「占位禁止跨里程碑」事实上已被跨——本批定性=**闸后补跑**,冻结语义随之注明,禁写成事前定方向。(c) 新增 P10:关卡屏已有立闸前的 v1 冻结样张(2026-09-28,11/11,产物在盘),Q2/T4/估时三处按「补判四项」而非从零改写。
### G3 「全仓 shader 仅一枚先例」(攻击面 3)
**结论:真。** `find -name *.gdshader` 全仓命中 1 件(`game/presentation/views/desaturation.gdshader`);`ShaderMaterial` 消费面仅 `board_view.gd`(:111 声明/:170-172 创建/:194 参数 tween),`.tscn` 内无 shader 节点;样张与机检面全为 PIL 代码绘制。**处置**:P9 补实测细节;Q3 乙案的「能力边界未验证」措辞保留,另补 shot_probe 出的是「现状图」不是「目标图」这一真实成本(防把「实拍管线已有」误读为「样张管线已有」)。
### G4 Q1 三案依赖链与「混合粒度」(攻击面 4)
- 「UI 贴图重构批」= 06:21-27 待办(Alpha 需完成)+08:156 待做 → **v1.0 读数真**。
- 「资产升档重产批」= **不是未裁**:06:465-471 载明已裁案乙(2026-09-30 迭代 9 三裁决)+2026-10-04 令暂不做(重启=用户点名)+2026-10-06 优先级再降,且 v3.0「档位上调方向候选」已被 v3.7 八敌逐键定档实质完成;08:154/§7 第 6 行的「待裁」只问**进不进 Alpha**。v1.0 把它与「立绘清晰度回退评估」并列为「两个未裁闸口」**不准确** → Q1 乙案改写为「一已裁挂起+一真未裁」,并补 A4 尺寸基线须显式解冻重校的连带硬账(圣经 L68)。
- 「立绘清晰度回退评估」= 08:146 待裁+§7 第 7 行真未裁 → **v1.0 读数真**。
- 「三案可混合」的粒度:全库无既裁边界(样张序列只做整屏闸,从未有文档规定四屏可用不同素材级别),**故保留为请裁项**,不属再问已裁。
- **处置**:Q1 文字改写;新增契约 6/7;「不做」新增禁代裁 08 未决项三条。
### G5 估时 4–7d 的构成依据(攻击面 5)
**结论:v1.0 无出处且加总错误。** T4 自述「逐屏 1.5–2d × 三屏」= 4.5–6d,却把 T4 上限定为 3d;六卡相加实为下限 4d/上限 11d。**处置**:T4 上限改 0–6d,合计改「≈4–11d(单跑战斗屏 4–5d,四屏全推超盒拆两批)」,并补先例出处:迭代 9 T8 整屏机检建造=3–4h(`.zcode/plans/m3-iter9-loop-plan-2026-09-30.md:65`,故 T2 走复用不重建)、迭代 9 批 A T1 样张机检冻结=2–3h(同计划书:58)、迭代 13 4.8–5.3d/迭代 16 6.9d/迭代 26 2–2.5d/迭代 29 5.5–7d(roadmap.md:63/:68/:85/:88),周期上限口径=「迭代周期 ≈ 一周左右」(2026-10-11 用户裁,roadmap.md:101)。
### G6 与另两张并行卡的互斥面(攻击面 6)
**结论:存在三处「一张卡裁了另一张卡前提」的次序耦合,v1.0 完全未写。** 已新增 §7.5 D1–D5:D1 徽章卡 Q2(色值冻结口径)须先于本卡(它决定本卡是否继承色相重冻义务——核查结论:不继承,但继承 H5 明度档与互辨抽测);D2 棋盘观感卡 Q2(重产 vs 换色)与本卡 Q1 指同一条素材线、且其落地会使本卡冻结的明度带/密度读数作废(**双向作废关系,须同窗呈报**);D3 三卡共写 06/08/07/art-bible/roadmap/active ⇒ 单迭代串行纪律下禁三线并行;D4 迭代 29 改的正是本卡 V7 依赖的门禁语义;D5 08 定稿纪律下三卡都只有改状态列权限、无增删条目权限。
### G7 验收 V 条可执行性(攻击面 7)
逐条对 `tools/` 实存脚本核:**V1 无工具**(方向稿留痕不可机检)→ 从「机检化」降级为件核;**V2 ①②④ = shot_checker B/C/D(+F)**,但 B 段只判 meta 登记的已知文字框、C 段只有战斗屏三点 → 标「须先扩取样点位」,**③焦点 = 人闸**(E 段明文);**V3 = 样张脚本机检段复用扩展+48 色新建**(全仓无 48 色工具,grep 零命中);**V4 五项中第 1 项可借既有断言、第 2 项须新建、第 3/4/5 项目检**;**V7 原写「跑 doc 三闸+熵增对基线」违反 AGENTS.md:19/59「run_all_gates.sh 是唯一闸入口与唯一闸清单,文档禁另列清单」** → 改为指路该脚本,并声明本批零闸件增删。附带核实:`game/smoke/shot_probe.gd`、`tools/golden_compare.py`、`tools/golden_baseline_refresh.sh`、`tools/repo_structure_audit.py`、`tools/doc_link_audit.py` 均实存(07 无样张条目为实查所得,已注明本批首登)。
### G8 红线自检:卡片有没有替用户拍板(攻击面 8)
- **发现两处预设**:①目标句把尺子写成「§1.4 + 战斗表现五项的**并集**断言集」,完成定义第 1 条同样预设并集——而合体方式正是 Q6 未裁项 → 改为「两套口径的合体方式归 Q6 裁决(本卡不预设并集或分闸)」。②完成定义第 2 条写死「新增 **v4** 冻结记录段」——v4 这一编号预设了「整屏续用格子序列编号」,而关卡屏已自成一列(「样张 v1」)→ 改为「编号口径属批内代拍,在冻结记录一次说清」。
- **一处定性失实而非拍板**:「站桩帧动画」被写成 Beta/后批事项(实为 08 待裁)→ 已改,并列入「不做」的禁代裁清单。
- **审过判为保留(不属代拍)**:防线三「本批不申请验收豁免」= 本批自我从严的执行纪律,非改动用户裁决;Q2「战斗屏第一优先不复议」= 引用既成文依据(06:37-43/战斗表现:73);§0.5 各 H 项与 §7.5 只呈代价与次序、不给推荐案;Q1–Q6 全条无推荐语(历批「推荐案被推翻」纪律维持)。
- 本批 Q 项清单净变动(v1.1 记录;**六项已全部经 2026-10-16 用户令裁定,见 §7,本节当时呈报的六项全保留口径现为裁决对表依据**):**Q1–Q6 六项全保留**(无一被既有条款裁过,逐条检索见 §0.6 与本节 G2/G4),另关闭本卡内三处隐性重问(色相重冻/随闸三裁决/敌层义务存废),依据已写入开工门。
- 词面规范扫描:v1.1 全文按本次审查指定的十项废止词逐一扫描,初稿命中五处(否定副词一项四处、完成动词一项一处)已全部换代,复扫零命中。
### G9 审查中判定为「本卡无权处置、已登记呈报」的文档面
`tools/render_sample_sheet_v3.py` 头注 L23/L1157 旧口径残留文本;06:39「尺子还没造出来」与 shot_checker 现状有差;06:41/checklist:14 的 §5.1 旧锚;06:611 行号读数(68/527 应为 68/562);**art-bible §8.2 L490 指路 m3-iter9-t8-rulings 文件已在 8e02dcf0 删除(死指涉第二例,属 Q5 同族,处置口径与 Q5 同案在本批一并呈报)**;战斗表现:83/:116「四区」旧措辞;art-bible 头注 L3 仍写「现行为 v3.11」而当时文末变更表已到 v3.14。**以上均为他文件文本,本审查一律未改**,只作为本批 T0 订正面对表登记;其中头注版本号与他文档措辞是否订正,归各文档自身纪律与用户裁决。
> **v1.3 口径换代注(2026-10-17 文档同步闸 v3)**:本节为 v1.1 时点的审查实录,原文不重写;其中「doc 三闸」一类闸件枚举字样与「写入文档/登记 06」类落文表述,按 v3 口径只读为历史记录——闸清单与域划分的唯一出处恒为 `tools/run_all_gates.sh`(本批按改动域跑 `--code` 档),文档落文一律列进 `reports/iter30-反哺点-<完成日>.md` 归文档批。
@@ -1,229 +0,0 @@
# 需求卡片:战斗一屏样张批(2026-10-28)
> **来源**:待办清单条目「战斗一屏样张 P0 未开工」(设计面=美术轨;状态=美术设计已闭合+待写卡)
> **唯一事实源**:`design/art/art-bible.md` **v3.22** + `design/art/art-prompts.md` §5「资产出图工单(四组九件)」
> **规则指针**:`AGENTS.md` 硬性红线(含 2026-10-28 新立的「禁变异类测试与撤销类取证」验证口径)/`design/engineering/process-conventions.md` §1–§3/`code-conventions.md`/`test-conventions.md` §8/`doc-conventions.md` §12
> **旧卡说明**:`reports/需求卡片-战斗一屏样张批-2026-10-16.md`(v1.4)于 2026-10-17 被裁定不满足开发条件、终止开工,属非有效卡,本卡不继承其任何裁决;其考证过的代码现状与断言设计可作参考。
---
## 1. 目标与完成定义
**做什么**:把已经定稿的战斗一屏美术规格,变成一张可判定的样张和一批可运行的实装。
1. 用 PIL 代码绘制产出**战斗一屏样张**(1280×800 基座 + 1920×1080 全局等比 1.35× 两版),内容覆盖 `art-bible` §1.4 整屏内容清单与战斗表现内容清单。
2. 把 §1.4 整屏四项闸与战斗表现五项标准,写成**可在样张脚本内执行的机检断言**。
3. 把五项设计定值**落进实装代码**(血条换色、名字行垫素底、顺位徽标提档、护甲图标换件、站桩文字不压地台)。
4. 程序化产出两件素材并入包(地台件、护甲图标三枚)。
**完成定义(全部满足才算完)**:
- 样张两版产出,方向稿目检通过(用户判定),目检通过后机检冻结;
- 整屏四项断言 + 战斗表现五项断言在样张脚本内全绿;
- 五项代码定值落码完成,对应断言在冒烟面内通过;
- 地台件与护甲三枚 PNG 入包并被实装引用;
- 三份批件写齐(实现报告/代拍板追认单/反哺点清单)。
---
## 2. 设计事实源(必填)
**样张过审 + art-bible 条款版本**:
| 依据 | 版本/节 | 本卡取用 |
|---|---|---|
| `design/art/art-bible.md` | **v3.22** | §1.4 整屏样张闸四项;§2 明度三档带与世界底层两子带;§4.1 色板(余量 0,第二道门=不得三通道全 <8);§4.2 血条=危险红 `#A8321F`;§6.1 背景契约与地台独立件;§7.2 文字可读性;§7.4 字号六档与豁免清单;§8.2 尺寸档与甲/乙类缩放;§8.4 方向集 |
| `design/art/art-prompts.md` | §5 资产出图工单(四组九件) | 本卡素材需求的直接来源:工单七字段(路径命名/尺寸档/色板约束/明度归属/程序化与否/验收判据/出图要点) |
| `design/gdd/ux/battle.md` | 现行 | **屏级分区布局与格边长公式的唯一裁决源**——本卡涉及分区尺寸只指路,不复述数值 |
**已定死、本卡不再讨论的定值**:站桩素底=深档 `#282220`;五处换色合并(铲刃→`#C4C4C4`、提灯→`#B07A14`、食人魔皮→`#B8A582`、石君王灰皮→`#786E62`、轻甲木色→`#CDB88C`);新增两枚(`#868C9A` 重甲冷灰、`#A79CB8` 卓尔浅紫灰皮);48 色余量已归零;渲染手段=PIL 代码绘制;素材输入级别=丙(样张即设计目标图)。
---
## 3. 现状事实(代码级实查,每条带 file:line)
1. **样张脚本现役**:`tools/render_sample_sheet_v3.py`(1174 行)已有断言框架——`CHECKS` 列表定义于 `:113`,断言登记于 `:117`,全绿判定于 `:1033`,报告输出于 `:1164`。另存 `tools/render_sample_sheet.py`(1009 行)为旧版。本批在 v3 脚本内扩展。
2. **血条填充是裸字面量**:`game/presentation/views/standee_body.gd:444` 画填充用 `Color(0.41, 0.61, 0.91)`,即 `#689CE8`(蓝族,v3.22 §4.2 已登记为违例退役值),**未经 UiKit 常量**。
3. **血条条底同为裸字面量**:`standee_body.gd:441` 用 `Color(0.10, 0.10, 0.11)`(引擎默认主题深底近似)。
4. **素底牌已在役**:`game/presentation/views/ui_kit.gd:193` `HP_BAR_BACK := Color8(40, 34, 32)`(即 `#282220`),血数垫牌绘制于 `standee_body.gd:451`。**深档素底色无需新增,直接复用本常量。**
5. **顺位徽标违规**:`standee_body.gd:437-438` 用裸值 `13` 绘制①②③(`UiKit.GOLD_BRIGHT`),既不在 §7.4 ①条带内读数在册面、也不在②装饰 caption 在册面,属「清单外 11–13px 文字」,v3.22 已裁定提档 `FS_HINT` 14。
6. **护甲图标是字形占位**:`standee_body.gd:8-10` `const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"}`;绘制于 `:485` 与 `:496`(`:496` 用 `12` 绘制护甲字形,与名字同排)。
7. **名字行与地台重叠(实装债)**:名字行基线 `standee_body.gd:495` = `Vector2(..., box.y - 1.0 - STANDEE_FOOT_MARGIN)`,而 `STANDEE_FOOT_MARGIN := 12.0` 定义于 `:13` → **基线 y = box.y - 13**。地台矩形 `standee_body.gd:405` 返回 `Rect2(18.0, box.y - 18.0, box.x - 36.0, 12.0)`,纵向覆盖 **box.y-18 .. box.y-6**。名字行基线落在地台带内,**重叠确认**(非传闻)。
8. **B 区高度文码不符**:`game/presentation/views/battle_stage_view.gd:28` `custom_minimum_size = Vector2(0, 252)`(代码 252),而 `art-bible` §8.2 站桩立绘排版方案二①定 B 区高 **270**。二者差 18px。
9. **血数 11px 属豁免**:`standee_body.gd:453-454` 以 `11` 绘制血数并垫 `HP_BAR_BACK` 牌,合 §7.4 豁免①(14.3:1),**不在本批改动范围**。
10. **素材在盘但不入 git、且是旧档**:立绘与瓦片位于主检出 `game/assets/art/`(`character/` 26 件含八敌与主角 `character_hero_idle`/`attack`/`hit`/`portrait`;`board/` 瓦片),**整个 `game/assets/art/` 不入 git**(`git ls-files game/assets/art` 计数 0),fresh worktree 内缺位。取用靠 `tools/render_sample_sheet_v3.py:50-56` 的 `asset_root()` 两级回退(worktree → 主检出),脚本可直接取到,**样张可画**。
11. **立绘为旧档占位、且未接线进实装**:`character_hero_idle.png` 仅 1220 字节(2026-09-25),属旧档占位,非精细手绘终稿(重产工单在 `art-prompts` §5.4,属后续素材批);同时 `standee_body.gd:20-21` 立绘挂点为 `null`、`set_portrait_texture`(`:208`)未被调用,实装站桩仍是**字形回退**态。本批不修接线。
---
## 4. 设计契约(违反=返工红线)
**(1)🔴 实现批禁写 `design/` 与根规则文件**:本批唯一可写目录=`reports/`;`art-bible`、`art-prompts`、`battle.md` 等一律只读。所有「文档该改什么」进反哺点清单,由用户裁决后归文档批落文。
**(2)🔴 样张必过 §1.4 整屏四项闸**:文字对比 ≥4.5:1/明度三档带成立(含世界底层两子带)/首焦唯一/文字不压纹理底。
**(3)🔴 方向稿目检先行**:目检通过后才做机检冻结;机检过了但气质被否决的,以目检为准返工。
**(4)🔴 禁删断言、禁放宽阈值换绿**:断言红就修实现,不动判据。
**(5)🔴 禁变异类测试与撤销类取证**(2026-10-28 用户裁,前口径已废):不许写「注入缺陷看断言是否变红」的变异验证,不许写「撤销改动看断言是否变红」的撤销证明。证明方式=每个需求点与每个行为改动都要有**从真实入口进入的验收断言**,可 grep 定位、**在当轮真实跑批中执行并通过**,判据绑被测改动特有的可观测提交面。证明做不出来=**停卡呈报**。
**(6)🔴 数值单一事实源**:色值、字号一律收 `UiKit` 常量,禁在视图里写裸 `Color(...)` 与裸字号字面量(本批正是要清掉 `:441`、`:444`、`:437`、`:496` 这四处)。
**(7)🔴 48 色余量已为 0**:本批**不新增任何色值**,只使用 v3.22 在册色;需要色值处一律引 `UiKit` 现有常量。
**(8)🔴 RNG 单点**:禁 `randomize()`/自建 `RandomNumberGenerator`;样张脚本的确定性散布沿用现役写法(`standee_body.gd:275, 302`)。
**(9)🔴 Core 层零 UI 依赖**:本批只动呈现层与工具脚本,core 侧零触碰,完整战斗仍须可 `--headless --script` 跑通。
**(10)🔴 样张判定对象不含立绘精细度**:样张以现有旧档立绘出方向稿(见第 3 节第 10、11 条),判定对象=布局分区、明度三档带、文字对比、首焦唯一、构图关系;**立绘自身的清晰度与精细度不作本批判定对象**,也不得据本批样张给立绘下结论。立绘重产(`art-prompts` §5.4)落位并完成站桩接线后,须**重出样张复验**——本条进反哺点清单。
---
## 5. 范围边界
### 5.1 做
1. PIL 画一屏样张两版(1280×800 基座;1920×1080=全局等比 1.35× + 左右黑边),内容按 §1.4 整屏内容清单 + 战斗表现内容清单(含类型微光呈现组、图类型色板变体组至少两套、三组关键帧条、主角与敌方描边同屏对照)。
2. 整屏四项 + 战斗表现五项的机检断言进 `tools/render_sample_sheet_v3.py` 的 `CHECKS`。
3. 五项代码定值落码 + 断言:血条换 `#A8321F`(清 `#689CE8`)、站桩名字行垫 `HP_BAR_BACK` 素底且不压地台、顺位徽标提档 `FS_HINT`、护甲图标由字形换三枚 24×24 PNG、地台件独立引用。
4. 两件素材程序化产出并入包:地台件 `scene_stage_plaza.png`(源 48×12 九宫格)、护甲图标三枚 `ui_armor_light/ward/heavy.png`(源 24×24)。
5. 三份批件:实现报告/代拍板追认单/反哺点清单。
6. **改前/改后并排对拍图**:改动前先用同一脚本、同一参数存一版**基线样张**,改动后与基线并排成图,作为用户目检的对照材料。基线留存是真实前后对照,不是人为制造红灯,不违反「禁变异类测试与撤销类取证」红线——红线禁的是「故意改坏看断言是否变红」,本项是「改好之后与原样并排」。
### 5.2 不做(含去向)
| 不做的项 | 去向 |
|---|---|
| B 区舞台背景插画 `scene_stage_bg.png` 正式出图 | 样张内按远景/中景两带用既有色值程序化铺出**设计目标态**;正式出图工单仍在 `art-prompts` §5.1,由后续素材批执行 → 进反哺点清单 |
| 主角与三 Boss 立绘重产 | `art-prompts` §5.4 工单仍在,由后续素材批执行 → 进反哺点清单 |
| 三关色板终值 | 待「三关色相 × 五组微光互辨抽测」条目(待办清单已登记,状态=待美术设计) |
| 营地场景图 | 独立条目,与本批无关 |
| B 区高度 252→270 的实装对齐 | 本批**只登记不修**:样张按代码现状 252 出(样张须反映实机)→ 进反哺点清单 |
---
## 6. 关联文档同步面(必填)
| 文档 | 关联点(本批实现会改变它的哪条陈述) | 同步义务 |
|---|---|---|
| `design/art/art-bible.md`(v3.22) | §1.4 整屏四闸的**通过事实**与抽样读数;§4.1 站桩素底深档 `#282220` 的实装落地;§6.1/§7.2 站桩文字位机检断言 | 进 `reports/iter<N>-反哺点-<日期>.md`,由用户裁决后归文档批落文 |
| `design/art/art-prompts.md` §5 | 组二地台件、组三护甲三枚已「程序化产出即终稿」;组一背景、组四立绘仍待出图 | 同上 |
| `design/gdd/ux/battle.md` | B 区高度:文 270 vs 代码 252 的 18px 差异 | 同上 |
| `design/gdd/systems/战斗循环/战斗表现.md` | 「一屏样张的验收标准」五项过闸结果 | 同上 |
| `design/engineering/test-conventions.md` | §8.4 撤销证明条文与 2026-10-28 新验证口径相冲突(旧口径已废) | 同上 |
| `AGENTS.md` | 「验收闭合」红线句仍保留「撤销该改动能让对应断言变红」旧措辞,与新口径冲突 | 同上 |
---
## 7. 任务卡 T0..Tn
### 7.0 文件结构图
| 文件 | 职责 | 复用/新增 | 改动性质 | 验证闸行号 |
|---|---|---|---|---|
| `tools/render_sample_sheet_v3.py` | 样张绘制与机检断言宿主 | 复用既有(扩展出口与 `CHECKS`) | 行为改动 | V1–V9 |
| `game/presentation/views/standee_body.gd` | 站桩绘制:血条、素底、徽标、护甲、地台引用 | 复用既有(就地改) | 行为改动 | V5–V8 |
| `game/presentation/views/ui_kit.gd` | 色值与字号常量单点 | 复用既有(`HP_BAR_BACK`、`FS_HINT` 已在) | 非行为改动(如需补常量则标行为) | V5、V7 |
| `game/assets/art/scene/scene_stage_plaza.png` | 地台九宫格件 | 新增(程序化产出) | 非行为改动(新增素材) | V9 |
| `game/assets/ui/icons/ui_armor_{light,ward,heavy}.png` | 护甲三档图标 | 新增(程序化产出) | 行为改动(替换字形占位) | V8 |
| `game/tests/` 冒烟面 | 五项定值的验收断言落点 | 复用既有冒烟段 | 行为改动(新增断言) | V5–V8 |
| `reports/` 三份批件 | 实现报告/代拍板追认单/反哺点清单 | 新增 | 非行为改动 | — |
### 7.1 卡片与估时
| 卡 | 内容 | 估时(工作日) |
|---|---|---|
| T0 | 开工准备:worktree、`.godot` 与字体 `.import` 边车拷贝、确认 v3 脚本可跑 | 0.25 |
| T1 | 样张底板:两版画布、B/C/D 分区(数值按 `battle.md` 指路,B 区按代码 252)、格边长 | 0.5 |
| T2 | 内容清单铺画:类型微光组、色板变体组、关键帧条、描边同屏对照 | 1.5 |
| T3 | 整屏四项断言进 `CHECKS` | 1.0 |
| T4 | 战斗表现五项断言进 `CHECKS` | 1.0 |
| T5 | 血条填充换 `#A8321F`(经 `UiKit` 常量)、条底收常量 | 0.25 |
| T6 | 站桩名字行垫 `HP_BAR_BACK` 素底,且文字位不压地台(消 5px 重叠) | 0.5 |
| T7 | 顺位徽标提档 `UiKit.FS_HINT` | 0.25 |
| T8 | 护甲图标由字形换三枚 24×24 PNG | 0.5 |
| T9 | 地台件与护甲三枚程序化产出并入包 | 0.5 |
| T10 | 产出物落位卡:三份批件写齐 + 改前/改后并排对拍图产出 | 0.25 |
| | **合计** | **≈6.5(区间 5.5–7.5)** |
**卡间接口**:
- T1 **产出**:`draw_base(canvas_w, canvas_h, b_h) -> Image`(B 区高由参数传入,T2/T3 依赖)
- T3/T4 **取用**:T1 的 `Image` 与分区矩形;**产出**:`CHECKS` 内断言条目(供 T10 汇总)
- T5–T8 **取用**:`UiKit.HP_BAR_BACK`、`UiKit.FS_HINT`、`UiKit` 新增血条填充常量(由 T5 定义,T6 复用)
- T9 **取用**:T8 定义的护甲三档键 `{"light","ward","heavy"}`
**步骤要求**:一步=一个动作 + 一个可核查结果,`- [ ]` 记步。实装步骤给确切签名与 file:line,**实现体不写**。
---
## 8. 验证闸(改动—验收对照表)
| # | 需求点 | 改动(文件:行 / 行为) | 验收测试(名称 + 落点) | 层级 | 真实入口 | 判据绑定面 | 证明方式 |
|---|---|---|---|---|---|---|---|
| V1 | 样张两版产出 | `tools/render_sample_sheet_v3.py` 新增两版出口 | `sample_sheet_two_editions` 落冒烟面 | 探针 | `python tools/render_sample_sheet_v3.py` 真实跑批 | 两版位图文件尺寸(1280×800 / 1920×1080) | 产物两版尺寸分别为 1280×800 与 1920×1080,跑批通过 |
| V2 | 文字对比 ≥4.5:1 | 同上(断言进 `CHECKS`) | `splash_text_contrast` 落 `CHECKS` | 探针 | 样张脚本真实跑批 | 样张位图文字像素区域对比度读数 | 断言在跑批内取绿 |
| V3 | 明度三档带成立(含两子带) | 同上 | `splash_luma_three_bands` 落 `CHECKS` | 探针 | 同上 | 样张位图逐区明度带读数(世界底 / 中 / 顶层) | 断言在跑批内取绿 |
| V4 | 首焦唯一 | 同上 | `splash_single_focus` 落 `CHECKS` | 探针 | 同上 | 样张位图对比度最高点分布(唯一性) | 断言在跑批内取绿 |
| V5 | 血条=危险红 `#A8321F` | `standee_body.gd:444` 填充改引 `UiKit` 常量;`:441` 条底收常量 | `hp_bar_fill_is_danger_red` 落冒烟面 | 单元 | 冒烟套真实跑批 | 血条填充像素采样色值 | 断言在跑批内取绿;全仓 grep 零 `#689CE8` 与零裸 `Color(0.41, 0.61, 0.91)` |
| V6 | 站桩名字行垫素底、不压地台 | `standee_body.gd:495-497` 基线改为地台上方 + 垫 `HP_BAR_BACK` | `standee_name_not_on_plinth` 落冒烟面 | 单元 | 冒烟套真实跑批 | 名字行绘制基线 y 与地台顶 y 坐标 | 断言在跑批内取绿(名字行基线 y < 地台顶 y) |
| V7 | 顺位徽标提档 14 | `standee_body.gd:437-438` 裸 `13` → `UiKit.FS_HINT` | `order_badge_uses_fs_hint` 落冒烟面 | 单元 | 冒烟套真实跑批 | 顺位徽标字号取值(FontFile 大小) | 断言在跑批内取绿;grep 该处零裸字号 |
| V8 | 护甲图标换三枚 PNG | `standee_body.gd:8-10` 常量改路径;`:485`/`:496` 绘制改贴图 | `armor_icon_textures_bound` 落冒烟面 | 单元 | 冒烟套真实跑批 | 三档键 → 对应 PNG 资源引用(ResourceLoader 加载结果) | 断言在跑批内取绿;三档键各绑到对应 PNG |
| V9 | 地台件与护甲三枚入包 | 产出入 `game/assets/` 并被实装引用 | `plinth_and_armor_assets_loaded` 落冒烟面 | 接口 | 冒烟套真实跑批 | `game/assets` 下资源文件存在且被实装引用(尺寸 48×12 / 24×24) | 断言在跑批内取绿(资源可加载且尺寸 48×12 / 24×24) |
| V10 | 改前/改后对拍图产出 | T1 存基线样张;T10 生成并排图 | `sample_sheet_before_after` 落冒烟面 | 探针 | 样张脚本真实跑批 | 基线版与改后版两张源样张文件 + 并排图输出文件 | 断言在跑批内取绿:基线版与改后版两张源文件均存在、尺寸一致、并排图已生成 |
- **方向稿目检**(用户判定)在 V1–V4 之前执行,目检通过后才冻结机检。
- **独立验收审计**:入口 `reports/iter<N>-验收审计-<日期>.md`,由未参与实现的会话或子代理执行,未过不得合并。
- **用户目检项一律「登记不阻塞」**:登记进实现报告即完成卡内义务,不作为合并或下一迭代开工的前置。
- 重闸表述:批末按本批改动域选档跑 `tools/run_all_gates.sh`(本批含代码面,走代码域)。
---
## 9. 异常分级
- **L1 自救**:单条断言红且定位明确 → 一次重试修实现,不改动判据。
- **L2 记录继续**:同一红灯连续三次 → 记录进实现报告,继续推进其余卡,不停批。
- **L3 停机写报告等用户**:触碰第 4 节任何一条 🔴 红线;或样张四闸中任一闸无论如何都过不去(可能意味着规格本身有缺口);或需要新增色值(余量为 0,须呈报作者裁)。
---
## 10. 代拍板白名单
1. 样张脚本内的临时变量命名、辅助函数拆分方式。
2. 断言在 `CHECKS` 内的 id 命名与登记顺序。
3. 程序化产出素材时的绘制实现细节(颜色取自 v3.22 在册值、不新增色)。
4. 两版样张的输出文件名与落盘目录(落 `artwork/` 在途区)。
超此清单者一律呈报。
---
## 11. 待裁点
**零待裁点,开工门=U0。**
说明:B 区高度文 270 vs 代码 252 的差异已在第 5.2 节定死处置(本批按代码 252 出样张、只登记不修),不构成待裁点;样张所需色值全部已在 v3.22 在册,本批零新增色,故无额度类分叉。
---
## 12. 落位与可复用性预检
**复用面**:
- 样张绘制**复用既有** `tools/render_sample_sheet_v3.py`(1174 行,已有画布、分区、`CHECKS` 框架),不新建脚本——新脚本会与既有断言体系并行,形成两套事实源。
- 色值与字号**复用既有** `UiKit` 常量(`HP_BAR_BACK`、`FS_HINT`、血条填充新常量同址登记),禁在视图内写裸字面量。
- 站桩绘制**就地改** `standee_body.gd`,不新建站桩视图。
**唯一面**:
- 血条填充色只落在 `UiKit` 一处常量,`:444` 与 `:441` 两处绘制点均引它,禁各自写字面量。
- 护甲三档键 `{"light","ward","heavy"}` 只在 `standee_body.gd` 常量处定义一次,`:485` 与 `:496` 共用。
- 地台几何已在 `:405` 单一来源(注释明写「防两处漂移」),本批不另立第二处几何。
**分层与交互面**:
- 样张脚本(`tools/`)→ 只产出图与断言报告,不进游戏运行时;与呈现层的交互=读取 `UiKit` 常量值,只读不写。
- 呈现层(`standee_body.gd`)→ 经 `UiKit` 取色与字号,不直连数据表;数值只经 ConfigDB 单点(本批无玩法数值改动)。
- 新增素材(PNG)→ 落 `game/assets/` 被呈现层按路径加载,路径串只在常量处定义一次。
- 跨层红线不因新增而绕:Core 零 UI、随机只经 RngService(本批无随机改动)、数值只经 ConfigDB。
@@ -19,7 +19,7 @@
- `reports/设计-战斗编排类变量归约-2026-10-29.md`——设计讨论段定稿全文。逐成员去向(38 项五去向)、两新件形态与耦合边界、attune 重裁决、已否决切法、随迁键义务、行为零变化风险点、键账预期、改址盘点,全部以该文档对应节为唯一合同;本卡不复制其表格,T0 逐名对账时引用。
- `design/engineering/architecture/architecture.md` v1.25——「战斗编排域拆件定稿」节轴丁两件(RoundCycle/BattleFacts,跨件单向接线、段类随动、attune 改判、S13 键账收敛义务、验收红线)与「模块归属·功能层」两行、「测试与质量域」S13 豁免第三条注销句——实现照册施工。
- 五待裁点裁决(2026-10-29 用户裁,对应设计文档 §13):①encounter 归战斗事实件=采纳;②treasure_inventory 注入改址=追认(「外部赋值路径零改址」句失效);③attune 重裁决=采纳「状态归 facts、行为留宿主」;④完美账写口升级=采纳(grant_segment 三处直写改经 facts 三方法);⑤PARITY 对拍批准=批内实跑须用户点头、不重锚。
- 五待裁点裁决(2026-10-29 用户裁,对应设计文档 §13):①encounter 归战斗事实件=采纳;②treasure_inventory 注入改址=追认(「外部赋值路径零改址」句失效);③attune 重裁决=采纳「状态归 facts、行为留宿主」;④完美账写口升级=采纳(grant_segment 三处直写改经 facts 三方法);⑤对拍闸已随 2026-10-30 工程批退役(用户裁:整局重放对拍不再作验收载体)——原 PARITY 批准待裁点就此注销。
## 3. 现状事实(代码级,2026-10-29 实查;开工 T0 按当轮 master 重测复核)
@@ -33,7 +33,7 @@
- attune 外部三面(零改址标的):`attune_has` 公有面消费=table_structure_guard_test 起战断言(:162/:166)与 breakpoint_resume_test;`attune_param_sum`=main.gd:567 与 treasure_effects_test;`attune_convert_state`=treasure_interact_controller.gd:125。三面全部留宿主公有面。
- 冻结面:`nest_preview_count` 外部直读=main.gd:536-537+treasure_effects_test:153;`round_index` 公有直读=system_test.gd:447-462(st.round_index)等四套件约 15 处。
- **设计文档 §11 漏列一处(T0 实查补入)**:`settle_segments_test.gd:27` 接线断言 `oc._grant_seg != null and oc._grant_seg.oc == oc`——段件迁 RoundCycle 后必红,须改指 `oc._rounds._grant_seg`。
- `tools/run_all_gates.sh` PARITY_TOUCH_FACES(:161 定义区)含 `game/core/battle_orchestrator.gd`——本批必触面。
- 对拍闸(原闸 11 与 PARITY_TOUCH_FACES 触面判定)已随 2026-10-30 工程批退役——本批无触面批准事项;T0 复核以此为准。
- `game/tests/static_scan.sh`——S8 await 文件集(:317,9 文件)与 S7 B1 域文件集(:359,9 文件)均未含两新件路径;round_cycle.gd 经 oc 调 B2 符号(`_enemy_system` 族)须随批增行,battle_facts.gd 零协作调用不入清单。
## 4. 设计契约(违反即返工的红线条款)
@@ -43,7 +43,7 @@
- 🔴 **信号顺序契约冻结**:五信号(step_row/battle_finished/round_started/board_changed/settlement_snapshot)留宿主;发射语句零迁移、发射时序零变化;A2 三 connect(:251-254)逐字。
- 🔴 **两快照出口签名与载荷逐字不变**:presentation_snapshot/diagnostic_snapshot 字段名、嵌套结构、值口径逐字——内部读数改指两件投影后值恒等(round_index/attune_safe_seen/完美三账/hero_atk 派生读均受理后不变量)。
- 🔴 **明细行协议词表零触碰**:基础 9 类+扩展行词表、SOURCE_EXEMPT_TYPES/SOURCE_EXEMPT_CLASSES 两常量、`_emit_row`/`_assert_row_source` 行发射基建留宿主零迁移。
- 🔴 **PARITY 触面**:battle_orchestrator.gd 触 PARITY_TOUCH_FACES——实跑对拍须经用户批准、不得借机重锚。
- 🔴 **行为零变化承载(对拍闸退役后)**:同种子逐位一致由关联套件的既有回放断言与两快照恒等断言承载;对拍闸(原闸 11)已随 2026-10-30 工程批退役,无闸级对拍批准事项。
- 🔴 **拆分边界纪律**(判据权威=code-conventions §1.1,此处立卡内执行条款):①每个拆出单元须有可陈述的职责语句与单一变化原因(实现报告逐件一行承载);②拆出单元须有集体身份——命不出名字、只能说「剩下的成员」的打包=凑计数袋,禁;③「过阈值」只许作为发现信号,作为拆分或收拢的动机即违规;④按纪律拆不出内聚单元 → 停卡呈报走豁免/还债通道,禁硬拆凑绿;⑤拆分沿职责轴不沿阈值轴。两新件职责语句见第 8 节(RoundCycle=轮结算周期的推进结构;BattleFacts=战斗事实的记录与查询口径)。
- 🔴 **禁为机检文本保留无效成员**:22 成员随件迁出+treasure_inventory/_hero_atk 两成员退役=宿主删 24 项(37−22−2+RoundCycle/BattleFacts 两新件接线引用=宿主 15,账平——grilling 复核订正原算术),禁保留别名、别名转发属性或空壳;机检键与卡片契约互斥时,实现段一律停卡呈报,不得自造第三条路。
- 🔴 **随迁键收敛义务**:迁移函数逐字纪律——`roll_round_actions`(45 行/分支≈8 贴线)、`settle_if_due`、`reset_round`、三 mark 方法迁移与改指**禁顺手增分支/局部**;函数迁入新文件若出超标键=卡内同步收敛(拆助手函数,拆分不改语义;先例=迭代 40 卡① 12 随迁键收敛),禁带新键进批末闸。
@@ -157,7 +157,7 @@ aura/decay/verdict 三段、battle_fx_host、reward_grant、damage_controller、
| 行 | 需求点 | 改动(文件:行/行为) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 能力行 |
|---|---|---|---|---|---|---|---|
| V0 | 行为零变化(本批总判据) | 全部结构改动(第 7 节清单) | PARITY 同种子完整局对拍(`tools/parity_anchor.py`,用户批准后实跑、禁重锚);关联套件零改址应绿 | 集成+对拍 | 真实局 begin_battle→挖掘→结算→终局 | step_row/settlement_snapshot 逐位一致——差异非零即停机呈报 | 既有:回放确定性(同种子三变体逐位) |
| V0 | 行为零变化(本批总判据) | 全部结构改动(第 7 节清单) | 关联套件零改址应绿(对拍闸已随 2026-10-30 工程批退役——总判据由 V1–V5 各行能力行并集承载);静态判据申报同上(键账收敛与 S7/S8 两清单增行) | 集成 | 真实局 begin_battle→挖掘→结算→终局 | 关联套件全绿且改址面零失败——出红且无改址归因即停机呈报 | 既有:轮内流与结算五段全序;既有:技能池数据面与回放独立;既有:伤害公式与掷点乘区;既有:秘宝结构冻结与 12 件效果 |
| V1 | 两新件建件接线(T1/T2) | battle_facts.gd/round_cycle.gd 新建;宿主 _init 建件接线;五段构造迁 round_cycle | 新增 settle_segments_test.gd 接线检查段内嵌〔编排归约 V1〕:`oc._rounds` 非空且 `_rounds.oc == oc`;五段段件齐在 `_rounds`;`oc._facts` 非空 | 单元 | BattleOrchestrator 直构(settle_segments_test 现行夹具同款) | 编排器接线面(新件在座与宿主回指——接线断言本体) | 既有:轮内流与结算五段全序 |
| V2 | reset_round 复位边界(险点) | battle_facts.reset_round 只清三缓冲;宿主三调用点改调 | 新增 round_flow_skill_pool_test.gd:_smoke_full_battle 内嵌〔编排归约 V2〕:终局后 `presentation_snapshot()["refresh_used"]` == 末轮结算 snapshot `economy.refresh_used` | 单元 | begin_battle→轮内刷新→停手→结算→终局 | 两快照同键值恒等(复位不碰 refresh_used 的可观测提交面) | 既有:轮内流与结算五段全序;既有:停手转存储备与轮内刷新(上限/AP 不重置/刷新图零清图奖励) |
| V3 | 段驱动与摇号迁移(重入门闸/settle_if_due/roll_round_actions/mines_hits 账) | 三函数迁出宿主;宿主调用点改址;trigger_cond_test 3 处改指 | 复用 round_flow_skill_pool_test 全套(_smoke_full_battle/_smoke_settlement_segments/_smoke_skill_pool_replay_independence)、settle_segments_test V4-3 真实一轮、skill_unlock_test、trigger_cond_test(改指后应绿)——零新增断言 | 单元 | begin_battle;stop→WAITING→settle_if_due | 五段裁定行序+settling 闸+终局早退+同种子摇定逐位(behavior_roll 流随件零漂移由既有回放断言钉住)+mines_hits 转存读数 | 既有:轮内流与结算五段全序;既有:技能池数据面与回放独立;既有:Boss 半血解锁技门控 |
@@ -177,13 +177,13 @@ aura/decay/verdict 三段、battle_fx_host、reward_grant、damage_controller、
## 13. MC/数值与 RNG 零触碰声明
本批零数值改动、零数据表改动、零 RNG 流词表改动、零玩法字面量引入——结构整改不改数值与 RNG 消费序。behavior_roll 封闭流随件迁移取号序不变(流名、开流时点、取号序逐字)。数值与 RNG 零触碰则 MC 零跑;battle_orchestrator.gd 触 PARITY_TOUCH_FACES——实跑对拍须经用户批准、不得借机重锚(批末闸 11 判 NEED-APPROVAL 时由用户裁决:批准实跑或裁跳过,卡内不定案)。
本批零数值改动、零数据表改动、零 RNG 流词表改动、零玩法字面量引入——结构整改不改数值与 RNG 消费序。behavior_roll 封闭流随件迁移取号序不变(流名、开流时点、取号序逐字)。数值与 RNG 零触碰则 MC 零跑;对拍闸已随 2026-10-30 工程批退役,无闸级对拍事项。
## 14. 异常分级
- **L1 自救**:改指漏改编译错、测试寻址漏改——就地修复重跑最窄确认。
- **L2 记录继续**:实测行数/改指处数与设计文档「约」值区间偏离、头注指址微调——记入实现报告「实现决定记录」。
- **L3 停机写报告等用户**:①机检预算互斥——两新件实数超第 9 节预算、BattleFacts 需增员、或迁移函数现出超标键=上游缺陷停卡退回写卡段(禁自造第三条路);②encounter 归属被实现期证伪——受理事实与轮 facts 复位节奏冲突实锤、且设计文档 §6.3 两条砍路均破冻结面(宿主 16 不可达)=停卡呈报走判据④通道;③T0 对账不符(成员去向对不上、基线出现函数级键、file:line 漂移过大);④V0 对拍差异非零(行为零变化被证伪);⑤PARITY 触面处置分歧;⑥断言面被下游兜底吸收致判据绑不上(按 test-conventions §8.4 出口呈报);⑦S13 键恶化或基线外新键。
- **L3 停机写报告等用户**:①机检预算互斥——两新件实数超第 9 节预算、BattleFacts 需增员、或迁移函数现出超标键=上游缺陷停卡退回写卡段(禁自造第三条路);②encounter 归属被实现期证伪——受理事实与轮 facts 复位节奏冲突实锤、且设计文档 §6.3 两条砍路均破冻结面(宿主 16 不可达)=停卡呈报走判据④通道;③T0 对账不符(成员去向对不上、基线出现函数级键、file:line 漂移过大);④V0 行为零变化被证伪(关联套件出红且无改址归因);⑤断言面被下游兜底吸收致判据绑不上(按 test-conventions §8.4 出口呈报);⑥S13 键恶化或基线外新键。
## 15. 测试削弱申报(允许改动的旧断言及理由)
@@ -191,7 +191,7 @@ aura/decay/verdict 三段、battle_fx_host、reward_grant、damage_controller、
## 16. 独立验收审计
合并前由未参与实现的会话或子代理执行;报告落 `reports/iter<N>-验收审计-<日期>.md`。审计至少做四件事(test-conventions §8.7):逐行核对第 11 节对照表(测试存在、真执行、尾缀可 grep、真实入口未绕过改动点);抽查判据绑定面(V2 两快照同键、V4 攻击单基值、V5 引用同一性——当轮真实跑批执行过);检查测试削弱(对照第 15 节零申报);核对全部「非行为改动」判定依据(零漂移证明是否在当轮真实跑批中成立、PARITY 批准凭据在案)。未过不得合并。
合并前由未参与实现的会话或子代理执行;报告落 `reports/iter<N>-验收审计-<日期>.md`。审计至少做四件事(test-conventions §8.7):逐行核对第 11 节对照表(测试存在、真执行、尾缀可 grep、真实入口未绕过改动点);抽查判据绑定面(V2 两快照同键、V4 攻击单基值、V5 引用同一性——当轮真实跑批执行过);检查测试削弱(对照第 15 节零申报);核对全部「非行为改动」判定依据(零漂移证明是否在当轮真实跑批中成立)。未过不得合并。
## 17. 代拍板白名单(允许 agent 自行拍板的微决策)
@@ -1,36 +1,36 @@
# 需求卡片——测速循环批(tune_sim_controller 循环内 await 真实消除)
> **版本状态**:v1.0 定稿——零待裁点(2026-10-29 用户裁:信号步进状态机形态、收尾置假还原作为已申报行为改动、双实现对拍入批、错误路径差异接受并文档化)。
> **版本状态**:v1.1 定稿——零待裁点(2026-10-30 停卡重裁:实现段 T0 三重证据定谳 v1.0 的「潜伏红」前提已被迭代 41 测试债批消化〔215da315——运行态收尾归还 run〕,按停卡呈报建议案剥离已申报行为改动 V3、T0 义务改现状复核、夹具申报差异两处减一处;2026-10-29 原四点裁决中②作废,①③④仍有效——裁决记录见第 15 节)。
> **接收方**:待办清单条目「零豁免五键清偿批——S13 余量五键真修四卡(2026-10-29 登记,来源=会话提及)」——本卡为其四卡中的测速循环批卡。
> **来源**:reports/设计-测速循环去await-2026-10-29.md(2026-10-29 设计闭合)。**唯一事实源**:`reports/设计-测速循环去await-2026-10-29.md` + `design/engineering/architecture/architecture.md` v1.25「测试与质量域」S13 测量面豁免注销句(2026-10-29 零豁免裁:原循环内 `await` 豁免〔2026-10-27〕注销——tune_sim_controller 改 process_frame 信号常驻连接+步进状态机,每帧一局、帧相位逐点对齐;随批申报行为改动一处:运行态收尾由「每局置假」还原为「整批收尾单次置假」,消除与 wd8/ui_tune_probe 断言面的静态矛盾)。
> **来源**:reports/设计-测速循环去await-2026-10-29.md(2026-10-29 设计闭合;§2.3 潜伏红推演与 §10 待裁点 1 的「还原」申报已随停卡重裁作废,§3–§7 形态与等价性论证仍有效)。**唯一事实源**:`reports/设计-测速循环去await-2026-10-29.md` + `design/engineering/architecture/architecture.md` v1.25「测试与质量域」S13 测量面豁免注销句(2026-10-29 零豁免裁:原循环内 `await` 豁免〔2026-10-27〕注销——tune_sim_controller 改 process_frame 信号常驻连接+步进状态机,每帧一局、帧相位逐点对齐;注销对象是 await 豁免、不依赖行为改动申报,句保持;句内「随批申报行为改动一处」半句已失效,随实现批现状化时一并修正——见第 6 节)+ 停卡呈报三重证据(iter44 worktree 分支 `reports/iter44-实现报告-2026-10-30.md` §5——重裁依据)。
## 1. 目标与完成定义
这批做什么:把 S13 静态质量闸(gdstyle)在册裸键 `game/presentation/tune_sim_controller.gd|quality/await-in-loop` 真实消除——`run(n)` 的 for 循环内 `await process_frame` 改为 process_frame 信号常驻连接+步进状态机(每帧回调执行一局、步尽次帧收尾断连),`run()` 只留函数级一次 `await _batch_finished` 保持协程身份;改后全文件零 for/while,该键从文本形态根除、此文件对这个键结构性免疫。随批执行两项已裁事项:把「每局置假」还原为「整批收尾单次置假」(已申报行为改动——消除与两个在库断言面的静态矛盾);新增双实现对拍套件,把帧相位等价论证变成当轮真实执行证据。
这批做什么:把 S13 静态质量闸(gdstyle)在册裸键 `game/presentation/tune_sim_controller.gd|quality/await-in-loop` 真实消除——`run(n)` 的 for 循环内 `await process_frame` 改为 process_frame 信号常驻连接+步进状态机(每帧回调执行一局、步尽次帧收尾断连),`run()` 只留函数级一次 `await _batch_finished` 保持协程身份;改后全文件零 for/while,该键从文本形态根除、此文件对这个键结构性免疫。随批执行一项已裁事项:新增双实现对拍套件,把帧相位等价论证变成当轮真实执行证据。**本批零行为改动**——收尾置假已在 run() 尾每批一次执行(迭代 41 测试债批 215da315 归还后的现行形态),本批收尾四语句同帧同相位迁入 `_finish_batch()`,语句集与语义零变化(第 3、4 节)。
什么状态算完:
1. `tune_sim_controller.gd` 全文件零循环、`await` 仅 `run()` 函数级一处;`python tools/gdstyle_gate.py` 对账该裸键消失(消失键不参与红判定)、无基线外新键。
2. 测量语义逐点等价(第 4 节四判据)经双实现对拍在当轮真实跑批中证明——五面对拍全等;wd8 三断言与 ui_tune_probe ⑤ 零改动转绿。
3. diff 中已申报行为改动恰一处(收尾置假还原,对照表 V3 行),无第二处行为改动。
2. 测量语义逐点等价(第 4 节四判据)经双实现对拍在当轮真实跑批中证明——五面对拍全等;wd8 三断言与 ui_tune_probe ⑤ 零改动保持绿。
3. diff 中行为改动零处——收尾四语句迁移为非行为改动(同帧同相位、语义零变化,对照表 V3 行),无任何行为漂移。
4. 脚本错误中断路径差异作为声明缺口写入实现报告(禁静默)。
5. 批末代码域闸全绿;实现报告与反哺点清单写齐方算收口。
## 2. 设计事实源
设计依据唯一出处=`reports/设计-测速循环去await-2026-10-29.md`(2026-10-29 设计闭合,零待裁点):§4 推荐案(process_frame 信号步进状态机伪代码结构)、§5 等价性证明清单(四判据+已声明缺口)、§6 键账预期、§7 行为零变化风险点。架构面凭据=`design/engineering/architecture/architecture.md` v1.25「测试与质量域」S13 测量面豁免注销句(2026-10-29 零豁免裁落文)——豁免注销、行为改动申报面与本卡一致。待办清单条目「零豁免五键清偿批」状态=文档设计已闭合+待写卡(本卡即其中一卡的写卡段产出)。
设计依据唯一出处=`reports/设计-测速循环去await-2026-10-29.md`(2026-10-29 设计闭合,零待裁点):§3 形态评估(process_frame 信号步进状态机采纳)、§4 推荐案(伪代码结构)、§5 等价性证明清单(四判据+已声明缺口)、§6 键账预期——以上各节不受前提消化影响、仍是本卡依据;该文档 §2.3 潜伏红推演与 §10 待裁点 1「还原」申报已作废(前提消化证据见停卡呈报)。架构面凭据=`design/engineering/architecture/architecture.md` v1.25「测试与质量域」S13 测量面豁免注销句(2026-10-29 零豁免裁落文)——豁免注销句保持,句内行为改动申报半句随实现批现状化修正(第 6 节同步面)。前提消化与重裁凭据=iter44 worktree 分支 `reports/iter44-实现报告-2026-10-30.md` §5 停卡呈报(git 历史证据 215da315+T0 两面实跑绿)。待办清单条目「零豁免五键清偿批」状态=已写卡+已编迭代 44(本卡 v1.1 重裁段产出)。
## 3. 现状事实(代码级,2026-10-29 实查;开工 T0 按当轮 master 重测重锚——行号基线为本日 master)
## 3. 现状事实(代码级,2026-10-30 实查;开工 T0 按当轮施工基面重锚——行号基线为本日 master)
- `game/presentation/tune_sim_controller.gd`(全件 68 行):`run(n)` 在 :36-52——:37 置运行态真、:40-42 开共享 JSONL 日志会话、:43 `panel.set_sim_running(true)`、:44 进度 0/n、:45 建 stats 六键束、:46 取自动策略驱动、:47-50 for 循环体(`_run_one_level` → 进度串 → :50 `await panel.get_tree().process_frame`——命中键的语句)、:51-52 收尾(close_session+报告注入)。`_run_one_level` 在 :56-68(5 参),**尾部两行 :67 `panel.set_sim_running(false)`/:68 `_running = false` 每局执行一次**(潜伏红本体,见下)。类成员 5(db/panel/_base_hp/_driver/_running)+常量 2(:7 SEED_BASE=4200、:8 PlaytestLogger preload);头注 4 行。
- 断言面一〔wd8〕`game/tests/unit/presentation/standee_fx_sweep_test.gd:126-134`:FakePanel(内类 Node,:20-40——setter 面并入树供 get_tree)+FakeDriver(:44-55——`run_level` 回调内读 `ctrl.is_running()` 逐局记 `seen`);:129 直构控制器(字典夹具 db)、:131 `await ctrl.run(2)`(run 必须可等待、挂起至整批完成的契约)、:132-134 三断言——`drv.seen == [true, true]`/收尾 `not ctrl.is_running() and not panel.running`/报告非空。套件在 run_all.gd manifest 在册(res://tests/unit/presentation/standee_fx_sweep_test.gd,expect 0)。
- 断言面二〔探针 ⑤〕`game/probe/ui_tune_probe.gd:63-83`::65-66 设滑条与 N=10、:67 `press_simulate_for_probe()`(真实按钮 pressed 信号链)、:68-71 守卫环(`while main.debug_overlay.is_running()` 逐帧等——SIM_GUARD_FRAMES=1800 在 :16)、:72-73 预算内未完成判红、:74-83 输出四串断言(关卡完成率/关卡完成剩余 HP 均〔口径换代 V7〕/败北节点/N=10)。面板语义面 `debug_overlay.gd`:`set_sim_running` :81-83 → `_sync_sim_button` :107-110(模拟与两层调试两钮禁用+「模拟中…」文案——浮窗运行态贯穿整批的可见面)、`is_running` :77-79。
- 调用面 `game/presentation/main_debug_flow.gd`:`_on_tune_simulate` :72-77——守卫(活跃结算/冒险早退)后 `root._tune_sim.run(n)` **不 await 调用**(返回的协程态丢弃——新形态必须保持此调用形态可用、零改动);`_start_debug_levels` :84 守卫读 `root._tune_sim.is_running()`(两层调试入口互查守卫)。
- 静态矛盾推演(设计文档 §2.3——T0 实跑定谳)::67-68 每局执行 → wd8 首局完成即把两个运行态置假 → `drv.seen` 实测 `[true, false]` 必红;探针按下返回后首局已在同一同步段内跑完、运行态已假 → 守卫环立即退出 → 输出停在进度串 → 四串断言全红。推断这两行原在循环之后(「循环体提函数」搬运时被卷入提取体——设计文档 §2.3),wd8 写于 d8 批且当时必绿可反证;两面均不在近期批次的关联档选测覆盖内(套件名与涉案文件无字面关联、probe_gate --full 非恒跑三保险),潜伏红因此未暴露。
- 键账:`tools/gdstyle_baseline.txt:15` = 该文件唯一在册键(await-in-loop 裸键,基线 16 键之一)。S13 扫描面=产品码三目录(game/core+data+presentation——probe/sim/tests 不在面,`game/tests/static_scan.sh` S13 头注在案);S8 文件集=九文件显式清单(static_scan.sh :317——不含本件);S9/S10 扫描面同不含 game/tests/。
- `game/presentation/tune_sim_controller.gd`(全件 68 行):`run(n)` 在 :36-54——:37 置运行态真、:40-42 开共享 JSONL 日志会话、:43 `panel.set_sim_running(true)`、:44 进度 0/n、:45 建 stats 六键束、:46 取自动策略驱动、:47-50 for 循环体(`_run_one_level` → 进度串 → :50 `await panel.get_tree().process_frame`——命中键的语句)、:51-54 收尾四语句(`close_session()` :51 → 报告注入 :52 → `panel.set_sim_running(false)` :53 → `_running = false` :54)——**收尾置假在 run() 尾、for 循环外、每批恰一次**。`_run_one_level` 在 :57 注释/:58-68(5 参),尾部 :66-68 为败局节点记账(defeat 分支)——非置假。类成员 5(db/panel/_base_hp/_driver/_running)+常量 2(:7 SEED_BASE=4200、:8 PlaytestLogger preload);头注 4 行。
- 前提消化定谳(v1.0「静态矛盾推演」作废依据):git 历史——迭代 41 测试债批提交 215da315(2026-10-29 合回)提交信息明载「两个产品回归随批修(……tune_sim 运行态收尾归还 run)」,收尾置假两行由该批归还 run() 尾并随该批申报;v1.0 成文(2026-10-29)与该批合回同日,其「现状考证」被上游批施工消化变质(读法 A;读法 B「考证当时即误读」与 215da315 提交信息矛盾,停卡呈报 §5 不采)。T0 实跑三重证据(iter44 worktree,停卡呈报 §5):wd8 三断言(现位 :145-147)全 PASS(套件 18 checks 全绿);ui_tune_probe 全链路 PASS(⑤ 守卫环 1800 帧预算内退出+输出四串在)。**潜伏红不存在**——两面现行全绿,「零改动转绿」改「零改动保持绿」。
- 断言面一〔wd8〕`game/tests/unit/presentation/standee_fx_sweep_test.gd:139-147`:FakePanel(内类 Node,:20-40——setter 面并入树供 get_tree)+FakeDriver(:44-55——`run_level` 回调内读 `ctrl.is_running()` 逐局记 `seen`);:142 直构控制器(字典夹具 db)、:144 `await ctrl.run(2)`(run 必须可等待、挂起至整批完成的契约)、:145-147 三断言——`drv.seen == [true, true]`/收尾 `not ctrl.is_running() and not panel.running`/报告非空。套件在 run_all.gd manifest 在册(res://tests/unit/presentation/standee_fx_sweep_test.gd,expect 0,manifest :188)。
- 断言面二〔探针 ⑤〕`game/probe/ui_tune_probe.gd:63-83`::65-66 设滑条与 N=10、:67 `press_simulate_for_probe()`(真实按钮 pressed 信号链)、:68-71 守卫环(`while main.ui.modals.debug_overlay.is_running()` 逐帧等——SIM_GUARD_FRAMES=1800 在 :16)、:72-73 预算内未完成判红、:74-83 输出四串断言(关卡完成率/关卡完成剩余 HP 均〔口径换代 V7〕/败北节点/N=10)。面板语义面 `debug_overlay.gd`:`set_sim_running` :81-83 → `_sync_sim_button` :107-110(模拟与两层调试两钮禁用+「模拟中…」文案——浮窗运行态贯穿整批的可见面)、`is_running` :77-79。
- 调用面 `game/presentation/main_debug_flow.gd`:`_on_tune_simulate` :72-77——守卫(活跃结算/冒险早退)后 :77 `root._tune_sim.run(n)` **不 await 调用**(返回的协程态丢弃——新形态必须保持此调用形态可用、零改动);`_start_debug_levels` :84 守卫读 `root._tune_sim.is_running()`(两层调试入口互查守卫)。
- 键账:`tools/gdstyle_baseline.txt:15` = 该文件唯一在册键(await-in-loop 裸键,基线 16 键之一)。S13 扫描面=产品码三目录(game/core+data+presentation——probe/sim/tests 不在面,`game/tests/static_scan.sh` S13 头注在案);S8 文件集=**十文件**显式清单(static_scan.sh :317 现行实查——不含本件);S9/S10 扫描面同不含 game/tests/。
- 产品码 process_frame 订阅面(T0 实查项):本件 :50(循环内 await)+`settlement_fx_view.gd:94`(play_damage_number 内一次性 await 等文本度量——战斗结算呈现期,与模拟批不并存)。现库无「run 途中新增订阅者」场景。
- 报告渲染面 `game/presentation/hud_presenter.gd` :197-218 `tune_sim_report`(静态函数,零触碰计划):空数组 `_mean`/`_p90` 双守卫(:221-227/:230-235)、完成率分母 `maxi(1, n)`(:206)——n=0 退化路径渲染安全(本批对拍 n=0 案的前提,实查在案)。
- 能力表现状:`game/tests/README.md` 能力表零 tune_sim/调试浮窗行;锚到本批断言宿主套件的唯一既有行=「站桩敌人呈现与结算演出落点」(代表用例 `standee_fx_sweep_test.gd:_init`——wd8 断言所在函数)。
- 能力表现状:`game/tests/README.md` 能力表「调参测速批量模拟(逐帧步进与统计读数)」行已入表(:131——写卡段 v1.0 同批义务,真实入口=批量模拟入口,代表用例=对拍套件);断言宿主 wd8 套件锚既有行「站桩敌人呈现与结算演出落点」(:130)。
## 4. 设计契约(违反即返工的红线条款,标 🔴 触碰=L3 停机呈报)
@@ -40,30 +40,30 @@
③ 终止条件与终止时读数不变——终止=计数走满 n;close_session/运行态复位/报告注入在同帧执行;
④ 输出逐字不变——报告=未触碰的 `HudPresenter.tune_sim_report`;stats 是 (种子, atk_pct, hp_pct) 的纯函数、帧时点不进测量值;JSONL 由未触碰的自动策略逐行写、close 时点同帧。
违反即返工;等价性对拍失败=L3 停机呈报。
- 🔴 **行为改动唯一=收尾置假还原**(已申报——对照表 V3 行):`_run_one_level` 尾部两行(:67-68)删除,运行态复位收进 `_finish_batch()` 收尾单次执行;禁任何其他行为漂移——`_on_tune_simulate` 不加运行态早退守卫(设计文档待裁点 3 备选否决)、发布态零触碰、探针①–④⑥–⑩ 段零触碰、`HudPresenter`/`PlaytestLogger`/`sim/level_autoplay.gd`/`debug_overlay.gd` 零触碰。
- 🔴 **行为改动零处(收尾语义零变化)**:收尾置假已在 run() 尾每批一次执行(:53-54,215da315 归还后的现行形态)——本批 `_run_one_level`(:58-68)零触碰、运行态复位语义零变化;收尾四语句(:51-54)迁入 `_finish_batch()` 为同帧同相位语句迁移(判据③承载——非行为改动,对照表 V3 行)。禁任何行为漂移——`_on_tune_simulate` 不加运行态早退守卫(设计文档待裁点 3 备选否决)、发布态零触碰、探针①–④⑥–⑩ 段零触碰、`HudPresenter`/`PlaytestLogger`/`sim/level_autoplay.gd`/`debug_overlay.gd` 零触碰。
- 🔴 **脚本错误中断路径差异=声明缺口**(设计文档 §5 缺口 1,接受并文档化):GDScript 无错误陷阱 API、差异不可消除——旧形态步进体内脚本错误=协程死亡静默停摆(运行态永真、报告永不落);新形态=计数未推进、次帧重试同一步(确定性错误逐帧重试,探针 1800 帧守卫可抓;生产态=持续重试的可见坏,与旧「静默卡死」同级不同形)。实现报告文档化义务(T3),禁静默、禁造第三条路(错误陷阱/守卫兜底均已否决);测量批遇错本就整批作废,两形态都不产出可用读数。
- 🔴 **S8 结算路径 await 零命中面不触碰**:本件不在 S8 文件集(九文件显式清单实查不含本件);本批零触碰 S8 文件集与一切闸件。
- 🔴 **S8 结算路径 await 零命中面不触碰**:本件不在 S8 文件集(十文件显式清单实查不含本件);本批零触碰 S8 文件集与一切闸件。
- 🔴 **拆分边界纪律**(判据权威=code-conventions §1.1,卡内执行条款):本批零拆出单元——步进状态机收编本件(职责语句与不拆理由见第 8 节);①拆出单元须有可陈述职责语句与单一变化原因;②命不出名字、只能说「剩下的成员」的打包=凑计数袋,禁;③「过阈值」只作发现信号——本批键消除靠形态根除、不靠拆件凑数;④按纪律拆不出内聚单元→停卡呈报;⑤禁沿阈值轴拆。
- 🔴 **RNG 与种子零触碰**:SEED_BASE=4200 与逐局种子切分(SEED_BASE+i)零变化——stats 是种子纯函数是等价性论证的前提。
- 🔴 **注释纪律**:新增头注/注释零变更叙事(「由 await 改为信号」类句禁——沿革归 git);S11 注释禁词由术语表解析供给,新增注释用白话描述现行形态与信号契约。
## 5. 范围边界
做什么:第 3 节四件事——T0 现状定谳(实跑两断言面)、`tune_sim_controller.gd` 步进状态机改造+收尾还原、双实现对拍套件、键账对账与收口。
做什么:第 3 节四件事——T0 现状复核(两面实跑应绿)、`tune_sim_controller.gd` 步进状态机改造(收尾语义零变化)、双实现对拍套件、键账对账与收口。
不做什么(明示去向):
- 零豁免其余四键(main.gd 类变量 49、battle_orchestrator 类变量 38、board_cell_unit 30、standee_body 19)——各归组合根注册面批/战斗编排归约批/呈现层状态件批(同条目四卡,待办清单在册)。
- `tools/gdstyle_baseline.txt` 禁实现批中途刷新——批末由用户人工重生成(第 6 节)。
- `_on_tune_simulate` 运行态早退守卫——不收件(+1 行为面、system_test 前置断言随之需复核,代价大于收益,设计文档待裁点 3 否决)。
- `design/` 整个目录、两份根流程清单——实现批禁写;该改的点列反哺点清单。`game/tests/README.md`:测量面专属能力行已随本卡写卡段同批入表(「调参测速批量模拟」行——写卡同批义务,grilling 复核订正),实现批禁写红线不变。
- `design/` 整个目录、两份根流程清单——实现批禁写;该改的点列反哺点清单。`game/tests/README.md`:测量面专属能力行已随本卡 v1.0 写卡段同批入表(「调参测速批量模拟」行),实现批禁写红线不变。
- 探针其余段与套件其余断言族(standee_fx_sweep 的 wd2/wd4/wd5/wd12 面)零触碰;`architecture.md` 其余豁免条目(battle_orchestrator 第三条注销句现状化归战斗编排归约批反哺点)零触碰。
## 6. 关联文档同步面
| 文档 | 关联点(本批实现会改变它的哪条陈述) | 同步义务 |
|---|---|---|
| design/engineering/architecture/architecture.md | 「测试与质量域」S13 测量面豁免注销句(v1.25——「设计闭合、实现批建件」批后现状化;随批申报行为改动句随之成为已实施事实) | 设计权威文档——实现批禁直改;逐条列进 `reports/iter<N>-反哺点-<日期>.md`(豁免注销句现状化),用户裁决后归文档批落文 |
| design/engineering/architecture/architecture.md | 「测试与质量域」S13 测量面豁免注销句(v1.25——「设计闭合、实现批建件」批后现状化;句保持——注销对象是 await 豁免、不依赖行为改动申报;句内「随批申报行为改动一处(收尾置假还原)」半句已失效〔前提被迭代 41 消化、申报已剥离〕,现状化时一并修正为收尾现行陈述) | 设计权威文档——实现批禁直改;逐条列进 `reports/iter44-反哺点-2026-10-30.md`(豁免注销句现状化,该清单 #6 已登记暂停点、随本卡 v1.1 恢复),用户裁决后归文档批落文 |
| game/tests/README.md | 能力表——测量面专属能力行「调参测速批量模拟(逐帧步进与统计读数)」已随本卡写卡段同批入表(真实入口=批量模拟入口,代表用例=本卡对拍套件);manifest 计数行随套件增减浮动(自助声明,零义务) | 机械面文档——实现批禁写;入表已完成于写卡段(本卡 v1.0 同批),实现期零改动 |
| tools/gdstyle_baseline.txt | tune_sim await-in-loop 裸键(:15,该文件唯一在册键)随键消失成死键 | 重生成为人工专用——批末由用户执行 `python tools/gdstyle_gate.py --refresh`(键数 16→15,降幅 <10% 不需 --force);禁实现批中途刷新 |
| design/gdd/(全目录) | 无关联文档——测量工具私有机制改造,不改游戏规则与玩家可见行为(tune_sim 全链路发布态不可达,设计文档 §7.3) | 无修订预期;契约句被触碰=停卡呈报走反哺点通道 |
@@ -72,18 +72,18 @@
| 文件 | 新增/复用 | 职责 | 行为改动 |
|---|---|---|---|
| game/presentation/tune_sim_controller.gd | 复用(改造) | 批量模拟控制器——process_frame 信号常驻连接+步进状态机;收尾单点 `_finish_batch`;全文件零循环 | 行为改动一处(V3 收尾还原)+结构改形(V1——零漂移由对拍证) |
| game/tests/unit/presentation/tune_sim_batch_parity_test.gd | 新增 | 双实现对拍配对套件——遗留 await 形态夹具(内类)+五面对拍断言 | 非行为改动(测试面) |
| game/presentation/tune_sim_controller.gd | 复用(改造) | 批量模拟控制器——process_frame 信号常驻连接+步进状态机;收尾单点 `_finish_batch`(四语句同帧同相位迁入);全文件零循环 | 非行为改动(结构改形——收尾语义零变化,零漂移由对拍证;回归锚面=对照表 V3 行) |
| game/tests/unit/presentation/tune_sim_batch_parity_test.gd | 新增 | 双实现对拍配对套件——现行 await 形态夹具(内类,收尾在 run() 尾的现行形态)+五面对拍断言 | 非行为改动(测试面) |
| game/tests/run_all.gd | 复用(改造) | manifest 增一行(新套件注册,expect 0) | 非行为改动(测试基建) |
结构图与第 8 节互指:新增件一件(对拍套件),其「为什么不能复用/落点唯一」理由见第 8 节;其余两行均为既有文件改造。
结构图与第 8 节互指:新增件一件(对拍套件),其「为什么不能复用/落点唯一」理由见第 8 节;其余两行均为既有文件改造。行为改动列全为零——每行的验收落点见对照表 V1/V2/V3 行。
## 8. 落位与可复用性预检
- **步进状态机不引入新文件(本件内改造)——为什么不拆新件**:单一职责已在件内——`tune_sim_controller` 的职责就是批量模拟控制,步进机制(每帧一局的节律与帧相位承载)是它的私有运行机制,随批生灭(run 期活成员,批末失效下次 run 重写),没有独立于批量模拟的存在意义;拆出「步进机」件命不出独立职责语句=凑计数袋(code-conventions §1.1 判据②④禁)。复用面核查:全仓 grep 无信号步进机同职责实现可复用;其余四形态(SceneTreeTimer 链/Timer 节点/递归协程/线程)已被设计文档 §3 逐一否决(相位不同或键不消)。
- **对拍套件落 `game/tests/unit/presentation/` 的唯一性与命名**:落点唯一——与断言宿主 wd8 同目录同协议(extends SceneTree+Kit 收尾),单元层承载;命名 `tune_sim_batch_parity_test.gd`(领域命名、无 ID 前缀);grep 同名与同义标识符实查无第二处。**遗留 await 形态副本全仓只许此一份**——产品码禁留第二实现(旧形态只活在测试夹具内);tests/ 不在 S13/S8/S9/S10 扫描面(扫描面=产品码三目录与 B1 九文件清单,实查在案),夹具内循环 await 合法在册。
- **对拍套件落 `game/tests/unit/presentation/` 的唯一性与命名**:落点唯一——与断言宿主 wd8 同目录同协议(extends SceneTree+Kit 收尾),单元层承载;命名 `tune_sim_batch_parity_test.gd`(领域命名、无 ID 前缀);grep 同名与同义标识符实查无第二处。**遗留 await 形态副本全仓只许此一份**——产品码禁留第二实现(旧形态只活在测试夹具内);tests/ 不在 S13/S8/S9/S10 扫描面(扫描面=产品码三目录与 S8 文件集,实查在案),夹具内循环 await 合法在册。
- **复用面**:夹具的假面板/假驱动复用 wd8 的形态先例(FakePanel/FakeDriver 同形——代码在各套件内自建,不跨套件共享 helper,禁散落);收尾复用 `test_kit.gd` 的 Kit.finish(零断言专用退出码协议)。
- **分层与交互**:控制器保持 RefCounted、零 Node 入树;process_frame 连接经 `panel.get_tree()` 取树(与现状同一出口);对拍套件 `--script` 模式零 autoload 依赖(db 用字典夹具,同 wd8 :129 先例);零新增跨层出口、零数值表读取、零随机。
- **分层与交互**:控制器保持 RefCounted、零 Node 入树;process_frame 连接经 `panel.get_tree()` 取树(与现状同一出口);对拍套件 `--script` 模式零 autoload 依赖(db 用字典夹具,同 wd8 :142 先例);零新增跨层出口、零数值表读取、零随机。
## 9. 实现规范预算(gdstyle 默认阈值对账:公开方法 ≤20、类变量 ≤15、单函数分支 ≤8、返回 ≤6、参数 ≤5、局部 ≤10、函数 ≤50 行)
@@ -93,7 +93,7 @@
|---|---|---|
| quality/await-in-loop(裸键,本案标的) | **消除** | 全文件零 for/while;await 仅 `run()` 函数级一处(`await _batch_finished`)——结构性免疫 |
| quality/allocation-in-loop | 不命中 | 文件零循环;`PlaytestLogger.new()` 在 run 顶层(现状即如此) |
| max-function-length ≤50 | 达标 | run 约 22 行、`_step_frame` 约 12 行、`_run_one_level` 删两行后约 10 行 |
| max-function-length ≤50 | 达标 | run 约 22 行、`_step_frame` 约 12 行、`_finish_batch` 约 6 行、`_run_one_level` 现行 11 行零触碰 |
| max-branches ≤8 | 达标 | `_step_frame` 一个 if/else;run 一个 if/else |
| max-local-variables ≤10 | 达标 | run 局部 3(logger/stats/autoplay——余者转成员);`_step_frame` 局部 1 |
| max-returns ≤6 | 达标 | 全 void |
@@ -108,15 +108,15 @@
## 11. 任务卡 T0..T3(估时为区间;工作量限制只属迭代层——合计 1–1.5 工作日)
**T0 现状定谳与读数重锚(0.25 工作日)**
**T0 现状复核与读数重锚(0.25 工作日)**
- [ ] 实跑 wd8 单套件(开发期最窄确认,不作验收凭据):`godot --headless --path game --script res://tests/run_all.gd -- --only=standee_fx_sweep`——记录 :132-134 三断言实况;红=潜伏红定谳成立。套件若因在册无关缺陷先红于 wd8 断言(待办清单「迭代 41 测试债修复」在册的 set_trans 一类),先核该债修复状态、不得借本批顺手修非申报面——读数以「wd8 断言可达性」为准呈报(L2 记录)
- [ ] 实跑 ui_tune_probe 单探针:`…console.exe --headless --path game res://probe/ui_tune_probe.tscn`——记录 ⑤ 守卫环与输出四串实况
- [ ] 判读:两面按第 3 节静态推演红=前提成立开工;两面实跑全绿=潜伏红证伪=设计前提崩塌——L3 停卡呈报(第 13 节)
- [ ] 按当轮 master 重锚第 3 节全部 file:line 与成员计数;漂移处就地改卡并记录
- [ ] 实跑 wd8 单套件(开发期最窄确认,不作验收凭据):`godot --headless --path game --script res://tests/run_all.gd -- --only=standee_fx_sweep`——记录 :145-147 三断言实况,**应绿**(绿=前提已被迭代 41 测试债批消化,按无行为改动形态开工)。套件若因在册无关债先红于 wd8 断言,先核该债修复状态、不得借本批顺手修非申报面——读数以「wd8 断言可达性」为准呈报(L2 记录)
- [ ] 实跑 ui_tune_probe 单探针:`…console.exe --headless --path game res://probe/ui_tune_probe.tscn`——记录 ⑤ 守卫环与输出四串实况,**应在**
- [ ] 判读:两面实跑全绿=重裁前提成立开工;任一面红=现状与重裁前提失配(迭代 41 修复被回退或新潜伏红)——L3 停卡呈报(第 13 节)
- [ ] 按当轮施工基面重锚第 3 节全部 file:line 与成员计数;漂移处就地改卡并记录(本卡行号基线=2026-10-30 master;同迭代卡①改址过 main_debug_flow/ui_tune_probe——两件相关行实测未漂移,仍以 T0 重锚为准)
- [ ] 实查产品码 process_frame 订阅面——预期两处(本件 :50+settlement_fx_view.gd:94);出现 run 途中新增订阅者场景=L2 记录+代码评审注记(设计文档 §5 缺口 2)
产出:潜伏红定谳读数(改动前基线证据,独立验收审计要核对);重锚后行号。
产出:现状复核读数(两面绿——前提消化链条的当轮证据,独立验收审计要核对);重锚后行号。
**T1 步进状态机改造(0.25–0.5 工作日)**
@@ -124,16 +124,16 @@
- [ ] 增 `signal _batch_finished` 与 6 个 run 期活成员(名字按设计文档 §4.1 逐字:`_sim_n: int`/`_sim_i: int`/`_sim_stats: Dictionary`/`_sim_autoplay`/`_sim_logger`/`_finished: bool`)
- [ ] `run(n)` 改形:setup 段(:37-46 语句逐字保持);n>0 分支——`is_connected` 守卫 connect `panel.get_tree().process_frame`(恰好连接一次,迭代 39 先例)→ `_step_frame()` 首步同步执行(与 await 形态「第 0 局在调用栈内跑完」对齐)→ `await _batch_finished`(函数级单次 await——键消除落点);n=0 分支直调 `_finish_batch()`(循环零进同步收尾,与 await 形态一致不挂起)
- [ ] 新增 `_step_frame() -> void`(0 参):`_sim_i < _sim_n` 分支——`_run_one_level(_sim_autoplay, _sim_stats, _sim_i, panel, _sim_logger)`+`panel.set_sim_progress("模拟中:%d/%d …" % [_sim_i + 1, _sim_n])`(串逐字保持现码 :49)+`_sim_i += 1`;else 分支——先 disconnect(防再入)→ `_finish_batch()` → `_batch_finished.emit()`
- [ ] 新增 `_finish_batch() -> void`(0 参):`_finished = true` → `close_session()` → `panel.set_sim_running(false)` → `_running = false` → `panel.set_sim_report(HudPresenter.tune_sim_report(_sim_n, panel.atk_pct(), panel.hp_pct(), _sim_stats, _base_hp))`(四语句相对次序按此草图,对两断言面不可观测——白名单 5)
- [ ] `_run_one_level` 删尾部两行(:67-68),其余逐字保持(5 参签名不变)
- [ ] 孤立验证:`--only=standee_fx_sweep` 全绿(wd8 三断言零改动转绿=潜伏红消除实证)+ui_tune_probe 单探针绿(probe_gate --full 段承载)
- [ ] 新增 `_finish_batch() -> void`(0 参):收尾四语句由 run() 尾 :51-54 迁入(`close_session()`→报告注入→`panel.set_sim_running(false)`→`_running = false`,同帧同相位——判据③;`_finished = true` 置位为新增首句,对两断言面不可观测——白名单 5)
- [ ] `_run_one_level` **零触碰**(:58-68 逐字保持——败局节点记账非置假;本批行为零改动的锚面)
- [ ] 孤立验证:`--only=standee_fx_sweep` 全绿(wd8 三断言零改动保持绿=收尾语义零变化实证)+ui_tune_probe 单探针绿(probe_gate --full 段承载)
- [ ] 自检:新增头注/注释零变更叙事、S11 禁词零命中
取用:T0 重锚行号。产出:`_batch_finished` 信号与 `_step_frame`/`_finish_batch` 签名(T2 对拍新形态即产品码本体);wd8 转绿读数。
取用:T0 重锚行号。产出:`_batch_finished` 信号与 `_step_frame`/`_finish_batch` 签名(T2 对拍新形态即产品码本体);wd8 保持绿读数。
**T2 双实现对拍夹具(0.25–0.5 工作日)**
- [ ] 建 `game/tests/unit/presentation/tune_sim_batch_parity_test.gd`(extends SceneTree,Kit 收尾,全类型标注):内类 FakePanel(Node——wd8 同形 setter 面,另带进度串/报告记录数组)+FakeDriver(RefCounted——固定返回值两档〔victory/defeat,同 wd8 :53-55 取值〕,零随机;逐局记录帧索引与运行态读数,帧索引经宿主注入的计数 Callable 读取)+AwaitLoopRunner(RefCounted——现 `run()` await 形态逐字副本,含自身 `is_running()` 镜像;**与产品码的申报差异仅两处**:尾部两行按还原语义置收尾、批末把 stats 六键束存夹具成员供对拍读取)
- [ ] 建 `game/tests/unit/presentation/tune_sim_batch_parity_test.gd`(extends SceneTree,Kit 收尾,全类型标注):内类 FakePanel(Node——wd8 同形 setter 面,另带进度串/报告记录数组)+FakeDriver(RefCounted——固定返回值两档〔victory/defeat,同 wd8 :53-55 取值〕,零随机;逐局记录帧索引与运行态读数,帧索引经宿主注入的计数 Callable 读取)+AwaitLoopRunner(RefCounted——现 `run()` await 形态逐字副本,含循环内 await 与收尾四语句在 run() 尾的现行形态(:47-54 语句逐字),含自身 `is_running()` 镜像;**与产品码新形态的申报差异仅一处**:批末把 stats 六键束存夹具成员供对拍读取——产品码 stats 经报告公开面观测,第 17 节直查受控例外申报)
- [ ] 对拍驱动:两形态各经 `run(n)` 真实入口驱动(新形态=真 TuneSimScript 直构,旧形态=夹具直构;同参假面板/假驱动各自实例);拓扑钉死——测试宿主先连 process_frame 帧计数、后启动两形态(两形态下「计数先于步进体」相对次序同构,序列可比);n=3 常规+n=0 退化两案
- [ ] 五面对拍断言(落点 `_init`,尾缀〔测速 V1〕..〔测速 V5〕):V1 帧索引序列——两形态逐局记录序列逐位相等(每步恰一帧+相位同点的执行证据);V2 运行态序列——驱动内 `is_running()` 读数序列逐位相等+收尾双运行态假相等;V3 进度串序列——面板记录的 set_sim_progress 串序列逐位相等;V4 stats 六键逐位——夹具暴露 stats 与测试内按假驱动固定返回值重算的六键期望(wins/dmg_all/hp_left_win/hp_boss/rounds_all/death_nodes)逐键相等;V5 报告全文逐字——两形态面板记录的 set_sim_report 串逐字相等,n=3 案含「关卡完成率/关卡完成剩余 HP 均/败北节点/N=」渲染面(n=0 案只断逐字相等——空 stats 渲染无剩余 HP 行,`_mean`/`_p90` 空守卫实查在案)
- [ ] `run_all.gd` manifest 增行 `{"path": "res://tests/unit/presentation/tune_sim_batch_parity_test.gd", "expect": 0}`
@@ -147,27 +147,27 @@
- [ ] 关联档复跑:`--only=standee_fx_sweep,tune_sim_batch_parity`+ui_tune_probe(probe_gate --full 段)全绿
- [ ] 批末代码域闸全绿:`tools/run_all_gates.sh --code`(改动含 game/ 下非 .md 行为载体——`--code` 档)
- [ ] 错误路径声明缺口写入实现报告(第 4 节 🔴 条全文迁入——禁静默)
- [ ] 产出物落位卡:`reports/iter<N>-实现报告-<日期>.md`(各卡状态+实机目检总清单〔本批零目检项〕+遗留读数义务+实现决定记录)与 `reports/iter<N>-反哺点清单-<日期>.md`(第 6 节同步面逐条落点)写齐,方算收口
- [ ] 产出物落位卡:`reports/iter44-实现报告-2026-10-30.md`(各卡状态+实机目检总清单〔本批零目检项〕+遗留读数义务+实现决定记录)与 `reports/iter44-反哺点清单-2026-10-30.md`(第 6 节同步面逐条落点)写齐,方算收口
编队注记:本卡与零豁免其余三卡无产品码触面交集(本卡触面=tune_sim_controller.gd+game/tests/ 两件);run_all.gd manifest 行与并行批可能交叠——编队归编排段(待办清单条目口径:编队待迭代 42 合回)。
编队注记:本卡与零豁免其余三卡无产品码触面交集(本卡触面=tune_sim_controller.gd+game/tests/ 两件);run_all.gd manifest 行与并行批可能交叠——编队归编排段。本卡在 iter44 worktree 施工(卡①卡② 已收口冻结在分支),T0 重锚以 worktree 基面为准。
## 12. 验证闸——改动—验收对照表
域口径:本批改动全为 `game/` 下非 .md 行为载体——批末跑代码域闸;里程碑步(T 段)出口跑一次全套闸,波内修复只做出红最小单元的最窄确认,新增测试先行孤立验证再并入全套(测试执行两入口铁规不破)。三件保险(冒烟/系统测试/静态扫描)恒跑;02 单元与集成按关联档选套件——**本卡显式申报套件:standee_fx_sweep_test(wd8——套件名与涉案文件无字面关联,diff 推导可能漏选,特此申报)+tune_sim_batch_parity_test(新增)**。测试方式两分法:本批全部调试接口自动化(假驱动/假面板直构+真实按钮信号链探针),零用户目检项(tune_sim 全链路发布态不可达——登记不阻塞条款零登记项)。**静态判据申报(不入对照表,test-conventions §8.4 出口)**:S13 await-in-loop 裸键消除无独立运行期提交面——判据=`python tools/gdstyle_gate.py` 与 `game/tests/static_scan.sh` 退出码(T3 执行),属静态判据+评审注记申报观测面局限;对拍夹具不在 S13/S8/S9/S10 扫描面的声明同此口径(扫描面=产品码三目录与 B1 九文件清单,static_scan.sh 实查在案)。**声明缺口(不入对照表——证明做不出出口)**:脚本错误中断路径差异不可消除,见第 4 节 🔴 条,实现报告文档化(T3)。**能力清单**:本批新增能力一行「调参测速批量模拟(逐帧步进与统计读数)」已随本卡写卡段同批入表(写卡同批义务,grilling 复核订正原「既有名锚定+后续入表」悬置形态)——对照表三行能力列改锚该行;manifest 计数行随套件增减浮动。
域口径:本批改动全为 `game/` 下非 .md 行为载体——批末跑代码域闸;里程碑步(T 段)出口跑一次全套闸,波内修复只做出红最小单元的最窄确认,新增测试先行孤立验证再并入全套(测试执行两入口铁规不破)。三件保险(冒烟/系统测试/静态扫描)恒跑;02 单元与集成按关联档选套件——**本卡显式申报套件:standee_fx_sweep_test(wd8——套件名与涉案文件无字面关联,diff 推导可能漏选,特此申报)+tune_sim_batch_parity_test(新增)**。测试方式两分法:本批全部调试接口自动化(假驱动/假面板直构+真实按钮信号链探针),零用户目检项(tune_sim 全链路发布态不可达——登记不阻塞条款零登记项)。**静态判据申报(不入对照表,test-conventions §8.4 出口)**:S13 await-in-loop 裸键消除无独立运行期提交面——判据=`python tools/gdstyle_gate.py` 与 `game/tests/static_scan.sh` 退出码(T3 执行),属静态判据+评审注记申报观测面局限;对拍夹具不在 S13/S8/S9/S10 扫描面的声明同此口径(扫描面=产品码三目录与 S8 文件集,static_scan.sh 实查在案)。**声明缺口(不入对照表——证明做不出出口)**:脚本错误中断路径差异不可消除,见第 4 节 🔴 条,实现报告文档化(T3)。**能力清单**:本批新增能力一行「调参测速批量模拟(逐帧步进与统计读数)」已随本卡 v1.0 写卡段同批入表(README :131)——对照表三行能力列锚该行;manifest 计数行随套件增减浮动。
| 行 | 需求点 | 改动(文件:行/行为) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 能力行 |
|---|---|---|---|---|---|---|---|
| V1 | 步进状态机改造(循环内 await 消除的落码面) | tune_sim_controller.gd :36-52 改形(run 函数级单次 `await _batch_finished`,全文件零循环)+:56-68 拆出 `_step_frame`/`_finish_batch` | 复用 wd8 驱动形态 `await ctrl.run(2)`(:131——run 协程身份契约,零改动);新增对拍 V4/V5(stats 六键逐位+报告逐字,落点 tune_sim_batch_parity_test.gd:_init,尾缀〔测速 V4/V5〕) | 单元 | TuneSimScript 直构(字典夹具 db+假面板+假驱动,wd8 :129 同款)经 `run(n)` 真实入口;对拍两形态同入口 | run 可等待(await 挂起=整批完成放行)+stats 六键逐位+报告全文逐字 | 新增:调参测速批量模拟(逐帧步进与统计读数) |
| V1 | 步进状态机改造(循环内 await 消除的落码面) | tune_sim_controller.gd :36-54 改形(run 函数级单次 `await _batch_finished`,全文件零循环;收尾四语句 :51-54 迁入 `_finish_batch`)+:58 起新增 `_step_frame`/`_finish_batch` | 复用 wd8 驱动形态 `await ctrl.run(2)`(:144——run 协程身份契约,零改动);新增对拍 V4/V5(stats 六键逐位+报告逐字,落点 tune_sim_batch_parity_test.gd:_init,尾缀〔测速 V4/V5〕) | 单元 | TuneSimScript 直构(字典夹具 db+假面板+假驱动,wd8 :142 同款)经 `run(n)` 真实入口;对拍两形态同入口 | run 可等待(await 挂起=整批完成放行)+stats 六键逐位+报告全文逐字 | 新增:调参测速批量模拟(逐帧步进与统计读数) |
| V2 | 双实现对拍(帧相位等价的执行化证据) | tune_sim_batch_parity_test.gd 新建+run_all.gd manifest 增行 | 新增五面对拍断言〔测速 V1–V3〕:帧索引序列/运行态序列/进度串序列逐位相等(落点 tune_sim_batch_parity_test.gd:_init) | 单元 | 两形态各经 `run(n)` 真实入口(n=3 常规+n=0 退化);帧索引经测试宿主 process_frame 计数 | 两形态五面记录序列逐位相等(每步恰一帧+相位同点的当轮执行证据——判据本体) | 新增:调参测速批量模拟(逐帧步进与统计读数) |
| V3 | 行为改动——收尾置假还原(已申报唯一行为改动) | tune_sim_controller.gd :67-68 删(每局置假)→ `_finish_batch` 收尾单次置假 | 复用 wd8 三断言〔wd8〕(`drv.seen == [true, true]`/收尾 `ctrl.is_running()` 假+`panel.running` 假/报告非空——零改动)+ui_tune_probe ⑤ 守卫环与输出四串(:63-83——零改动) | 单元+探针 | wd8 假驱动回调读 `is_running()`(`await ctrl.run(2)`);探针 `press_simulate_for_probe()` → 按钮 pressed 信号链 → `run(10)` → 守卫环逐帧复查 | 驱动逐局运行态读数序列 [true, true]+收尾双运行态假+守卫环 1800 帧预算内退出+`output_text()` 四关键串(浮窗运行态贯穿整批的可观测提交面) | 新增:调参测速批量模拟(逐帧步进与统计读数) |
| V3 | 运行态语义零变化(收尾迁移的回归锚面——非行为改动行) | 收尾四语句(:51-54)迁入 `_finish_batch` 同帧同相位——非行为改动判断依据:语句集与语义零变化、对两断言面不可观测,等价证据由对拍 V2/V5 承载;`_run_one_level` :58-68 零触碰 | 复用 wd8 三断言〔wd8〕(:145-147——`drv.seen == [true, true]`/收尾双运行态假/报告非空——零改动**保持绿**)+ui_tune_probe ⑤ 守卫环与输出四串(:63-83——零改动保持绿) | 单元+探针 | wd8 假驱动回调读 `is_running()`(`await ctrl.run(2)`);探针 `press_simulate_for_probe()` → 按钮 pressed 信号链 → `run(10)` → 守卫环逐帧复查 | 驱动逐局运行态读数序列 [true, true]+收尾双运行态假+守卫环 1800 帧预算内退出+`output_text()` 四关键串(浮窗运行态贯穿整批的可观测提交面——绿=保持绿,非转绿) | 新增:调参测速批量模拟(逐帧步进与统计读数) |
反向核对:diff 中行为改动共一处(V3 行);其余为非行为改动——V1 结构改形(零漂移判据=对拍五面全等+wd8/probe 零改动转绿)、V2 测试面新增、manifest 增行(测试基建)。孤儿断言:对拍五断言挂 V1/V2 两行,无孤儿。
反向核对:diff 中行为改动共零处;其余为非行为改动——V1 结构改形(零漂移判据=对拍五面全等+wd8/probe 零改动保持绿)、V2 测试面新增、V3 回归锚面行(非行为改动)、manifest 增行(测试基建)。孤儿断言:对拍五断言挂 V1/V2 两行,wd8/probe 既有断言挂 V3 行,无孤儿。
## 13. 异常分级
- **L1 自救**:编译错、断言尾缀措辞、夹具记录数组名笔误——就地修复,重跑最窄单元确认。
- **L2 记录继续**:T0 套件因在册无关债先红于 wd8 断言(记录可达性事实,不借批顺手修);行号重锚漂移(就地改卡记录);产品码 process_frame 订阅面发现第三处(记录+评审注记——非 run 途中新增场景不红)。
- **L3 停机写报告等用户**:①T0 潜伏红证伪——两面实跑全绿=「每局置假系搬运卷入」前提崩塌,停卡退回写卡段重裁行为改动申报面;②等价性对拍失败——五面任一不等=测量语义漂移,设计前提崩塌;③机检预算互斥——改造后实测键超第 9 节预算或基线外新键=上游缺陷停卡退回写卡段(含禁为机检保留无效成员条款冲突);④S13 对账 await 键未消或冒新键;⑤判据绑不上可观测提交面(证明做不出=停卡呈报,禁自造注入破坏或回退取证)。
- **L3 停机写报告等用户**:①T0 两面任一红=现状与重裁前提失配(迭代 41 修复被回退或新潜伏红),停卡呈报;②等价性对拍失败——五面任一不等=测量语义漂移,设计前提崩塌;③机检预算互斥——改造后实测键超第 9 节预算或基线外新键=上游缺陷停卡退回写卡段(含禁为机检保留无效成员条款冲突);④S13 对账 await 键未消或冒新键;⑤判据绑不上可观测提交面(证明做不出=停卡呈报,禁自造注入破坏或回退取证)。
## 14. 代拍板白名单(逐条列,超清单记实现报告「实现决定记录」)
@@ -175,20 +175,20 @@
2. 头注 `_batch_finished` 契约行措辞(信号语义与发射时点不变)。
3. `_finished` 成员零读者确认后删去(成员 11→10——键账只降不升,行为与断言面零变化)。
4. 夹具假面板/假驱动成员命名微调(记录语义不变;与 wd8 同形自建、不跨套件共享)。
5. `_finish_batch` 收尾段四语句相对次序按第 11 节 T1 草图次序落码(对两个断言面不可观测——实现期查 git 历史原序可进一步对齐)。
5. 收尾段语句相对次序——现行 run() 尾次序(close→报告→置假→_running)与设计草图次序(close→置假→_running→报告)对两个断言面均不可观测,两种皆可落码(实现期查 git 历史原序可进一步对齐);`_finished = true` 置位为新增首句(成员写,不可观测)。
6. 排版机械改写(空行、preload 顺序、注释换行)。
## 15. 待裁点
零待裁点,开工门=U0。(裁决记录:2026-10-29 用户裁四点——①信号步进状态机形态采纳〔SceneTreeTimer 链/Timer 节点/递归协程/线程四形态淘汰依据见设计文档 §3〕;②收尾置假还原作为已申报行为改动随批实施;③双实现对拍入批;④脚本错误中断路径差异接受并文档化。)
零待裁点,开工门=U0。(裁决记录:2026-10-29 用户裁四点——①信号步进状态机形态采纳〔SceneTreeTimer 链/Timer 节点/递归协程/线程四形态淘汰依据见设计文档 §3〕;②收尾置假还原作为已申报行为改动随批实施【已作废——前提被迭代 41 测试债批 215da315 消化,v1.1 剥离】;③双实现对拍入批;④脚本错误中断路径差异接受并文档化。2026-10-30 停卡重裁(用户裁,按停卡呈报建议案逐条落卡):剥离行为改动申报 V3、T0 义务改现状复核、夹具申报差异两处减一处;重裁沿用 2026-10-29 grilling 复核门结论(reports/复核-grilling-零豁免五卡-2026-10-29.md,修正全落),重裁差异面未新增设计裁决。)
## 16. 测试削弱申报
无申报项——wd8 三断言与 ui_tune_probe ⑤ 零改动零削弱;对拍五断言为新增非改判;夹具与产品码的两处申报差异(收尾还原语义+stats 暴露成员)系新增件自身设计、设计文档 §5 在案,非既有断言削弱。禁删断言、禁放宽阈值、禁缩小扫描面、禁把真实入口换成下层直调;未申报的削弱由独立验收审计判红。
无申报项——wd8 三断言与 ui_tune_probe ⑤ 零改动零削弱(保持绿);对拍五断言为新增非改判;夹具与产品码新形态的申报差异仅一处(夹具暴露 stats 供对拍读取)系新增件自身设计、设计文档 §5 在案,非既有断言削弱。禁删断言、禁放宽阈值、禁缩小扫描面、禁把真实入口换成下层直调;未申报的削弱由独立验收审计判红。
## 17. 独立验收审计
合并前由未参与实现的会话或子代理执行;报告落 `reports/iter<N>-验收审计-<日期>.md`,未过不得合并。审计至少做五件事:①逐行核对第 12 节对照表(测试存在、真的执行、尾缀可 grep、真实入口未绕过改动点);②抽查判据绑定面——帧索引序列是否真从 process_frame 发射点记录、对拍拓扑两形态是否同构可比、V3 行判据是否钉在运行态提交面而非终态兜底;③核对 T0 定谳证据在案(改动前 wd8/probe 红读数——潜伏红链条完整,缺 T0 读数=审计不过);④核对行为改动唯一性——diff 中除 V3 外无第二处行为改动;⑤检查测试削弱(对照第 16 节零申报)与内部字段直查受控例外(本批零直查——新形态 stats 经报告公开面观测)。
合并前由未参与实现的会话或子代理执行;报告落 `reports/iter44-验收审计-2026-10-30.md`,未过不得合并。审计至少做五件事:①逐行核对第 12 节对照表(测试存在、真的执行、尾缀可 grep、真实入口未绕过改动点);②抽查判据绑定面——帧索引序列是否真从 process_frame 发射点记录、对拍拓扑两形态是否同构可比、V3 行判据是否钉在运行态提交面(保持绿证据,非终态兜底);③核对现状复核证据在案(T0 两面实跑绿读数+停卡呈报三重证据引注〔215da315 提交信息〕——前提消化链条完整,缺 T0 复核读数=审计不过);④核对行为改动零处——diff 中任何行为改动(含收尾四语句语义漂移)=审计不过,收尾迁移的等价性由对拍 V2 运行态序列+V5 报告逐字承载;⑤检查测试削弱(对照第 16 节零申报)与内部字段直查受控例外(本批零直查——新形态 stats 经报告公开面观测)。
## 18. MC/数值与 RNG 零触碰声明
+5 -5
View File
@@ -5,8 +5,8 @@
## 当前位(只回答「现在该做什么」;三行内随批次换代,禁止在此扩写成状态快照)
- **阶段**:M3——Alpha 版本的完整实现(2026-10-09 立规:未明令宣布完成前停留 M3,迭代只排 M3 需求,M4 不排期;根目录 stage.txt 为旧阶段开关仅存档)。
- **迭代位**:迭代 1–41 全部已完成——迭代 41 两卡(核心数据面结构收敛批+呈现层结构收敛批)2026-10-29 合回(合并提交 eb9bb537,测试特许移交+用户裁直接合回),同日完成批后收尾(反哺点落文+产出物退役+账面换代)。
- **下一步**:待编池:营地交互重做卡⑤(排卡④之后)、卡④(已退回美术设计段,美术设计闭合后重立卡)、冒险前资源配比预设(冻结备选)、迭代 30 原卡②③(2026-10-17 已剔出迭代 30 规划、退回待设计,待美术/设计定稿后重新立卡)。批后可裁事项:gdstyle 基线 152 死键 refresh 清账(`python tools/gdstyle_gate.py --refresh`,待办清单在册)——用户发令时机。
- **迭代位**:迭代 1–43 全部已完成——迭代 43(战斗一屏样张批)2026-10-30 合回(合并提交 3b66d1a6),同日完成批后收尾(反哺点 13 条处置闭合:落文四处 art-bible v3.23/art-prompts v2.10/战斗表现 v1.2/test-conventions 旧句清偿,消解三条,移交待办清单两条目;产出物与两卡需求卡片退役;方向稿目检登记体验验证清单 ㊶)。迭代 42 收尾记录见 git 提交 7ec2af58。
- **下一步**:迭代 44 待开工(组合根注册面+战斗编排归约+测速循环三卡,编队前置=迭代 42 合回已满足)——开工前宜裁 PARITY 对拍漂移查因/重锚(闸 11 触面)与迭代 43 批后待裁三点(待办清单在册)。待编池:营地交互重做卡⑤(排卡④之后)、卡④(已退回美术设计段,美术设计闭合后重立卡)、冒险前资源配比预设(冻结备选)、迭代 30 原卡②③(2026-10-17 已剔出迭代 30 规划、退回待设计,待美术/设计定稿后重新立卡)。批后可裁事项:gdstyle 基线 refresh 收账随零豁免各批合回一次人工 `--refresh --force`(待办清单在册)。
## 里程碑时间线
@@ -75,15 +75,15 @@
| 迭代 39 | 已完成(2026-10-27) | 已退役(git 历史可考) | 2026-10-26 | ≈10.15 个工作日〔用户特批 2026-10-26 超 10 个工作日〕 | 已退役(git 历史可考) |
| 迭代 40 | 已完成(2026-10-28) | 已退役(git 历史可考) | 2026-10-27 | ≈6.375 个工作日 | 已退役(git 历史可考) |
| 迭代 41 | 已完成(2026-10-29) | 已退役(git 历史可考) | 2026-10-27 | ≈13.15 个工作日〔用户特批 2026-10-27 超 10 个工作日〕 | 已退役(git 历史可考) |
| 迭代 42 | 进行中 | reports/需求卡片-gdstyle尾波清偿批-2026-10-29.md<br>reports/需求卡片-level_runner商店委托层拆除-2026-10-29.md | 2026-10-29 | ≈5.375 个工作日 | — |
| 迭代 43 | 待开工 | reports/需求卡片-战斗一屏样张批-2026-10-28.md | 2026-10-29 | ≈6.5 个工作日 | — |
| 迭代 42 | 已完成(2026-10-29) | 已退役(git 历史可考) | 2026-10-29 | ≈5.375 个工作日 | 已退役(git 历史可考) |
| 迭代 43 | 已完成(2026-10-30) | 已退役(git 历史可考) | 2026-10-29 | ≈6.5 个工作日 | 已退役(git 历史可考) |
| 迭代 44 | 待开工 | reports/需求卡片-组合根注册面批-2026-10-29.md<br>reports/需求卡片-战斗编排归约批-2026-10-29.md<br>reports/需求卡片-测速循环批-2026-10-29.md | 2026-10-29 | ≈7.875 个工作日 | — |
| 迭代 45 | 待开工 | reports/需求卡片-呈现层状态件批-2026-10-29.md<br>reports/需求卡片-gdstyle尾波补件批-2026-10-29.md | 2026-10-29 | ≈7 个工作日 | — |
- 迭代 38 收口为带红合并〔用户特批 2026-10-26〕:09 静态扫描 S13 gdstyle 恶化 2 条经批准记入基线存量红(git 提交 c4c69701),卡② 停卡退上游,处方已于 2026-10-26 修订定稿 v1.3(git 提交 b3ea7532),随「gdstyle 存量债全量清偿」条目波 7 待编排。
- 迭代 39 收口为带红合并〔用户特批 2026-10-27〕:02 EV 锚 goblin 0.9148∉[1.00,1.20](触发器批有意行为改动,fp f74b9a48ef4a)已随 EV 锚重锚批换带归绿(git 提交 ba5105aa);08d 生涯.md:55 行号漂移已随 2026-10-28 批后收尾反哺落文修复归绿。三卡拆分后终局 gdstyle 恶化 2 条(battle_orchestrator max-file-length、level_map_screen max-class-variables)归迭代 40 卡①还债与迭代 41 存量债序列。
- 迭代 40 收口为用户特许免闸直合〔2026-10-28〕:全量域验收回执未取得(回执环节断于用户特许,验收审计报告如实记档不作绿报;独立审计结论=通过,F1 一行修 iter40 8a319369 先行入库)。卡① 契约 2 宿主行数残余债(实测 1036 行,合并适配带入 36 行)经用户裁登记归后续批清偿(待办清单「战斗域结构深拆批」条目承载);09 存量红剩 level_map_screen 类变量一条归迭代 41 卡③。
- 迭代 41 收口为测试特许移交+用户裁直接合回〔2026-10-28/2026-10-29〕:全部套件与闸停跑(gdstyle 结构闸除外——开发期读数反馈在跑),验收断言、golden 对拍与独立验收审计一并移交用户执行,2026-10-29 用户裁直接合回(合并提交 eb9bb537);golden 改前锚未建让步与四项实机目检已迁体验验证清单 ㊵;gdstyle 基线 152 死键 refresh 待用户发令(待办清单在册)。
- 迭代 41 收口为测试特许移交+用户裁直接合回〔2026-10-28/2026-10-29〕:全部套件与闸停跑(gdstyle 结构闸除外——开发期读数反馈在跑),验收断言、golden 对拍与独立验收审计一并移交用户执行,2026-10-29 用户裁直接合回(合并提交 eb9bb537);golden 改前锚未建让步与四项实机目检已迁体验验证清单 ㊵;gdstyle 基线 152 死键 refresh 已执行(2026-10-29 人工重锚 168→16 键,git 提交 fafb4fee)。
- 历史行的「—」= 该列事实不可考:迭代 1~8 早于需求卡片制;编排时点与预计周期不再回溯,细节查 git 提交史。
- 「已退役(git 历史可考)」= 需求卡片或实现报告曾存在,已按「证据只留最新」退役;已完成迭代的实现报告列只填 reports/ 现存路径或本保留字,不填「—」。
- 新迭代行的编排与各列判据见 doc-conventions §12.2 与 iter-plan 技能,由流水线闸(`tools/pipeline_gate.py`)机检。
-203
View File
@@ -1,203 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""parity_anchor.py — 「逐位一致对拍」锚工具(架构重构前后逐位一致验收;原需求卡已按「吸收即死」退役,凭据见 git 提交史)
载体 = adventure_monte_carlo.gd 同种子批量完整局(SceneTree headless,自持 ConfigDB,零渲染)。
三变体钉死(种子面错开,覆盖 base / 同调账 / 商店-GP 账三类 T2/T3 重构面):
base : -- 20 71000 both min off off off
attune : -- 20 72000 both min on off off
shop : -- 20 73000 both min off on off
锚双面:
A. RUNSUM 逐行块(stdout 局级摘要面,字符串列表原样入库 anchor.json——红灯时可打印首条差异行);
B. playtest JSONL 全事件流(剔除带墙钟的 session/diagnosis 两类行;文件名本身带时间戳不入比对面)。
比对口径 = 字节级逐位一致(同引擎二进制+同种子确定性,冻结前已实测两跑全同)。
原始面不入库(体量 MB 级):红灯取证用 dump 模式对重构前后各落一份原始数据再 diff——
旧提交随时可重生成同字节原始面(确定性保证),锚只存 sha256 清单。
用法:
python tools/parity_anchor.py freeze # 冻结锚(重构前基线,写入 game/tests/evidence/parity/anchor.json)
python tools/parity_anchor.py check # 对拍:重跑同参批,与锚逐位比对(全同退出 0,差异退出 1 并打印首差异)
python tools/parity_anchor.py dump --out DIR # 只跑批落原始面到 DIR(红灯取证)
可选 --variants base,attune,shop / --quick(只跑 base)
"""
import argparse
import hashlib
import json
import re
import subprocess
import sys
import time
from pathlib import Path
from godot_user_dir import resolve_user_dir
ROOT = Path(__file__).resolve().parent.parent
GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe"
GAME_DIR = ROOT / "game"
ANCHOR_PATH = ROOT / "game" / "tests" / "evidence" / "parity" / "anchor.json"
USER_LOGS = resolve_user_dir(ROOT) / "logs"
# 变体表:名字 → (seed0, attune, shop)。(N=20/策略 both/route min/feat off 全变体钉死。)
VARIANTS = {
"base": (71000, "off", "off"),
"attune": (72000, "on", "off"),
"shop": (73000, "off", "on"),
}
N_RUNS = 20
BATCH_RE = re.compile(r"^BATCH (\w+) N=(\d+) 终局计数=(.*) 日志=user://logs/(playtest_\d+_\S+\.jsonl)\s*$")
TS_ROW_KINDS = ("session", "diagnosis") # 带墙钟 ts 的两类行——剔出比对面
def run_variant(name: str, seed0: int, attune: str, shop: str, timeout: int = 560):
"""跑一个变体批,返回 (runsum_lines, {policy: jsonl绝对路径})。"""
before = set(p.name for p in USER_LOGS.glob("playtest_*.jsonl")) if USER_LOGS.is_dir() else set()
cmd = [GODOT, "--headless", "--path", str(GAME_DIR), "--script",
"res://sim/adventure_monte_carlo.gd", "--",
str(N_RUNS), str(seed0), "both", "min", attune, shop, "off"]
proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8",
errors="replace", timeout=timeout)
out = (proc.stdout or "") + (proc.stderr or "")
runsum = [ln for ln in out.splitlines() if ln.startswith("RUNSUM ")]
logs = {}
for ln in out.splitlines():
m = BATCH_RE.match(ln)
if m:
logs[m.group(1)] = m.group(4)
after = set(p.name for p in USER_LOGS.glob("playtest_*.jsonl")) if USER_LOGS.is_dir() else set()
fresh = after - before
resolved = {}
for policy, fname in logs.items():
p = USER_LOGS / fname
if p.is_file():
resolved[policy] = p
else:
cand = [f for f in fresh if f.startswith("playtest_%d_" % seed0)]
if len(cand) == 1:
resolved[policy] = USER_LOGS / cand[0]
else:
print("[parity] FAIL:变体 %s 的 %s 日志文件定位失败(fresh=%s)" % (name, policy, cand))
sys.exit(2)
expected_policies = 2
if len(runsum) != N_RUNS * expected_policies or len(resolved) != expected_policies:
print("[parity] FAIL:变体 %s 产出不完整(RUNSUM %d 行 / 日志 %d 份)" % (name, len(runsum), len(resolved)))
print(out[-3000:])
sys.exit(2)
return runsum, resolved
def filter_jsonl(path: Path) -> bytes:
"""剔除带 ts 行,保留原始字节行序(bit-exact;不做任何再序列化)。"""
kept = []
with open(path, "rb") as f:
for raw in f.read().split(b"\n"):
if not raw.strip():
continue
head = raw[:48]
if any(('{"kind":"%s"' % k).encode() in head for k in TS_ROW_KINDS):
continue
kept.append(raw)
return b"\n".join(kept) + b"\n"
def collect(variants, quick=False):
if quick:
variants = [v for v in variants if v == "base"] or ["base"]
data = {}
for v in variants:
seed0, attune, shop = VARIANTS[v]
t0 = time.time()
print("[parity] 跑批 %s(N=%d×2 策略,seed0=%d,attune=%s,shop=%s)…" % (v, N_RUNS, seed0, attune, shop))
runsum, logs = run_variant(v, seed0, attune, shop)
faces = {"runsum": runsum, "jsonl": {}}
for policy in sorted(logs):
body = filter_jsonl(logs[policy])
faces["jsonl"][policy] = {
"sha256": hashlib.sha256(body).hexdigest(),
"rows": body.count(b"\n"),
}
data[v] = faces
print("[parity] %s 完成(%.0fs,RUNSUM %d 行,JSONL %s)" %
(v, time.time() - t0, len(runsum),
", ".join("%s=%s" % (p, faces["jsonl"][p]["sha256"][:12]) for p in sorted(faces["jsonl"]))))
return data
def dump_variant_raw(variants, outdir: Path):
outdir.mkdir(parents=True, exist_ok=True)
for v in variants:
seed0, attune, shop = VARIANTS[v]
runsum, logs = run_variant(v, seed0, attune, shop)
(outdir / ("%s_runsum.txt" % v)).write_text("\n".join(runsum) + "\n", encoding="utf-8", newline="\n")
for policy, p in logs.items():
(outdir / ("%s_%s.jsonl" % (v, policy))).write_bytes(filter_jsonl(p))
print("[parity] dump %s → %s" % (v, outdir))
def main():
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="迭代27重构逐位一致对拍锚")
ap.add_argument("mode", choices=["freeze", "check", "dump"])
ap.add_argument("--variants", default="base,attune,shop")
ap.add_argument("--quick", action="store_true", help="只跑 base 变体")
ap.add_argument("--out", default="", help="dump 模式输出目录")
args = ap.parse_args()
variants = [v.strip() for v in args.variants.split(",") if v.strip() in VARIANTS]
if not variants:
print("[parity] FAIL:无有效变体(可选 %s)" % list(VARIANTS))
return 2
if args.mode == "dump":
dump_variant_raw(variants, Path(args.out if args.out else str(ROOT / "tools" / "parity_dump")))
return 0
data = collect(variants, quick=args.quick)
if args.mode == "freeze":
ANCHOR_PATH.parent.mkdir(parents=True, exist_ok=True)
anchor = {"variants": {v: data[v] for v in variants}}
ANCHOR_PATH.write_text(json.dumps(anchor, ensure_ascii=False, indent=1), encoding="utf-8", newline="\n")
print("[parity] 锚已冻结 → %s(变体 %s)" % (ANCHOR_PATH, variants))
return 0
# check
if not ANCHOR_PATH.is_file():
print("[parity] FAIL:锚不存在(先 freeze)→ %s" % ANCHOR_PATH)
return 2
anchor = json.loads(ANCHOR_PATH.read_text(encoding="utf-8"))
bad = 0
for v in variants:
if v not in anchor["variants"]:
print("[parity] FAIL:锚无变体 %s(锚含 %s)" % (v, list(anchor["variants"])))
bad += 1
continue
old = anchor["variants"][v]
new = data[v]
if old["runsum"] != new["runsum"]:
bad += 1
print("[parity] ✗ %s RUNSUM 漂移(%d 行 vs %d 行)" % (v, len(old["runsum"]), len(new["runsum"])))
for i, (a, b) in enumerate(zip(old["runsum"], new["runsum"])):
if a != b:
print("[parity] 首差异第 %d 行:" % i)
print("[parity] 锚: %s" % a[:600])
print("[parity] 今: %s" % b[:600])
break
for policy in sorted(old["jsonl"]):
o, n = old["jsonl"][policy], new["jsonl"].get(policy)
if n is None:
bad += 1
print("[parity] ✗ %s/%s 本次缺日志" % (v, policy))
elif o["sha256"] != n["sha256"]:
bad += 1
print("[parity] ✗ %s/%s JSONL 逐位漂移(锚 %s… 行数 %d vs 今 %s… 行数 %d)" %
(v, policy, o["sha256"][:12], o["rows"], n["sha256"][:12], n["rows"]))
if bad:
print("[parity] 逐位一致对拍 ✗ 失败 %d 项——该波回炉(V1 红线)" % bad)
return 1
print("[parity] 逐位一致对拍 ✓ 全同(变体 %s,RUNSUM+JSONL 双面逐位一致)" % variants)
return 0
if __name__ == "__main__":
sys.exit(main())
-68
View File
@@ -1,68 +0,0 @@
#!/usr/bin/env python3
"""V12 同种子复放比对器(卡② 效果引擎无状态化批):解析两次 MC 采集的 RUNSUM 行按 seed 配对判读。
两判据模式——parity:整条记录逐位相等(off 组,未受影响路径须透明);
input-parity:输入面八字段 + 顶层字段集恒等、种子集与局数一致(formed/rules 组,输出面允许 RNG 级联位移)。
退出码 0 绿 / 1 红 / 2 用法或解析错。字段集从基线文件推导,禁硬编码。"""
import sys, json, argparse
INPUT_KEYS = ["policy", "seed", "atk_pct", "hp_pct", "route", "attune", "equipped", "feat_mode"]
def load(path):
recs = {}
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line.startswith("RUNSUM "):
continue
try:
d = json.loads(line[len("RUNSUM "):])
except json.JSONDecodeError:
print("解析失败行:", line[:80])
sys.exit(2)
recs[d.get("seed")] = d
return recs
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--mode", choices=["parity", "input-parity"], required=True)
ap.add_argument("--base", required=True)
ap.add_argument("--post", required=True)
ap.add_argument("--expect", type=int, required=True)
a = ap.parse_args()
base, post = load(a.base), load(a.post)
if len(base) != a.expect or len(post) != a.expect:
print("局数不符:base=%d post=%d expect=%d" % (len(base), len(post), a.expect))
return 1
if set(base) != set(post):
print("种子集不一致")
return 1
red = False
if a.mode == "parity":
diff = [s for s in base if base[s] != post[s]]
if diff:
red = True
print("off 组逐位差异 %d 局: %s" % (len(diff), diff[:10]))
for s in diff[:3]:
k = [kk for kk in base[s] if base[s].get(kk) != post[s].get(kk)]
print(" seed", s, "差异字段:", k)
else:
bkeys = set(next(iter(base.values())).keys())
pkeys = set(next(iter(post.values())).keys())
if bkeys != pkeys:
red = True
print("顶层字段集不一致 base-p=%s post-b=%s" % (bkeys - pkeys, pkeys - bkeys))
for s in base:
for kk in INPUT_KEYS:
if base[s].get(kk) != post[s].get(kk):
red = True
print("输入面字段 %s 在 seed %s 不一致: %r vs %r" % (kk, s, base[s].get(kk), post[s].get(kk)))
# levels_cleared 直方图仅报告(方向不作判据)
lc = post[s].get("levels_cleared") if post else None
print("判读:%s" % ("FAIL" if red else "PASS"))
return 1 if red else 0
if __name__ == "__main__":
sys.exit(main())
+16
View File
@@ -18,6 +18,10 @@
# ④ 基准档 1280 单档(Q4)。1920 档注记:16:9 全屏下 stretch=canvas_items/aspect=keep 等比
# 1.35×+左右黑边各 96px,构图与基准档一致(分辨率案甲)——分辨率维度不重复建闸。
# (1920 布局机检面现无承载,待补。)
# ⑤ 脚本错误扫描(三硬闸之二):shot/fx/board 三段 Godot 日志逐段过 tools/error_scan.py,
# 命中 SCRIPT ERROR/Parse Error 即红(标记与 run_smoke.py 同源);python 工具自身输出不扫。
# 此类错误不改退出码、不计失败数,且会中断所在函数的剩余断言——只能由外部扫日志。
# 新增日志段必须同步加扫描调用,漏加=该段静默错误漏网。
# 过程产物纪律(Q2):一切截图 PNG 落 mktemp 临时区,闸毕即弃——任何过程 PNG 禁入 git;
# 例外=game/tests/golden/ 基线(测试资产入库,.gitignore 已核不挡)。
# 窗口化运行会短暂弹 Godot 窗口(本机可开窗口径);禁改 --headless 跑本闸
@@ -41,6 +45,15 @@ kill_orphans() {
local p; for p in $(orphans); do taskkill //F //PID "$p" >/dev/null 2>&1; done
}
scan_log() { # scan_log <段名> <日志>——脚本错误扫描;日志不可读(error_scan exit 2)同判红
local name="$1" log="$2"
if ! python "$REPO/tools/error_scan.py" "$log" >"$TMP/err_scan.txt" 2>&1; then
echo "[render_gate] FAIL — $name 脚本错误扫描未通过(命中或日志不可读):"
sed -n '1,6p' "$TMP/err_scan.txt" | sed 's/^/ /'
_rc=1
fi
}
# 冷检出刷导入(.godot 不入 git,fresh worktree 首跑一次性付)
if [ ! -f "$GAME/.godot/uid_cache.bin" ]; then
echo "[render_gate] 冷检出:editor --quit 刷导入(一次性,可到 1-2 分钟)"
@@ -66,6 +79,7 @@ if [ "$_rc" -eq 0 ]; then
_rc="${PIPESTATUS[0]}"
[ "$_rc" -ne 0 ] && echo "[render_gate] FAIL — shot_checker A-D 断言存在红(S1 停机点:首跑存量 FAIL 全量呈报登记待办清单)"
fi
scan_log "shot_probe" "$TMP/shot.log"
echo "======== fx_shot_probe 三时刻取证(Q7) ========"
FX="$TMP/fxshots"
@@ -77,6 +91,7 @@ if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
elif [ "$st" -ne 0 ]; then
echo "[render_gate] FAIL — fx_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1
fi
scan_log "fx_shot_probe" "$TMP/fx.log"
fx_evidence() { # 取证双检:模式文件存在且非零字节(零字节/缺失任一即 FAIL——Q7)
local pat="$1" min="$2" n=0 f
for f in "$FX"/$pat; do
@@ -101,6 +116,7 @@ if [ "$st" -eq 124 ] || [ "$st" -eq 137 ]; then
elif [ "$st" -ne 0 ]; then
echo "[render_gate] FAIL — board_shot_probe 退出码=$st(取证失败=闸红)"; _rc=1
fi
scan_log "board_shot_probe" "$TMP/bd.log"
for b in board_L1_dungeon.png board_L2_underdark.png board_L3_deep_underdark.png board_meta.json; do
if [ ! -s "$BD/$b" ]; then echo "[render_gate] FAIL — 取证缺失/零字节: $b"; _rc=1; fi
done
+649 -76
View File
@@ -34,6 +34,13 @@ v3.0 口径(方向稿=目检先行闸,冻结归机检):
输出 = artwork/mockups/sample-batch-v3-final-2026-09-30.png(v3.5 冻结口径的正式产物)
+ 机检报告(中间产物,吸收即死不入 git;断言定值=A3b Δ≥30/A4 尺寸基线)。
用法:python tools/render_sample_sheet_v3.py
python tools/render_sample_sheet_v3.py --gen-assets (本批追加:产出 §5.2/§5.3 四件素材入包)
python tools/render_sample_sheet_v3.py --save-baseline(本批追加:把当前整屏 mockup 存为改前基线)
迭代 43 扩展(2026-10-29 战斗一屏样张批):整屏样张两版出口(1280×800 基座 + 1920×1080 全局等比
1.35×+左右黑边,§8.2 1920 档策略)+ art-bible §1.4 整屏四项断言(S1–S4)+ 战斗表现五项断言(P1–P5)
+ S3b 图类型色板变体第二套 + S5 三组关键帧条 + S6 描边同屏对照与遮挡对照组 + V1 两版尺寸断言
+ V10 改前/改后基线并排对拍;B 区高按代码现状 252 出样(art-bible §8.2 文 270 差异只登记不修)。
"""
from PIL import Image, ImageDraw, ImageFilter, ImageChops, ImageFont
import os
@@ -60,6 +67,10 @@ BOARD_DIR = ("game", "assets", "art", "board")
CHAR_DIR = ("game", "assets", "art", "character")
ICON_DIR = ("game", "assets", "ui", "icons")
OUT_PNG = os.path.join(ROOT, "artwork", "mockups", "sample-batch-v3-final-2026-09-30.png")
OUT_ED1 = os.path.join(ROOT, "artwork", "mockups", "sample_battle_screen_1280x800.png")
OUT_ED2 = os.path.join(ROOT, "artwork", "mockups", "sample_battle_screen_1920x1080.png")
BASELINE_PNG = os.path.join(ROOT, "artwork", "mockups", "sample_battle_screen_baseline_1280.png")
OUT_BEFORE_AFTER = os.path.join(ROOT, "artwork", "mockups", "sample_battle_screen_before_after.png")
OUT_REPORT = os.path.join(ROOT, "reports", "sample-v3-final-机检报告-2026-09-30.md")
T_FONT = asset_root("artwork", "ui-reuse-2026-09-23",
"4-字体", "fusion-pixel-12px(OFL)", "fusion-pixel-12px-proportional-latin.ttf")
@@ -107,7 +118,7 @@ F_LBL2 = ImageFont.truetype(ZH_FONT, 12)
F_TITLE = ImageFont.truetype(ZH_FONT, 19)
F_STANDEE = ImageFont.truetype(ZH_FONT, 42) # 敌像字形占位
F_NAME = ImageFont.truetype(ZH_FONT, 12)
F_ORDER = ImageFont.truetype(ZH_FONT, 13)
F_ORDER = ImageFont.truetype(ZH_FONT, 14) # 顺位徽标=UiKit.FS_HINT 14(§7.4 清单外 11–13px 提档;镜像实装)
F_INTENT = ImageFont.truetype(ZH_FONT, 15)
CHECKS = [] # (id, name, ok, detail)
@@ -729,21 +740,47 @@ def render_main_board():
return board, placement, records, band, gmap
# ══ B 区站桩(mockup 1:1) ══
# ══ B 区站桩(mockup 1:1——镜像 standee_body.gd 实装定值:血条危险红/条底收口/徽标 FS_HINT/
# 护甲 PNG/名字行素底不压地台/地台件九宫格拉伸;缺位回退与实装同构) ══
_PLAZA_CACHE = {}
_ARMOR_CACHE = {}
def _plaza_tex():
"""地台件 48×12(缺位回退 None——程序化平色镜像实装 fallback)。"""
if "tex" not in _PLAZA_CACHE:
try:
_PLAZA_CACHE["tex"] = Image.open(asset_root(
"game", "assets", "art", "scene", "scene_stage_plaza.png")).convert("RGBA")
except FileNotFoundError:
_PLAZA_CACHE["tex"] = None
return _PLAZA_CACHE["tex"]
def _armor_tex(armor):
"""护甲图标 24×24(缺位回退 None——mockup 画字形,实装画字形占位同构)。"""
if armor not in _ARMOR_CACHE:
try:
_ARMOR_CACHE[armor] = Image.open(asset_root(
"game", "assets", "ui", "icons", "ui_armor_%s.png" % armor)).convert("RGBA")
except FileNotFoundError:
_ARMOR_CACHE[armor] = None
return _ARMOR_CACHE[armor]
def draw_standee(canvas, ox, oy, kind, name, enemy_id, order=1, armor="light",
hp=1.0, intent="", statuses=(), dead=False):
"""站桩 150×176(standee_view.gd 几何):血条+状态行头顶/立绘盒 96px/底座/敌名行。"""
"""站桩 150×176(standee_body.gd 几何):血条+状态行头顶/立绘盒 96px/底座/敌名行。"""
d = ImageDraw.Draw(canvas)
W, H = 150, 176
# 头顶:顺位徽标(敌)+血条
# 头顶:顺位徽标(敌,FS_HINT 14)+血条(填充=危险红 #A8321F 不因敌我分色;条底=HP_BAR_TRACK)
tx = ox + 8
if kind == "enemy":
d.text((tx, oy + 2), "①②③"[order - 1], font=F_ORDER, fill=(176, 122, 20))
d.text((tx, oy + 2), "①②③"[order - 1], font=F_ORDER, fill=(224, 184, 96))
tx += 18
bw = W - 16 - (18 if kind == "enemy" else 0)
d.rectangle([tx, oy + 4, tx + bw, oy + 14], fill=(60, 52, 42))
d.rectangle([tx, oy + 4, tx + int(bw * hp), oy + 14],
fill=(124, 196, 124) if kind == "hero" else (150, 60, 48))
d.rectangle([tx, oy + 4, tx + bw, oy + 14], fill=(26, 26, 28))
d.rectangle([tx, oy + 4, tx + int(bw * hp), oy + 14], fill=(168, 50, 31))
# 状态图标行(血条正下方,16px 定稿 PNG)
sx = ox + (W - 17 * max(1, len(statuses))) // 2
for i, ic in enumerate(statuses):
@@ -758,39 +795,103 @@ def draw_standee(canvas, ox, oy, kind, name, enemy_id, order=1, armor="light",
glyph = {"goblin": "▂", "bugbear": "▆", "ogre_chief": "█"}.get(enemy_id, "敌")
col = (150, 60, 48) if not dead else (110, 104, 98)
d.text((ox + W // 2, pbox_y + 48), glyph, font=F_STANDEE, fill=col, anchor="mm")
# 意图徽标(立绘上半部居中悬浮)
# 意图徽标(立绘上半部居中悬浮;镜像实装:PANEL_DATA 素牌垫底——四色墨对牌 4.7–5.8:1)
if intent:
glyph, col = {"damage": ("⚔", (198, 61, 34)), "buff": ("✦", (46, 134, 222)),
"debuff": ("☠", (122, 66, 148)), "charge": ("⚔", (176, 122, 20))}[intent]
glyph, col = {"damage": ("⚔", (150, 44, 36)), "buff": ("✦", (30, 90, 150)),
"debuff": ("☠", (122, 66, 148)), "charge": ("⚔", (116, 80, 11))}[intent]
ts = d.textbbox((0, 0), glyph, font=F_INTENT)
tw_, th_ = ts[2] - ts[0], ts[3] - ts[1]
d.rectangle([ox + W // 2 - tw_ / 2 - 4, pbox_y + 30 - th_ * 0.75,
ox + W // 2 + tw_ / 2 + 4, pbox_y + 30 + th_ * 0.35], fill=(233, 217, 184))
d.text((ox + W // 2, pbox_y + 30), glyph, font=F_INTENT, fill=col, anchor="mm")
# v3 地台+落影(§6.1 舞台正面目标:石板地台承载,椭圆落影压脚)
# v3 地台+落影(§6.1 舞台正面目标:石板地台承载,椭圆落影压脚;地台=独立件九宫格横向拉伸)
sh = Image.new("RGBA", (W, 30), (0, 0, 0, 0))
ImageDraw.Draw(sh).ellipse([28, 6, W - 28, 24], fill=(0, 0, 0, 70))
canvas.alpha_composite(sh, (ox, oy + 146))
d.rectangle([ox + 18, oy + 158, ox + W - 18, oy + 170], fill=(150, 130, 100), outline=(90, 76, 58))
d.rectangle([ox + 18, oy + 158, ox + W - 18, oy + 160], fill=(196, 172, 132))
# 敌名行:护甲类型图标+纯名字(AC 数值不展示)
ped_w = W - 36
ped_x, ped_y = ox + 18, oy + 158
tex = _plaza_tex()
if tex is not None:
canvas.paste(tex.crop((0, 0, 4, 12)).resize((4, 12), Image.NEAREST), (ped_x, ped_y))
canvas.paste(tex.crop((4, 0, 44, 12)).resize((ped_w - 8, 12), Image.NEAREST), (ped_x + 4, ped_y))
canvas.paste(tex.crop((44, 0, 48, 12)).resize((4, 12), Image.NEAREST), (ped_x + ped_w - 4, ped_y))
else:
d.rectangle([ped_x, ped_y, ped_x + ped_w, ped_y + 12], fill=PLAZA_BODY, outline=PLAZA_EDGE)
d.rectangle([ped_x, ped_y, ped_x + ped_w, ped_y + 2], fill=PLAZA_RIM)
# 名字行:整行垫 HP_BAR_BACK 素底(牌底沿=地台顶——文字禁直接压地台 §6.1/§7.2;
# 护甲图标=24×24 PNG 缺位回退字形;几何镜像 standee_body._name_plate_rect)
if kind == "enemy":
d.text((ox + 16, oy + 172), ARMOR_ICONS[armor], font=F_NAME, fill=ARMOR_COLORS[armor], anchor="lm")
d.text((ox + 30, oy + 172), name, font=F_NAME, fill=(240, 226, 200), anchor="lm")
itex = _armor_tex(armor)
if itex is not None:
icon_w, icon_draw = 24.0, ("tex", itex)
else:
gtxt = ARMOR_ICONS[armor]
icon_w = d.textbbox((0, 0), gtxt, font=F_NAME)[2]
icon_draw = ("glyph", gtxt, ARMOR_COLORS[armor])
name_w = d.textbbox((0, 0), name, font=F_NAME)[2]
plate_w = icon_w + 3.0 + name_w + 8.0
px0 = ox + (W - plate_w) / 2
py0 = oy + 158 - 26
d.rectangle([px0, py0, px0 + plate_w, py0 + 26], fill=(40, 34, 32))
base_x, base_y = px0 + 4, py0 + 17 # 基线=牌顶+17(镜像 _name_baseline;box.y-27 < 地台顶)
if icon_draw[0] == "tex":
canvas.paste(icon_draw[1].resize((24, 24), Image.NEAREST),
(int(base_x), py0 + 1), icon_draw[1].resize((24, 24), Image.NEAREST))
else:
d.text((base_x, base_y), icon_draw[1], font=F_NAME, fill=icon_draw[2], anchor="ls")
d.text((base_x + icon_w + 3.0, base_y), name, font=F_NAME, fill=(240, 226, 200), anchor="ls")
if dead:
gray = Image.new("RGBA", (W, H), (100, 100, 100, 120))
canvas.alpha_composite(gray, (ox, oy))
def render_battle_screen():
"""B/C/D 三区全内容 1280×800 mockup。"""
scr = Image.new("RGBA", (1280, 800), (24, 20, 17, 255))
d = ImageDraw.Draw(scr)
# ── B 区 252px 全宽(v3 舞台:世界底层暗背景+顶光柱氛围;文字=信息面层暖白) ──
d.rectangle([0, 0, 1280, 252], fill=(62, 52, 42), outline=UIKIT["PANEL_BORDER"])
beam = Image.new("RGBA", (1280, 252), (0, 0, 0, 0))
B_H = 252 # B 区高=代码现状(battle_stage_view custom_minimum_size 252;§8.2 文 270 差异只登记不修)
CANVAS_W = 1280 # 基座档(art-bible §8.1)
def draw_base(canvas_w=CANVAS_W, canvas_h=800, b_h=B_H):
"""样张底板(T1):B/C/D 三区骨架——分区数值按 battle.md 指路、B 区按代码 252;格边长=CELL_D 45
(D 区 10×10 板实比,§8.1)。返回 (img, zones)——zones 供 S/P 断言取样:
b=(0,0,w,b_h) 含明细带;band=明细带 24px;c=(0,b_h+4,240,h);d=(244,b_h+4,w,h)。"""
img = Image.new("RGBA", (canvas_w, canvas_h), (24, 20, 17, 255))
d = ImageDraw.Draw(img)
c_y = b_h + 4
# B 区(舞台底=世界底层下沿子带 L 0.014–0.06;文字=信息面层暖白)
d.rectangle([0, 0, canvas_w, b_h], fill=(62, 52, 42), outline=UIKIT["PANEL_BORDER"])
beam = Image.new("RGBA", (canvas_w, b_h), (0, 0, 0, 0))
db = ImageDraw.Draw(beam)
for bx0 in (110, 900, 1055, 1210):
for i in range(60):
a = int(26 * (1 - i / 60.0))
db.rectangle([bx0 - 46 + i // 3, 0, bx0 + 46 - i // 3, 40 + i * 3], fill=(255, 236, 200, a))
scr.alpha_composite(beam, (0, 0))
img.alpha_composite(beam, (0, 0))
# 明细带(挖掘期收起 24px)
d.rectangle([0, b_h - 24, canvas_w, b_h], fill=(46, 39, 32))
# C 区
d.rectangle([0, c_y, 240, canvas_h], fill=UIKIT["PANEL_HERO"], outline=UIKIT["PANEL_BORDER"])
# D 区
d.rectangle([244, c_y, canvas_w, canvas_h], fill=(34, 29, 24), outline=UIKIT["PANEL_BORDER"])
# D 区顶行工具件(停手/图例/刷新+AP 点阵+RAP)
d.rectangle([252, c_y + 6, 304, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88))
d.text((278, c_y + 17), "停手", font=F_LBL2, fill=(232, 222, 200), anchor="mm")
d.rectangle([310, c_y + 6, 354, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88))
d.text((332, c_y + 17), "?图例", font=F_LBL2, fill=(232, 222, 200), anchor="mm")
d.rectangle([360, c_y + 6, 428, c_y + 28], fill=(90, 80, 66), outline=(120, 106, 88))
d.text((394, c_y + 17), "刷新 8|剩2", font=F_LBL2, fill=(232, 222, 200), anchor="mm")
for i in range(6):
col = (232, 200, 120) if i < 4 else (120, 106, 88)
d.ellipse([440 + i * 18, c_y + 10, 452 + i * 18, c_y + 22], fill=col)
d.text((552, c_y + 17), "RAP 6/6", font=F_LBL2, fill=(200, 180, 140), anchor="lm")
zones = {"b": (0, 0, canvas_w, b_h), "band": (0, b_h - 24, canvas_w, b_h),
"c": (0, c_y, 240, canvas_h), "d": (244, c_y, canvas_w, canvas_h)}
return img, zones
def render_battle_screen(board_img=None):
"""B/C/D 三区全内容 1280×800 mockup(T1 底板+T2 内容)。返回 (scr, zones, board_origin)。"""
scr, zones = draw_base()
d = ImageDraw.Draw(scr)
b_h = B_H
d.text((640, 14), "第 3 轮 · 挖掘", font=ImageFont.truetype(ZH_FONT, 15), fill=(240, 226, 200), anchor="mm")
draw_standee(scr, 40, 34, "hero", "主角", "", hp=0.7, statuses=("attack_up", "ac_up"))
d.text((560, 120), "兑现演出区\n(结算期:光带 → 弹道 → 受击)", font=ImageFont.truetype(ZH_FONT, 13),
@@ -799,34 +900,28 @@ def render_battle_screen():
draw_standee(scr, 973, 34, "enemy", "熊地精", "bugbear", order=2, armor="light", hp=0.8, intent="charge",
statuses=("alert",))
draw_standee(scr, 1128, 34, "enemy", "食人魔酋长", "ogre_chief", order=3, armor="light", hp=1.0, intent="buff")
# 明细带(挖掘期收起 24px)
d.rectangle([0, 228, 1280, 252], fill=(46, 39, 32))
# 明细带摘要行 + 结算语境 hero 剪影 mini(P2 三语境之一;右端贴装不越缘)
d.text((10, 240), "── 第 3 轮 ── 地精 受到 4 点伤害(增伤 ×1)· 熊地精 蓄力(重击预告)…", font=F_LBL2,
fill=(240, 226, 200), anchor="lm")
# ── C 区 240×548 ──
d.rectangle([0, 256, 240, 800], fill=UIKIT["PANEL_HERO"], outline=UIKIT["PANEL_BORDER"])
mini = HERO.resize((24, int(24 * HERO.size[1] / HERO.size[0])), Image.NEAREST)
scr.alpha_composite(mini, (1236, b_h - 24))
# ── C 区内容(次级文对齐 §7.2 ≥4.5:1——STONE_DEEP/GOLD_INK 在册墨) ──
d.rectangle([8, 264, 232, 340], outline=(120, 106, 88), width=1) # 攻击数据框(直角细线)
d.text((16, 276), "攻击 6", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22))
d.text((16, 298), "(+50% 增伤 ×1)", font=F_LBL2, fill=(140, 90, 30))
d.text((16, 298), "(+50% 增伤 ×1)", font=F_LBL2, fill=(116, 80, 11))
d.text((16, 318), "护甲 AC 12", font=ImageFont.truetype(ZH_FONT, 15), fill=(30, 26, 22))
d.text((8, 352), "魔法道具槽(预留)", font=F_LBL2, fill=(120, 110, 98))
d.text((8, 352), "魔法道具槽(预留)", font=F_LBL2, fill=(85, 77, 65))
for i in range(4):
d.rectangle([8 + i * 40, 368, 8 + i * 40 + 34, 402], outline=(120, 106, 88), width=1)
d.text((8, 416), "本关强化列表(预留)", font=F_LBL2, fill=(120, 110, 98))
# ── D 区 ──
d.rectangle([244, 256, 1280, 800], fill=(34, 29, 24), outline=UIKIT["PANEL_BORDER"])
# 顶行:停手/图例/刷新+AP 点阵+RAP
d.rectangle([252, 262, 304, 284], fill=(90, 80, 66), outline=(120, 106, 88))
d.text((278, 273), "停手", font=F_LBL2, fill=(232, 222, 200), anchor="mm")
d.rectangle([310, 262, 354, 284], fill=(90, 80, 66), outline=(120, 106, 88))
d.text((332, 273), "?图例", font=F_LBL2, fill=(232, 222, 200), anchor="mm")
d.rectangle([360, 262, 428, 284], fill=(90, 80, 66), outline=(120, 106, 88))
d.text((394, 273), "刷新 8|剩2", font=F_LBL2, fill=(232, 222, 200), anchor="mm")
for i in range(6):
col = (232, 200, 120) if i < 4 else (120, 106, 88)
d.ellipse([440 + i * 18, 266, 452 + i * 18, 278], fill=col)
d.text((552, 273), "RAP 6/6", font=F_LBL2, fill=(200, 180, 140), anchor="lm")
return scr
d.text((8, 416), "本关强化列表(预留)", font=F_LBL2, fill=(85, 77, 65))
# ── D 区主样张板(10×10@45px=450×450+悬崖 8)嵌进工具行之下居中 ──
board_origin = None
if board_img is not None:
bx = 244 + (1036 - board_img.size[0]) // 2
by = b_h + 4 + 34 + (800 - b_h - 4 - 34 - board_img.size[1]) // 2
scr.paste(board_img.convert("RGB"), (bx, by), board_img)
board_origin = (bx, by)
return scr, zones, board_origin
# ══ 机检 ══
@@ -1033,6 +1128,211 @@ def run_checks(board, placement, records, band, gmap, canvas_img=None):
return all(c[2] for c in CHECKS)
# ══ 整屏四项(S1–S4,art-bible §1.4 整屏样张闸)+战斗表现五项(P1–P5)+护甲对比锚(A7) ══
def _grad_top_cells(scr, frac=100):
"""整屏横向梯度图(步长 2)——首焦唯一性取样用,返回 [(ΔL, x, y)] 降序。"""
rgbp = scr.convert("RGB").load()
w, h = scr.size
grad = []
for y in range(0, h, 2):
for x in range(0, w - 2, 2):
c1, c2 = rgbp[x, y], rgbp[x + 2, y]
l1 = 0.2126 * c1[0] + 0.7152 * c1[1] + 0.0722 * c1[2]
l2 = 0.2126 * c2[0] + 0.7152 * c2[1] + 0.0722 * c2[2]
grad.append((abs(l1 - l2), x, y))
grad.sort(reverse=True)
return grad[:max(1, len(grad) // frac)]
def _mask_maxch_dist(img, box, tol=40):
"""隔离剪影掩码:四角中值底色 + 三通道最大距离阈值(蓝夹克对暖底 maxΔ>40 可掩)。"""
crop = img.crop(box).convert("RGB")
w, h = crop.size
px = crop.load()
corners = [px[1, 1], px[w - 2, 1], px[1, h - 2], px[w - 2, h - 2]]
bg = tuple(sorted(c[i] for c in corners)[1] for i in range(3))
mask = []
for yy in range(h):
for xx in range(w):
c = px[xx, yy]
mask.append(1 if max(abs(c[i] - bg[i]) for i in range(3)) > tol else 0)
return mask, (w, h)
def _resample_mask(mask, size, tw=24, th=24):
w, h = size
return [mask[(ty * h // th) * w + (tx * w // tw)] for ty in range(th) for tx in range(tw)]
def _mask_iou(a, b):
inter = sum(1 for x, y in zip(a, b) if x and y)
uni = sum(1 for x, y in zip(a, b) if x or y)
return (inter / uni) if uni else 0.0
def _region_wcag(img, box):
"""区域 WCAG 相对亮度均值(0–1 线性域——§2 明度带口径;region_luma 是 0–255 加权域不可混用)。"""
crop = img.crop(box).convert("RGB")
px = crop.load()
tot = n = 0
for yy in range(0, crop.size[1], 2):
for xx in range(0, crop.size[0], 2):
tot += wcag_lum(px[xx, yy])
n += 1
return tot / max(1, n)
def run_screen_checks(scr, zones, board_origin, frames_strip, frames_meta, s6_img, s6_meta):
"""整屏样张闸四项 + 战斗表现五项(战斗表现「一屏样张的验收标准」)+护甲图标挂载对比锚。"""
# S1 整屏①全部文字对直接背景 ≥4.5:1(静态对表——对值即图内实际用色;大字档 3:1 由 A3a 承载)
pairs = [("暖白×舞台底", (240, 226, 200), (62, 52, 42)), ("金亮徽标×舞台底", (224, 184, 96), (62, 52, 42)),
("暖白×明细带底", (240, 226, 200), (46, 39, 32)), ("标题墨×C区板", (30, 26, 22), (232, 220, 196)),
("暖白×名字行素底", (240, 226, 200), (40, 34, 32)), ("金深墨×C区板", (116, 80, 11), (232, 220, 196)),
("深化石×C区板", (85, 77, 65), (232, 220, 196)), ("暖白×工具钮", (232, 222, 200), (90, 80, 66))]
ratios = [(n, round((max(wcag_lum(a), wcag_lum(b)) + 0.05) / (min(wcag_lum(a), wcag_lum(b)) + 0.05), 2))
for n, a, b in pairs]
weakest = min(ratios, key=lambda r: r[1])
check("S1", "整屏①全部文字对直接背景 ≥4.5:1(WCAG AA)", all(r[1] >= 4.5 for r in ratios),
"最弱 %s=%.2f;全部 %s" % (weakest[0], weakest[1], ratios))
# S2 整屏②明度三档带 + 世界底层两子带(§2;舞台=实测区域读数,格底=实测板面;WCAG 0–1 域)
stage_l = _region_wcag(scr, (300, 190, 600, 220))
bx, by = board_origin
board_span = 10 * CELL_D + 8
floor_l = _region_wcag(scr, (bx + 2 * CELL_D, by + 3 * CELL_D + 10, bx + 4 * CELL_D, by + 4 * CELL_D - 10))
info_l = wcag_lum(INK_L_GOLD)
mid_l = wcag_lum(PLAZA_BODY)
ok_s2 = (info_l > mid_l > stage_l and 0.08 <= floor_l <= 0.14 and 0.014 <= stage_l <= 0.06
and 0.22 <= mid_l <= 0.45 and (floor_l - stage_l) >= 0.02)
check("S2", "整屏②明度三档带成立(信息>世界中层>世界底层)+世界底层两子带(0.014–0.06/0.08–0.14,间隔≥0.02)",
ok_s2, "信息 %.2f/中(地台) %.2f/底层 舞台 %.3f+格底 %.3f/子带间隔 %.3f"
% (info_l, mid_l, stage_l, floor_l, floor_l - stage_l))
# S3 整屏③首焦唯一(§3.4 整屏延伸:高对比内容量集中单区 ≥55%——挖掘期第一注视点=棋盘区
# 〔battle.md 信息层级〕;候选区=数字区/B 区立绘,C 区规划面与工具行抢焦即违)
top = _grad_top_cells(scr, 10)
bx, by = board_origin
board_span = 10 * CELL_D + 8
b_rect = zones["b"]
c_rect = zones["c"]
e_board = e_b = e_c = e_tool = 0
for g, x, y in top:
if bx <= x < bx + board_span and by <= y < by + board_span:
e_board += g
elif b_rect[0] <= x < b_rect[2] and b_rect[1] <= y < b_rect[3]:
e_b += g
elif c_rect[0] <= x < c_rect[2] and c_rect[1] <= y < c_rect[3]:
e_c += g
elif zones["d"][1] <= y < zones["d"][1] + 30:
e_tool += g
total = e_board + e_b + e_c + e_tool
shares = {"数字区": e_board / max(1, total), "B 区": e_b / max(1, total),
"C 区": e_c / max(1, total), "工具行": e_tool / max(1, total)}
winner = max(shares, key=shares.get)
check("S3", "整屏③首焦唯一(高对比内容量集中单区 ≥55%,无第二焦点竞争;候选=数字区/B 区立绘)",
shares[winner] >= 0.55 and winner in ("数字区", "B 区"),
"能量占比 %s(首焦=%s)" % ({k: round(v, 2) for k, v in shares.items()}, winner))
# S4 整屏④文字禁直接压纹理底(垫容器/罩层):素底牌顶右上角无字形区平底 + 全牌主色占比
# + 牌区零地台板身色(§6.1/§7.2 同文断言的样张侧)+明细带平底
d_ = ImageDraw.Draw(Image.new("RGB", (1, 1)))
name_w = d_.textbbox((0, 0), "地精", font=F_NAME)[2]
plate_w = 24 + 3 + name_w + 8
px0 = 818 + (150 - plate_w) / 2
py0 = 34 + 132
plate_crop = scr.convert("RGB").crop((int(px0), int(py0), int(px0 + plate_w), int(py0) + 26))
all_px = list(plate_crop.getdata())
from collections import Counter as _Counter
main_share = _Counter(all_px).most_common(1)[0]
flat_crop = plate_crop.crop((plate_crop.size[0] - 10, 1, plate_crop.size[0] - 2, 7))
flat_px = list(flat_crop.getdata())
flat_plate = sum(1 for c in flat_px if c == (40, 34, 32)) / max(1, len(flat_px))
rgbp = scr.convert("RGB").load()
plinth_hit = sum(1 for yy in range(int(py0), int(py0) + 26) for xx in range(int(px0), int(px0 + plate_w))
if rgbp[xx, yy][:3] == PLAZA_BODY)
band_flat = region_luma(scr, (700, 232, 1100, 246))
band_expect = sum(w * c for w, c in zip((0.2126, 0.7152, 0.0722), (46, 39, 32)))
check("S4", "整屏④文字禁直接压纹理底(名字行=素底牌平底+牌区零地台板身色+明细带平底)",
flat_plate >= 0.95 and main_share[0] == (40, 34, 32) and main_share[1] >= len(all_px) * 0.6
and plinth_hit == 0 and abs(band_flat - band_expect) < 3,
"牌顶角平底 %.0f%%;主色 %s 占 %.0f%%;牌区地台色命中 %d;明细带 L %.2f"
% (flat_plate * 100, main_share[0], main_share[1] * 100.0 / len(all_px), plinth_hit, band_flat))
# P1 战斗表现①棋盘区零打扰(数字格砖心零战斗元素签名色;数字对比硬指标由 A3a 承载。
# 签名表不含 CONTACT_GLOW——其色与数字冷亮墨 INK_L_COLD 差 <24 不可色分,空间约束由 F5a 承载)
sigs = list(GLOW_GROUPS.values()) + [DANGER_RED]
viol = []
for (dx_, dy_), dg in DIGITS.items():
if dg <= 0:
continue
x0, y0 = bx + dx_ * CELL_D, by + dy_ * CELL_D
m = int(CELL_D * 0.2)
for yy in range(y0 + m, y0 + CELL_D - m, 2):
for xx in range(x0 + m, x0 + CELL_D - m, 2):
c = rgbp[xx, yy][:3]
if any(all(abs(c[i] - s_[i]) <= 24 for i in range(3)) for s_ in sigs):
viol.append(((dx_, dy_), c))
break
check("P1", "战斗表现①棋盘区零打扰(数字格砖心零战斗元素签名色;挖掘期抢焦禁令由 S3 首焦承载)",
not viol, "违例 %s" % viol if viol else "%d 个数字格砖心全净" % len(DIGITS))
# P2 战斗表现②剪影跨舞台通用(灰度轮廓通道——S6 三语境隔离剪影两两 IoU ≥0.35)
masks = [_mask_maxch_dist(s6_img, box) for box in s6_meta["hero_boxes"]]
rm = [_resample_mask(m, s) for m, s in masks]
ious = [round(_mask_iou(rm[0], rm[1]), 2), round(_mask_iou(rm[0], rm[2]), 2), round(_mask_iou(rm[1], rm[2]), 2)]
check("P2", "战斗表现②剪影跨舞台通用(灰度三语境两两 IoU ≥0.35——识别只靠轮廓通道)",
min(ious) >= 0.35,
"舞台×棋盘 %s/舞台×明细 %s/棋盘×明细 %s" % tuple(ious))
# P3 战斗表现③动画方案可读(三组关键帧 ≥3 帧、相邻帧互异、退场末帧=灰化残位非瞬消)
fw, fh = frames_meta["frame_size"]
distinct, death_ok, death_note = True, True, ""
for key, (xs, y0) in frames_meta["strips"].items():
crops = [frames_strip.crop((x, y0, x + fw, y0 + fh)).convert("RGB") for x in xs]
for c1, c2 in zip(crops, crops[1:]):
if list(c1.getdata()) == list(c2.getdata()):
distinct = False
if key == "death":
sat = list(crops[2].convert("HSV").split()[1].getdata())
ink = sum(1 for p in crops[2].getdata() if p != (34, 29, 24))
death_ok = max(sat) <= 90 and ink > 200
death_note = "末帧最大饱和 %d/残位像素 %d" % (max(sat), ink)
check("P3", "战斗表现③动画方案可读(三组关键帧条各 3 帧+相邻帧互异+退场末帧灰化残位禁瞬消)",
distinct and death_ok, death_note or "三组帧互异")
# P4 战斗表现④战斗空间成立(双舞台结构边界在位:C/D 顶缘描边连续+区间净缝+明细带在位)
c_y = zones["c"][1]
def line_hit(y, x0, x1, col, tol=30):
n = hits = 0
for x in range(x0, x1, 4):
n += 1
if all(abs(rgbp[x, y][i] - col[i]) <= tol for i in range(3)):
hits += 1
return hits / max(1, n)
ok_p4 = (line_hit(c_y, 246, 1276, UIKIT["PANEL_BORDER"]) >= 0.9
and line_hit(c_y, 2, 238, UIKIT["PANEL_BORDER"]) >= 0.9
and all(rgbp[x, B_H + 2][:3] == (24, 20, 17) for x in range(250, 1270, 60))
and line_hit(B_H - 2, 250, 1270, (46, 39, 32)) >= 0.9)
check("P4", "战斗表现④战斗空间成立(双舞台边界线在位+区间净缝;视线锚归 S3 首焦唯一)", ok_p4,
"C/D 顶缘描边与净缝读数见 detail")
# P5 战斗表现⑤敌层插入位置可裁(遮挡对照:顶层 数字/印记/小人 可辨、敌像垫底可辨)
ox0, oy0, ox1, oy1 = s6_meta["overlap"]
crop = s6_img.crop((ox0, oy0, ox1, oy1)).convert("RGB")
cpx = crop.load()
def count_near(col, tol=28):
return sum(1 for yy in range(crop.size[1]) for xx in range(crop.size[0])
if all(abs(cpx[xx, yy][i] - col[i]) <= tol for i in range(3)))
cnt = {"数字墨": count_near(INK_L_WARM), "封印黑": count_near(SEAL_BLACK, 12),
"小人蓝": count_near(HERO_BLUE), "敌像灰": count_near((110, 104, 98), 14)}
ok_p5 = cnt["数字墨"] >= 25 and cnt["封印黑"] >= 8 and cnt["小人蓝"] >= 20 and cnt["敌像灰"] >= 10
check("P5", "战斗表现⑤敌层插入位置可裁(遮挡对照组:数字>小人>旗>敌,顶层三件可辨计数达标)", ok_p5,
"计数 %s" % cnt)
# A7 护甲三枚对挂载素底 ≥3:1(§5.3 共用验收——色值=产出件常量,尺寸/装载由 V8/V9 单测承载)
a_mount = wcag_lum((40, 34, 32))
a_ratios = [(k, round((max(wcag_lum(c), a_mount) + 0.05) / (min(wcag_lum(c), a_mount) + 0.05), 2))
for k, c in [("轻甲", ARMOR_LIGHT_COL), ("魔甲", ARMOR_WARD_COL), ("重甲", ARMOR_HEAVY_COL)]]
check("A7", "护甲三枚对挂载底色(名字行素底 #282220)图形对比 ≥3:1(§5.3 共用验收)",
all(r[1] >= 3.0 for r in a_ratios), " %s" % a_ratios)
return all(c[2] for c in CHECKS)
def cg_board_region(board, x0, y0, c):
return board.crop((x0, y0, x0 + c, y0 + c)).convert("RGB")
@@ -1090,26 +1390,263 @@ def render_s3(cell=64):
return Image.merge("HSV", (h, s, v)).convert("RGB").convert("RGBA")
def render_s3b(cell=64):
"""S3b 图类型色板变体第二套(§1.4 内容清单「至少两套」的第三套槽位;§4.3 四槽换色示意——
deep_underdark 方向候选:灰黑主调+矿脉金属,§6.3 终值未冻结本套为方向示意)。信息层恒定:
数字亮墨在换色后重画;微光取与 S3 不同组(blast 覆盖另一组互辨)。"""
rows = ["WWWW", "WFFW", "WFWF"]
gmap = GMap(rows)
board, placement, glow = render_board(gmap, cell, overlays=False)
gd = Image.new("RGBA", board.size, (0, 0, 0, 0))
draw_type_glow(gd, 1 * cell, 2 * cell, cell, "blast")
board.alpha_composite(gd)
board = draw_slab_cliff(board, cols=gmap.w)
h, s, v = board.convert("HSV").split()
h = h.point(lambda _: 12)
s = s.point(lambda x: min(255, int(10 + x * 0.30)))
v = v.point(lambda x: int(x * 0.90))
out = Image.merge("HSV", (h, s, v)).convert("RGB").convert("RGBA")
draw_digit_direct(out, 2 * cell, 1 * cell, cell, 5, F_DIGIT) # 信息层恒定——换色后重画
return out
def render_keyframes():
"""S5 三组关键帧条(战斗表现「一屏样张的验收标准」内容清单:受击/退场/高光兑现,
每组至少起中末三帧;退场末帧=灰化残位——「死亡禁止瞬消」机器代理;每组标注减动效简化形态)。
返回 (strip, meta)——meta[key]=(行 x 列表, 行 y0),供 P3 断言按 bbox 取样。"""
fw, fh, gap = 110, 116, 16
labels = [("hit", "受击:常态→闪白+火花→回位(减动效=时长减半)"),
("death", "退场:常态→灰化+碎块→塌陷残位(减动效=直达残位;禁瞬消)"),
("highlight", "高光兑现:行→金框强调→定格(减动效=直达终态)")]
row_h = fh + 30
strip = Image.new("RGBA", ((fw + gap) * 3 + gap, row_h * 3 + 6), (24, 20, 17, 255))
d = ImageDraw.Draw(strip)
meta = {"frame_size": (fw, fh), "strips": {}}
def base_frame(sx, sy, col_body, flash=0, sparks=0, shards=(), sink=0, alpha=255, gold=0):
x0, y0 = sx, sy + 4
d.rectangle([x0, y0, x0 + fw, y0 + fh], fill=(34, 29, 24), outline=(120, 106, 88))
cy = y0 + 58 + sink
if gold == 2:
d.rectangle([x0 + 4, y0 + fh - 34, x0 + fw - 4, y0 + fh - 6], fill=(240, 168, 40), outline=(224, 184, 96), width=2)
elif gold == 1:
d.rectangle([x0 + 4, y0 + fh - 34, x0 + fw - 4, y0 + fh - 6], outline=(224, 184, 96), width=2)
else:
d.rectangle([x0 + 4, y0 + fh - 34, x0 + fw - 4, y0 + fh - 6], outline=(120, 106, 88), width=1)
d.text((x0 + 10, y0 + fh - 20), "伤害 4 → 地精", font=F_LBL2, fill=(240, 226, 200))
col = (col_body[0], col_body[1], col_body[2], alpha)
d.text((x0 + fw // 2, cy), "▆", font=F_STANDEE, fill=col, anchor="mm")
for k, (ox_, oy_) in enumerate(shards):
d.rectangle([x0 + fw // 2 + ox_, cy + oy_, x0 + fw // 2 + ox_ + 6, cy + oy_ + 4],
fill=(110, 104, 98, alpha))
if sparks:
for k in range(sparks):
ang = 6.283 * k / sparks
d.ellipse([x0 + fw // 2 + 26 * math.cos(ang) - 2, cy - 14 * math.sin(ang) - 2,
x0 + fw // 2 + 26 * math.cos(ang) + 2, cy - 14 * math.sin(ang) + 2],
fill=(230, 190, 90))
if flash:
d.rectangle([x0 + 1, y0 + 1, x0 + fw - 1, y0 + fh - 1], fill=(255, 255, 255, flash))
xs = [gap + i * (fw + gap) for i in range(3)]
for gi, (key, label) in enumerate(labels):
y0 = 4 + gi * row_h
meta["strips"][key] = (list(xs), y0)
if key == "hit":
base_frame(xs[0], y0, (150, 60, 48))
base_frame(xs[1], y0, (150, 60, 48), flash=120, sparks=6)
base_frame(xs[2], y0, (150, 60, 48))
elif key == "death":
base_frame(xs[0], y0, (150, 60, 48))
base_frame(xs[1], y0, (110, 104, 98), shards=[(-18, 6), (14, 10), (-6, 14)], alpha=200)
base_frame(xs[2], y0, (110, 104, 98), sink=6,
shards=[(-26, 18), (20, 22), (-10, 26), (8, 20), (-2, 30)], alpha=150)
else:
base_frame(xs[0], y0, (150, 60, 48), gold=0)
base_frame(xs[1], y0, (150, 60, 48), gold=1)
base_frame(xs[2], y0, (240, 168, 40), gold=2)
d.text((xs[2] + fw + gap + 4, y0 + 46), label, font=F_LBL2, fill=(225, 215, 195))
return strip, meta
def _outline_underlay(src, outline_rgb):
"""alpha 边缘 1px 外扩描边垫层(主角深墨描边/敌方暖白描边共用)。"""
a = src.split()[3]
dil = a.filter(ImageFilter.MaxFilter(3))
edge = ImageChops.subtract(dil, a)
under = Image.new("RGBA", src.size, outline_rgb + (255,))
under.putalpha(edge)
return under
def render_outline_overlap():
"""S6 剪影跨舞台三语境(P2 灰度取样,隔离平底免污染)+描边同屏对照(主角深墨 (40,19,13)/
敌方暖白族——art-prompts §5.4 描边条款)+护甲三枚展示+遮挡对照组(P5——敌人姿态精灵与
数字/封印印记/小人同格叠放,绘制优先级 数字>小人>旗>敌)。返回 (img, meta)。"""
W, H = 940, 190
img = Image.new("RGBA", (W, H), (24, 20, 17, 255))
d = ImageDraw.Draw(img)
meta = {"hero_boxes": [], "overlap": None, "armor": {}}
# ① 战斗舞台语境:主角立绘 96px + 深墨描边
por = PORTRAIT_SRC.resize((96, 96), Image.NEAREST)
img.alpha_composite(_outline_underlay(por, (40, 19, 13)), (18, 14))
img.alpha_composite(por, (18, 14))
meta["hero_boxes"].append((18, 14, 18 + 96, 14 + 96))
d.text((18, 116), "主角·战斗舞台语境\n描边=深墨 (40,19,13)", font=F_LBL2, fill=(225, 215, 195))
# ② 棋盘格语境:小人 48px 压已开格底
d.rectangle([150, 14, 214, 78], fill=(24, 20, 16))
fl = FLOORS[0].resize((64, 64), Image.NEAREST)
img.paste(fl.convert("RGB"), (150, 14))
h48 = HERO.resize((48, int(48 * HERO.size[1] / HERO.size[0])), Image.NEAREST)
img.alpha_composite(h48, (150 + (64 - h48.size[0]) // 2, 14 + 64 - h48.size[1] - 8))
meta["hero_boxes"].append((150 + (64 - h48.size[0]) // 2, 14 + 64 - h48.size[1] - 8,
150 + (64 - h48.size[0]) // 2 + h48.size[0], 14 + 64 - 8))
d.text((150, 116), "棋盘格语境\n(隔离已开格底)", font=F_LBL2, fill=(225, 215, 195))
# ③ 结算明细带语境:小人 24px 压明细带底
d.rectangle([238, 14, 302, 78], fill=(46, 39, 32))
h24 = HERO.resize((24, int(24 * HERO.size[1] / HERO.size[0])), Image.NEAREST)
img.alpha_composite(h24, (238 + 8, 14 + 52 - h24.size[1]))
meta["hero_boxes"].append((238 + 8, 14 + 52 - h24.size[1], 238 + 8 + h24.size[0], 14 + 52))
d.text((238, 116), "结算明细带语境\n(同剪影 24px)", font=F_LBL2, fill=(225, 215, 195))
# ④ 敌方描边对照:字形多向偏移暖白垫底+本体深朱
for ddx, ddy in [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (1, -1), (-1, 1), (1, 1)]:
d.text((350 + 32 + ddx, 14 + 32 + ddy), "█", font=F_STANDEE, fill=(240, 226, 200), anchor="mm")
d.text((350 + 32, 14 + 32), "█", font=F_STANDEE, fill=(150, 60, 48), anchor="mm")
d.text((350, 116), "敌方描边=暖白族\n(非黑非深褐)", font=F_LBL2, fill=(225, 215, 195))
# ⑤ 护甲三枚展示(源 24×24 挂 HP_BAR_BACK 素底——工单 §5.3 挂载底色口径)
for i, key in enumerate(["light", "ward", "heavy"]):
ax = 470 + i * 60
d.rectangle([ax, 14, ax + 40, 54], fill=(40, 34, 32))
tex = _armor_tex(key)
if tex is not None:
img.paste(tex, (ax + 8, 22), tex)
meta["armor"][key] = (ax + 8, 22, ax + 8 + 24, 22 + 24)
d.text((ax + 4, 58), {"light": "轻甲", "ward": "魔甲", "heavy": "重甲"}[key], font=F_LBL2, fill=(225, 215, 195))
d.text((470, 116), "护甲三枚 24×24\n挂载底=名字行素底", font=F_LBL2, fill=(225, 215, 195))
# ⑥ 遮挡对照组:敌像(最底)→旗→小人+蓝框→数字(最顶)——P5 断言:顶层三件仍可辨
cx, cy0 = 700, 10
d.rectangle([cx, cy0, cx + 68, cy0 + 68], fill=(24, 20, 16))
img.paste(FLOORS[1].resize((64, 64), Image.NEAREST).convert("RGB"), (cx + 2, cy0 + 2))
d.text((cx + 34, cy0 + 30), "▆", font=ImageFont.truetype(ZH_FONT, 30), fill=(110, 104, 98), anchor="mm")
px_, py_ = cx + 30, cy0 + 12
d.line([(px_, py_), (px_, cy0 + 52)], fill=(222, 210, 186), width=2)
d.polygon([(px_, py_), (px_ + 14, py_ + 4), (px_, py_ + 9)], fill=SEAL_BLACK)
h28 = HERO.resize((24, int(24 * HERO.size[1] / HERO.size[0])), Image.NEAREST)
img.alpha_composite(h28, (cx + 36, cy0 + 40 - h28.size[1]))
d.rectangle([cx + 3, cy0 + 3, cx + 65, cy0 + 65], outline=HERO_BLUE, width=2)
d.text((cx + 14, cy0 + 46), "6", font=ImageFont.truetype(ZH_FONT, 24), fill=INK_L_WARM)
meta["overlap"] = (cx, cy0, cx + 68, cy0 + 68)
d.text((cx, cy0 + 76), "遮挡对照组:数字>小人>旗>敌\n(敌像最底,顶层三件可辨)", font=F_LBL2, fill=(225, 215, 195))
return img, meta
# ══ 素材程序化产出(--gen-assets;art-prompts §5.2/§5.3 工单,色值全取 v3.22 在册值零新增) ══
PLAZA_EDGE = (76, 64, 56) # 描边 #4C4038(§4.1 换色合并登记目标值)
PLAZA_RIM = (201, 175, 131) # 顶缘受光线 #C9AF83(掩尘同枚——合并登记)
PLAZA_BODY = (150, 130, 100) # 板身 #968264(STAND_STONE)
ARMOR_INK = (51, 38, 27) # 图标深墨描边 #33261B(主墨)
ARMOR_LIGHT_COL = (205, 184, 140) # 轻甲 #CDB88C(WARM_FAINT——换色合并⑤)
ARMOR_WARD_COL = (240, 226, 200) # 魔法护甲 #F0E2C8(暖白)
ARMOR_HEAVY_COL = (134, 140, 154) # 重甲 #868C9A(ARMOR_PLATE_COLD)
def _ink_outline_polygon(d, pts, fill, width=3):
"""深墨描边画法:先画 width+2 的墨线再画 width 的面色(=1px 墨缘,工单「1px 深墨描边」)。"""
d.line(pts, fill=ARMOR_INK, width=width + 2)
d.line(pts, fill=fill, width=width)
def gen_scene_plaza():
"""地台件 48×12(§5.2 工艺要点:1px 上描边+2px 顶缘受光线+8px 板身+1px 下描边;左右 1px 全周闭合)。"""
im = Image.new("RGBA", (48, 12), PLAZA_BODY + (255,))
d = ImageDraw.Draw(im)
d.rectangle([0, 0, 47, 11], outline=PLAZA_EDGE + (255,), width=1) # 全周闭合描边
d.rectangle([1, 1, 46, 2], fill=PLAZA_RIM + (255,)) # 顶缘受光线 2px(纵向不缩放区)
return im
def _armor_canvas():
return Image.new("RGBA", (24, 24), (0, 0, 0, 0))
def gen_armor_light():
"""轻甲 = 皮条/布带记号(斜跨皮带+扣件,甲面覆盖率≈0,主体留空——§5.3.1;3px 带宽 1px 墨描边)。"""
im = _armor_canvas()
d = ImageDraw.Draw(im)
_ink_outline_polygon(d, [(5, 18), (18, 6)], ARMOR_LIGHT_COL, width=3) # 斜跨皮带
d.rectangle([9, 9, 14, 14], outline=ARMOR_INK + (255,), width=1) # 皮带扣
d.rectangle([10, 10, 13, 13], fill=ARMOR_LIGHT_COL + (255,))
return im
def gen_armor_ward():
"""魔法护甲 = 符纹饰带(横向符纹带+两枚 ≤3×3 符点,零金属甲板——§5.3.2)。"""
im = _armor_canvas()
d = ImageDraw.Draw(im)
d.rectangle([4, 10, 19, 14], fill=ARMOR_INK + (255,))
d.rectangle([4, 11, 19, 13], fill=ARMOR_WARD_COL + (255,)) # 符纹带
d.rectangle([8, 17, 10, 19], fill=ARMOR_WARD_COL + (255,)) # 符点一(3×3)
d.rectangle([13, 4, 15, 6], fill=ARMOR_WARD_COL + (255,)) # 符点二(3×3)
return im
def gen_armor_heavy():
"""重甲 = 金属甲板覆盖肩与胸(肩两块+胸一块,覆盖率三档最大——§5.3.3)。"""
im = _armor_canvas()
d = ImageDraw.Draw(im)
for box in [(3, 3, 10, 9), (13, 3, 20, 9), (7, 12, 16, 21)]: # 肩×2+胸×1
d.rectangle(list(box), fill=ARMOR_INK + (255,))
d.rectangle([box[0] + 1, box[1] + 1, box[2] - 1, box[3] - 1], fill=ARMOR_HEAVY_COL + (255,))
return im
def gen_batch_assets():
"""产出四件素材并入包:地台件 scene/ + 护甲三枚 ui/icons/(§5.2/§5.3——程序化直出即终稿)。"""
out = []
plaza_path = os.path.join(ROOT, "game", "assets", "art", "scene", "scene_stage_plaza.png")
os.makedirs(os.path.dirname(plaza_path), exist_ok=True)
im = gen_scene_plaza()
im.save(plaza_path)
out.append((plaza_path, im.size, 3))
for key, fn in [("light", gen_armor_light), ("ward", gen_armor_ward), ("heavy", gen_armor_heavy)]:
p = os.path.join(ROOT, "game", "assets", "ui", "icons", "ui_armor_%s.png" % key)
im = fn()
im.save(p)
out.append((p, im.size, 2))
for p, size, ncol in out:
colors = len({c for c in Image.open(p).convert("RGBA").getdata() if c[3] > 0})
print("[gen] %s size=%s 不透明像素域色数=%d" % (p, size, colors))
if size not in ((48, 12), (24, 24)) or colors > 32:
raise RuntimeError("素材产出校验失败: %s" % p)
print("[gen] 四件素材产出完成(§5.2/§5.3 工单口径)")
return out
def main():
if "--gen-assets" in sys.argv:
gen_batch_assets()
return 0
print("=== 样张 v3 方向稿生成 + 机检(参考) ===")
board, placement, records, band, gmap = render_main_board()
canvas = Image.new("RGBA", (1560, 1560), (24, 20, 17, 255))
canvas = Image.new("RGBA", (1560, 2080), (24, 20, 17, 255))
ok = run_checks(board, placement, records, band, gmap, canvas)
d = ImageDraw.Draw(canvas)
d.text((20, 14), "战斗一屏样张方向稿 v3-draft(2026-09-29,art-bible v3.0)——格底分档+亮墨直画+色牌后备+B区看台+E格高墙立面(v3.1);机检 %s(方向稿参考,冻结归目检后机检批);巢穴纹=程序占位" %
("全绿" if ok else "有 FAIL"), col=(232, 222, 200), font=F_TITLE)
# 三区 mockup(T1 底板 + T2 内容;板面在 render_battle_screen 内嵌装)
scr, zones, board_origin = render_battle_screen(board)
if "--save-baseline" in sys.argv:
os.makedirs(os.path.dirname(BASELINE_PNG), exist_ok=True)
scr.convert("RGB").save(BASELINE_PNG)
print("saved(baseline):", BASELINE_PNG)
return 0
frames_strip, frames_meta = render_keyframes()
s6_img, s6_meta = render_outline_overlap()
run_screen_checks(scr, zones, board_origin, frames_strip, frames_meta, s6_img, s6_meta)
# 三区 mockup
scr = render_battle_screen()
# D 区主样张板(10×10@45px=450×450)嵌进 mockup D 区(工具行之下居中)
bx = 244 + (1036 - board.size[0]) // 2
by = 256 + 34 + (800 - 256 - 34 - board.size[1]) // 2
scr.paste(board.convert("RGB"), (bx, by), board)
d = ImageDraw.Draw(canvas)
d.text((20, 14), "战斗一屏样张方向稿 v3-draft(2026-09-29,art-bible v3.0;迭代 43 扩展:整屏两版+S1–S4/P1–P5 断言)——机检 %s(方向稿参考,冻结归目检后机检批)" %
("全绿" if ok else "有 FAIL"), col=(232, 222, 200), font=F_TITLE)
canvas.paste(scr, (20, 52))
d.text((20, 866), "三区 mockup(1280×800 实比):B 区站桩(主角立绘 48×48 朝右/敌字形占位 ▂▆█/顺位/意图/名行=护甲图标+纯名字)",
d.text((20, 866), "三区 mockup(1280×800 实比,B 区按代码 252):B 区站桩(主角立绘 96 朝右/敌字形占位/顺位 FS_HINT/意图素牌/血条=危险红/名行=素底牌+护甲 PNG)",
col=(225, 215, 195), font=F_LBL)
d.text((20, 886), "C 区攻击数据框+预留槽 · D 区主样张板(方案甲拼装/接触带微光=蓝缘带/悬停金框/封印印记/危险格巢穴占位)",
d.text((20, 886), "C 区攻击数据框+预留槽 · D 区主样张板(方案甲拼装/接触带微光/悬停金框/封印印记/危险格巢穴占位)",
col=(225, 215, 195), font=F_LBL)
# S2 数字全档
@@ -1121,11 +1658,15 @@ def main():
s2h = s2.resize((s2.size[0] // 2, s2.size[1] // 2), Image.NEAREST)
canvas.paste(s2h, (20, oy2 + 76))
# S3 underdark
# S3 / S3b 图类型色板变体两套(§1.4 内容清单:至少两套)
oy3 = oy2 + 76 + 60
s3 = render_s3()
canvas.paste(s3, (20, oy3))
d.text((20 + s3.size[0] + 14, oy3 + 8), "S3 图类型换色对照(underdark 冷调,§4.3 四槽换色示意):\n信息层恒定——亮墨三档/微光五组/封印黑/危险红同 S2",
s3b = render_s3b()
canvas.paste(s3b, (320, oy3))
d.text((630, oy3 + 8), "S3 图类型换色对照(underdark 冷调,§4.3 四槽换色示意):\n信息层恒定——亮墨三档/微光五组/封印黑/危险红同 S2",
col=(225, 215, 195), font=F_LBL)
d.text((630, oy3 + 78), "S3b 变体第二套(deep_underdark 方向候选:灰黑+矿脉金属;\n§6.3 终值未冻结,方向示意)——两套并陈满足 §1.4「至少两套」",
col=(225, 215, 195), font=F_LBL)
# S4 图例
@@ -1134,40 +1675,72 @@ def main():
for name, rgb in [("封印黑", SEAL_BLACK), ("危险红", DANGER_RED), ("奖励金", REWARD_GOLD),
("小人蓝(候选)", HERO_BLUE), ("接触带微光(候选)", CONTACT_GLOW)]:
d.rectangle([x, oy4, x + 34, oy4 + 34], fill=rgb, outline=(90, 80, 66))
d.text((x - 4, oy4 + 38), name, col=(225, 215, 195), font=F_LBL2) if False else d.text((x - 4, oy4 + 38), name, font=F_LBL2, fill=(225, 215, 195))
d.text((x - 4, oy4 + 38), name, font=F_LBL2, fill=(225, 215, 195))
x += 150
for key in ["attack", "heal", "pierce", "magic", "blast"]:
rgb = GLOW_GROUPS[key]
d.rectangle([x, oy4, x + 34, oy4 + 34], fill=rgb, outline=(90, 80, 66))
d.text((x - 4, oy4 + 38), GLOW_FAMILY[key] + "微光", font=F_LBL2, fill=(225, 215, 195))
x += 150
d.text((20, oy4 + 66), "机检断言=F1 方案甲拼装/F2 旗∈未开格/F3 脚锚自底 25%/F4 0 格无字形/F5 微光带限+F6 立面双明度带/F7 悬崖同源 + A1 色板禁入带/A2 周界闭合/A3 图底分离/A4 混排/A5 净空/A6 观感密度——详见机检报告;全过=四冻结,气质终审=批末用户目检",
col=(225, 215, 195), font=F_LBL2)
d.text((20, oy4 + 66), "机检断言=F1 方案甲拼装/F2 旗∈未开格/F3 脚锚自底 25%/F4 0 格无字形/F5 微光带限+F6 立面双明度带/F7 悬崖同源"
" + A1 色板禁入带/A2 周界闭合/A3 图底分离/A4 混排/A5 净空/A6 观感密度", col=(225, 215, 195), font=F_LBL2)
d.text((20, oy4 + 82), "整屏四项 S1–S4 + 战斗表现五项 P1–P5 + A7 护甲对比 + V1 两版出口/V10 基线并排——详见机检报告;"
"全过=机检冻结,气质终审=用户目检(方向稿目检先行)", col=(225, 215, 195), font=F_LBL2)
os.makedirs(os.path.dirname(OUT_PNG), exist_ok=True)
# S5 三组关键帧条 / S6 剪影三语境+描边对照+遮挡对照组
oy5 = oy4 + 104
canvas.paste(frames_strip, (20, oy5))
d.text((430, oy5 + 8), "S5 三组关键帧条(受击/退场/高光兑现,各起中末三帧;\n退场末帧=灰化残位——禁瞬消;每组标注减动效简化形态)", col=(225, 215, 195), font=F_LBL)
oy6 = oy5 + frames_strip.size[1] + 26
canvas.paste(s6_img, (20, oy6))
d.text((970, oy6 + 8), "S6 剪影跨舞台三语境(P2 灰度取样)+描边同屏对照\n(主角深墨/敌方暖白族)+护甲三枚展示+遮挡对照组(P5)", col=(225, 215, 195), font=F_LBL)
# 两版样张出口(V1)+改前/改后并排(V10)
os.makedirs(os.path.dirname(OUT_ED1), exist_ok=True)
canvas.convert("RGB").save(OUT_PNG)
print("saved:", OUT_PNG)
scr.convert("RGB").save(OUT_ED1)
print("saved:", OUT_ED1)
scale = 1080.0 / scr.size[1]
w2 = int(round(scr.size[0] * scale))
ed2 = Image.new("RGB", (1920, 1080), (0, 0, 0))
ed2.paste(scr.convert("RGB").resize((w2, 1080), Image.NEAREST), ((1920 - w2) // 2, 0))
ed2.save(OUT_ED2)
print("saved:", OUT_ED2)
check("V1", "样张两版产出(1280×800 基座 + 1920×1080=全局等比 1.35×+左右黑边,§8.2 1920 档策略)",
os.path.exists(OUT_ED1) and os.path.exists(OUT_ED2)
and Image.open(OUT_ED1).size == (1280, 800) and Image.open(OUT_ED2).size == (1920, 1080),
"ed1 %s / ed2 %s / 黑边各 %dpx" % (Image.open(OUT_ED1).size, Image.open(OUT_ED2).size, (1920 - w2) // 2))
has_base = os.path.exists(BASELINE_PNG)
if has_base:
ba = Image.new("RGB", (2570, 810), (24, 20, 17))
base_im = Image.open(BASELINE_PNG).convert("RGB")
ba.paste(base_im, (5, 5))
ba.paste(Image.open(OUT_ED1).convert("RGB"), (1285 + 5, 5))
ba.save(OUT_BEFORE_AFTER)
print("saved:", OUT_BEFORE_AFTER)
check("V10", "改前/改后并排对拍图产出(基线版与改后版两源样张尺寸一致+并排图生成)",
has_base and Image.open(BASELINE_PNG).size == (1280, 800) and os.path.exists(OUT_BEFORE_AFTER),
"baseline %s / ed1 1280×800 / 并排 %s" % (Image.open(BASELINE_PNG).size if has_base else "缺(先 --save-baseline)", OUT_BEFORE_AFTER))
# A5b 终稿净空(装配后画布——A5 原断言跑在空白底板上,此处补实际成图净空)
edge_hits_final = edge_content(canvas)
check("A5b", "装配成稿四缘 2px 净空(实际画幅,零溢出)", edge_hits_final == 0, "缘带内容像素 %d" % edge_hits_final)
ok_all = all(c[2] for c in CHECKS)
# 机检报告
os.makedirs(os.path.dirname(OUT_REPORT), exist_ok=True)
with open(OUT_REPORT, "w", encoding="utf-8") as f:
f.write("# 样张 v3 机检报告【冻结】(2026-09-30 批 A——v3.5 用户验收态断言定值落锁;气质终审已由 2026-09-29 用户验收豁免,台账 281)\n\n")
f.write("生成:`python tools/render_sample_sheet_v3.py`;v3.5 冻结口径:格底分档(世界底层档 L≈0.10,带 0.08–0.14)/亮墨直画三档/色牌后备/物件级装饰/B 区舞台;断言定值=A3b 图底分离 Δ≥30(实测 38)+A4 尺寸基线(立绘 46×48/小人源 32×32 硬等);F6 沿用 v3.4 双明度带口径 Δ≥18。\n\n")
f.write("## v1 否决原因 → 断言映射\n\n")
f.write("| 否决原因(2026-09-28 用户) | 断言承载 |\n|---|---|\n")
f.write("| ①显示太丑(总纲) | A6 观感密度(整板明度 std ≥50.9=v1 实测 44.31×1.15)+既有装饰变体 ≥2 种 |\n")
f.write("| ②封印色冲突 | 封印黑 #000000(9b6fcf6 已落,A1a 对表自动覆盖) |\n")
f.write("| ③立体感不够 | A3 阈值 40→55 + F6 顶面/立面双明度带 Δ≥18 + F7 整板悬崖 DEPTH_PX 同源 |\n")
f.write("| ④微光格只留微光 | F5c 微光段零 draw_inlay(d1dea82 已落) |\n\n")
f.write("# 样张 v3 机检报告(2026-09-30 批 A 断言定值 + 迭代 43 整屏扩展 S1–S4/P1–P5/V1/V10/A7/A5b)\n\n")
f.write("生成:`python tools/render_sample_sheet_v3.py`;v3.5 冻结口径:格底分档/亮墨直画三档/色牌后备/物件级装饰/B 区舞台;A3b 图底分离 Δ≥30;A4 尺寸基线(立绘 46×48/小人源 32×32 硬等)。"
"迭代 43 扩展:B 区按代码 252 出样;整屏四项与战斗表现五项断言落 CHECKS;两版样张出口与基线并排对拍。方向稿目检先行,机检冻结归目检通过后。\n\n")
f.write("## 断言明细\n\n")
f.write("| 断言 | 判据 | 结果 | 明细 |\n|---|---|---|---|\n")
for cid, name, okk, detail in CHECKS:
f.write("| %s | %s | %s | %s |\n" % (cid, name, "PASS" if okk else "**FAIL**", detail.replace("|", "/")))
f.write("\n结论:**%s**(%d/%d PASS)。v3 方向稿机检=可行性参考(A3 亮墨对比为硬指标);冻结 = 用户目检方向稿后机检批(v3.0 闸形:目检先行→机检冻结)。\n" %
("全绿" if ok else "有红灯", sum(1 for c in CHECKS if c[2]), len(CHECKS)))
f.write("\n结论:**%s**(%d/%d PASS)。\n" % ("全绿" if ok_all else "有红灯", sum(1 for c in CHECKS if c[2]), len(CHECKS)))
print("saved:", OUT_REPORT)
print("=== 机检结论: %s (%d/%d) ===" % ("全绿" if ok else "FAIL", sum(1 for c in CHECKS if c[2]), len(CHECKS)))
return 0 if ok else 1
print("=== 机检结论: %s (%d/%d) ===" % ("全绿" if ok_all else "FAIL", sum(1 for c in CHECKS if c[2]), len(CHECKS)))
return 0 if ok_all else 1
if __name__ == "__main__":
+77 -57
View File
@@ -11,11 +11,11 @@
# 回执即作废须重跑;跑闸时若代码面还带着未提交改动,回执会标 dirty_game,合回闸拒绝采信。
#
# 用法:tools/run_all_gates.sh [--code|--docs|--quick]
# --code 只跑代码域闸(00 冒烟测试/01 系统测试/02 单元与集成测试/03 探针 --click/
# 04 探针 --full/05 渲染闸/09 静态扫描/11 回放确定性对拍+10 熵增闸)
# --code 只跑代码域闸(00 冒烟测试/01 系统测试/02 单元与集成测试/03 场景探针/
# 05 渲染闸/09 静态扫描+10 熵增闸)
# ——本批改动含 game/ 下非 .md 的行为载体(代码/测试脚本/探针/场景/数据表/素材)时用。
# 关联档选闸(2026-10-29 用户裁,推广自 2026-10-28 影响面选闸):00 冒烟/01 系统测试/
# 09 静态扫描三件保险恒跑不降;03/04 探针与 05 渲染闸维持影响面口径(本批 game/ 改动
# 09 静态扫描三件保险恒跑不降;03 场景探针与 05 渲染闸维持影响面口径(本批 game/ 改动
# 全在测量面 game/tests、game/sim 时记 SKIP);02 单元与集成改「选套件跑」——套件集=
# 需求卡片申报 ∪ diff 推导并集(算法与套件族映射表见脚本中部),横切面(公共件/数据表/
# autoload/数值·存档·RNG·结算触面)回落全套件;回执 scope 字段记 full|related。
@@ -35,7 +35,7 @@
# 基线红 + 现红且清单未变 → 「存量红·对基线零新增」,不判失败(但会在回执里留名)
# 基线红 + 现红但清单新增 → 红(新增红项)
# 任意闸 exit 与基线不符 → 红
# 本次未跑的闸(--quick 跳过/条件闸件未触面)→ 不比闸况,只记进回执 not_run
# 本次未跑的闸(--quick 跳过/影响面选闸 SKIP)→ 不比闸况,只记进回执 not_run
# 基线文件首次自动生成(把当次结果当基线),此后每次跑批与之比。
# 分域跑批只比本域跑过的闸——域外闸不入 NAMES,不产生「基线绿 → 现红」的假变化。
#
@@ -157,8 +157,8 @@ fi
declare -a NAMES=() EXITS=() LOGS=()
FAILED=0
# PARITY 触面表——gate 11 数值·存档·RNG·结算触面判定与关联档横切判定共用同一清单(单点定义,勿在别处复制)。
PARITY_TOUCH_FACES="
# 横切触面表——关联档选闸的横切回落判定清单(数值·存档·RNG·结算触面;单点定义,勿在别处复制)。
CROSSCUT_TOUCH_FACES="
game/data/tables/
game/core/rng_service.gd
game/core/save_manager.gd
@@ -173,7 +173,7 @@ game/sim/adventure_monte_carlo.gd
# ── 套件族映射表(关联档·推导半边的唯一事实源)──
# game/ 代码面文件 → tests/ 套件族词(--only 子串词,词=套件目录名)。机械近似:同族内
# 套件覆盖面仍可能宽于被测文件,保守方向=宁多勿漏。横切判定(公共件前缀/project.godot/
# PARITY 触面表)先于本表执行,故表中不收 damage_math/damage_controller/battle_orchestrator/
# 横切触面表)先于本表执行,故表中不收 damage_math/damage_controller/battle_orchestrator/
# settlement/** 等数值·存档·RNG·结算触面文件——它们恒走横切回落全量。映射不到的文件返回空=横切处理。
suite_terms_for() { # suite_terms_for <仓库相对路径> → stdout: 套件族词(空格分隔,可空)
case "$1" in
@@ -217,8 +217,8 @@ suite_terms_for() { # suite_terms_for <仓库相对路径> → stdout: 套件
}
# ── 关联档选闸(2026-10-29 用户裁,推广自 2026-10-28 影响面选闸)──
# 三件保险恒跑不降:00 冒烟/01 系统测试/09 静态扫描(10 熵增闸照旧两域都跑,11 数值·存档·RNG·结算触面判定照旧)。
# 03/04 探针与 05 渲染闸维持影响面口径:本批 diff 相对 master 分叉点的 game/ 改动若全部落在
# 三件保险恒跑不降:00 冒烟/01 系统测试/09 静态扫描(10 熵增闸照旧两域都跑)。
# 03 场景探针与 05 渲染闸维持影响面口径:本批 diff 相对 master 分叉点的 game/ 改动若全部落在
# 测量面(game/tests/**、game/sim/**——测试与模拟驱动件,不触场景树与渲染输出)则记 SKIP。
# 02 单元与集成改「选套件跑」:套件集=申报 ∪ 推导 取并集——
# 申报:在册需求卡片(本批 diff 触及的 reports/需求卡片*.md ∪ roadmap「进行中」迭代行登记的
@@ -228,7 +228,7 @@ suite_terms_for() { # suite_terms_for <仓库相对路径> → stdout: 套件
# 映射不到的文件一律按横切处理。
# 横切回落全量:diff 命中 game/data/tables/**、公共件前缀(config_db*/rng_service*/
# save_manager*/meta_progression*/main*——main* 覆盖组合根拆件与主场景)、
# autoload 注册面(game/project.godot)、或下方 PARITY 触面表任一 → 02 跑全部套件。
# autoload 注册面(game/project.godot)、或下方横切触面表任一 → 02 跑全部套件。
# 测量面批(game/ 改动全在 tests/sim):02 照旧全套件跑(不加过滤)。
# 回执 scope 字段:full|related(02 跑子集=related);映射表只在本脚本一处定义。
NONBEHAVIOR=0
@@ -241,7 +241,7 @@ if in_domain code; then
_behavior="$(echo "$_gfiles" | grep -vE '^(game/tests/|game/sim/)' || true)"
if [ -n "$_gfiles" ] && [ -z "$_behavior" ]; then
NONBEHAVIOR=1
echo "[gates] 影响面选闸:game/ 改动全在测量面(tests/sim)——03/04 探针/05 渲染闸 → SKIP(00/01/09 三件保险恒跑)"
echo "[gates] 影响面选闸:game/ 改动全在测量面(tests/sim)——03 场景探针/05 渲染闸 → SKIP(00/01/09 三件保险恒跑)"
fi
# ── 02 选套件推导(只对行为面代码文件做——测量面文件不进推导,其验证就是 02/09 自身)──
_codefiles="$(echo "$_gfiles" | grep -viE '\.md$' | grep -vE '^(game/tests/|game/sim/)' || true)"
@@ -257,7 +257,7 @@ if in_domain code; then
CROSSCUT=1; break ;;
esac
[ "$_f" = "game/project.godot" ] && { CROSSCUT=1; break; } # autoload 注册面
for _pat in $PARITY_TOUCH_FACES; do
for _pat in $CROSSCUT_TOUCH_FACES; do
case "$_f" in
"$_pat"*) CROSSCUT=1; break 2 ;;
esac
@@ -300,6 +300,24 @@ if in_domain code; then
fi
fi
# ── 代码域起手自检(闸承载:只省烧闸,不做合回判定——真判定在 00–11 与合回 guard)──
if in_domain code; then
# worktree 素材同步:素材走 gitignore 不入 git,worktree 冷检出缺件=呈现断言红的环境成因;
# 主工作区直跑=脚本内自动无操作。
bash "$REPO/tools/sync_worktree_assets.sh"
# 熵增预检:垃圾目录/越界文件先于长跑批暴露(熵增闸在批末,撞上才红=整轮白烧)。
if ! "$PY" "$REPO/tools/repo_structure_audit.py" >"$STAMP_DIR/entropy_preflight.log" 2>&1; then
echo "[gates] 起手自检红:熵增预检不过——顶层目录越界/根文件异动,先清理再跑闸(日志 $STAMP_DIR/entropy_preflight.log)"
grep -aE "越界|✗|违例" "$STAMP_DIR/entropy_preflight.log" | tail -5 | sed 's/^/ | /'
exit 1
fi
# master 推进预警(worktree 才会触发):master 已越过本分支=合回 guard 第 6 况必拦,
# 先并 master 再出闸,省一轮白烧;本闸回执照出(开发期跑批不受阻)。
if [ -f "$RECEIPT" ] && ! git -C "$REPO" merge-base --is-ancestor master HEAD 2>/dev/null; then
echo "[gates] 警告:master 已推进至本分支之外——本闸回执照出,但合回前须并 master 重出闸。"
fi
fi
echo "闸机跑批 · 域=$DOMAIN · 仓库 $REPO"
echo "起跑 commit: $(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || echo '非git目录')"
echo "python: $PY"
@@ -322,24 +340,58 @@ gate() { # gate <名称> <日志名> <命令...>
return 0
}
gate_unit() { # run_all 是 --script 模式,退出码为唯一判据
gate_unit() { # run_all 是 --script 模式,退出码为唯一判据;分片态=多 Godot 进程各跑一片
local name="02 单元与集成测试"
# 关联档选跑:把套件词列表经 --only= 传给 run_all(闸机来源标记 --only-source=gates,
# 入口据此打关联档横幅、不落「仅供开发期」免责句);全量态不传任何选跑参数。
local -a _user_args=()
if [ "$SCOPE" = related ] && [ -n "$ONLY_TERMS" ]; then
_user_args=(-- "--only=${ONLY_TERMS// /,}" --only-source=gates)
_user_args+=("--only=${ONLY_TERMS// /,}" "--only-source=gates")
fi
local _shards="${GATE_SHARDS:-4}"
echo ""
echo "════════════════════════════════════════════"
echo " $name(--script 模式,退出码为唯一判据)"
if [ "$SCOPE" = related ] && [ -n "$ONLY_TERMS" ]; then
echo " 关联档选跑:--only=$ONLY_TERMS"
fi
if [ "$_shards" -gt 1 ]; then
echo " 分片并行:$_shards 进程(GATE_SHARDS 可调,=1 回落单进程原路径)"
fi
echo "════════════════════════════════════════════"
( cd "$REPO" && "$GODOT" --headless --path "$(cygpath -m "$GAME")" --script res://tests/run_all.gd "${_user_args[@]}" ) \
>"$STAMP_DIR/unit.log" 2>&1
local rc=$?
local rc=0
if [ "$_shards" -le 1 ]; then
local -a _cmd=(--headless --path "$(cygpath -m "$GAME")" --script res://tests/run_all.gd)
[ ${#_user_args[@]} -gt 0 ] && _cmd+=(-- "${_user_args[@]}")
( cd "$REPO" && "$GODOT" "${_cmd[@]}" ) \
>"$STAMP_DIR/unit.log" 2>&1
rc=$?
else
# 分片并行:串行道套件由 run_all 内部分配钉第 1 片(RNG 日志会话跨片互踩,见其头注);
# 各片独立日志,退出码聚合(任一片非 0=闸红);日志按片序合并供错误扫描与指纹取材
#(指纹只取 FAIL 行排序集合、绿闸记 green,片序与合并不改变指纹)。
local -a _pids=()
local k
for k in $(seq 1 "$_shards"); do
( cd "$REPO" && "$GODOT" --headless --path "$(cygpath -m "$GAME")" --script res://tests/run_all.gd \
-- "${_user_args[@]}" "--shard=$k" "--shard-count=$_shards" ) \
>"$STAMP_DIR/unit_shard_$k.log" 2>&1 &
_pids+=($!)
done
local _shard_rc _any=0
for k in $(seq 1 "$_shards"); do
wait "${_pids[$((k - 1))]}"
_shard_rc=$?
echo "[run_all] 分片 $k/$_shards 退出码 $_shard_rc"
[ "$_shard_rc" -ne 0 ] && _any=1
done
[ "$_any" -ne 0 ] && rc=1
: >"$STAMP_DIR/unit.log"
for k in $(seq 1 "$_shards"); do
echo "── unit 分片 $k/$_shards ──" >>"$STAMP_DIR/unit.log"
cat "$STAMP_DIR/unit_shard_$k.log" >>"$STAMP_DIR/unit.log"
done
fi
# 三硬闸之二(脚本错误扫描):子套件的 SCRIPT ERROR/Parse Error 只写 stderr,入口内收不到,
# 只能在编排层扫本闸日志。命中即红——此类错误不改退出码、且会中断所在函数的剩余断言。
if [ "$rc" -eq 0 ] && ! "$PY" "$REPO/tools/error_scan.py" "$STAMP_DIR/unit.log" >>"$STAMP_DIR/unit.log" 2>&1; then
@@ -360,15 +412,12 @@ gate "00 冒烟测试" "smoke_boot.log" "$PY" "$REPO/tools/run_smoke.py"
gate "01 系统测试" "system_test.log" "$PY" "$REPO/tools/run_system_test.py"
# ── 2. 单元与集成测试 ──
gate_unit
# ── 3. 场景探针 · 行为走查段 ──
# ── 4. 场景探针 · 断言段(iter27 漏跑的就是这一段)──
# ── 3. 场景探针(走查+断言十件一次跑齐;iter27 漏跑断言段事故后固化为无条件整闸)──
if [ $NONBEHAVIOR -eq 1 ]; then
NAMES+=("03 探针 --click"); EXITS+=("SKIP"); LOGS+=("")
NAMES+=("04 探针 --full"); EXITS+=("SKIP"); LOGS+=("")
echo ""; echo "[gates] 03/04 探针 → SKIP(影响面选闸:测量面改动不触场景树与探针断言面)"
NAMES+=("03 场景探针"); EXITS+=("SKIP"); LOGS+=("")
echo ""; echo "[gates] 03 场景探针 → SKIP(影响面选闸:测量面改动不触场景树与探针断言面)"
else
gate "03 探针 --click" "probe_click.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --click
gate "04 探针 --full" "probe_full.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --full
gate "03 场景探针" "probe.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --click --full
fi
# ── 5. 渲染面(窗口化,依赖 PIL)──
if [ $QUICK -eq 1 ] || [ $NONBEHAVIOR -eq 1 ]; then
@@ -392,35 +441,6 @@ gate "08d 引用可核" "doc_ref.log" "$PY" "$REPO/tools/doc_ref_audit.py" --str
# ── 8e. 能力审计(GDD 纯度+能力清单与覆盖域表结构,判据=test-conventions §8.9+doc-conventions §5)──
gate "08e 能力审计" "capability.log" "$PY" "$REPO/tools/capability_audit.py" --strict
fi
# ── 6c. 回放确定性对拍(条件闸件;属代码域)──
if in_domain code; then
# 触面判定:本批 diff 相对 master 分叉点,命中 PARITY_TOUCH_FACES(脚本上部单点定义)之一即「触面」。
# 触面也**不自动跑**——对拍调的是 MC 脚本,属「MC/统计类跑批发起前须用户批准」红线管辖;
# 闸只负责每次跑批都把触面判定做一遍并显式要求批准,杜绝「该跑却没人记得」。
# 用户批准后设 PARITY_APPROVED=1 重跑本脚本即实跑三变体对拍。
_base="$(git -C "$REPO" merge-base master HEAD 2>/dev/null || true)"
_changed_files="$(git -C "$REPO" diff --name-only "${_base:-HEAD}" HEAD 2>/dev/null || true)"
_touch_hit=0
for _f in $_changed_files; do
for _pat in $PARITY_TOUCH_FACES; do
case "$_f" in
"$_pat"*) _touch_hit=1 ;;
esac
done
done
_parity_name="11 回放确定性对拍"
if [ "$_touch_hit" -eq 0 ]; then
NAMES+=("$_parity_name"); EXITS+=("SKIP"); LOGS+=("")
echo ""; echo "[gates] $_parity_name → SKIP(本批 diff 未触数值/存档/RNG/结算面)"
elif [ "${PARITY_APPROVED:-0}" = "1" ]; then
gate "$_parity_name" "parity.log" "$PY" "$REPO/tools/parity_anchor.py" check
else
NAMES+=("$_parity_name"); EXITS+=("NEED-APPROVAL"); LOGS+=("")
echo ""
echo "[gates] $_parity_name → 本批 diff 触及数值/存档/RNG/结算面,对拍需用户批准发起"
echo " (批准后设 PARITY_APPROVED=1 重跑本脚本即实跑;不得借机重锚)"
fi
fi
# ── 7. 静态扫描(代码域)+ 熵增闸(两域都跑:它查顶层目录/根文件/图像白名单/退役路径,
# 文档批新增或挪动 design/ 下的文件同样会绊它;纯文本扫描,秒级)──
@@ -471,9 +491,9 @@ def fingerprint(exit_code, log):
"""红项指纹:只取 exit!=0 时的 FAIL 关键行集合,忽略时序/耗时噪声
(超时挂死行的 pid=NNN 随跑批变化,归一化为 pid=N 后再入哈希——否则存量红指纹逐轮漂移)。
绿闸一律记 'green'——绿闸日志里的「零红」「全绿」等字样不是红项,采进来只会造噪声。"""
# SKIP / NEED-APPROVAL 都属「本闸未实跑」状态,指纹一律记 skip——
# 两者互变不是闸况变化(同是没跑),只有「未跑 → 跑出结果」或「绿 → 红」才算变化。
if exit_code in ("0", "SKIP", "NEED-APPROVAL"):
# SKIP 属「本闸未实跑」状态(--quick/影响面选闸),指纹一律记 skip——
# 没跑不是闸况变化,只有「未跑 → 跑出结果」或「绿 → 红」才算变化。
if exit_code in ("0", "SKIP"):
return "green" if exit_code == "0" else "skip"
if not log:
return "red-nolog"
@@ -514,7 +534,7 @@ for n in names:
if b["exit"] == c["exit"] and b["fp"] == c["fp"]:
continue
if c["fp"] == "skip":
# 本闸这次没跑(--quick 跳过/条件闸件未触面)——没有结果可比,不判闸况变化。
# 本闸这次没跑(--quick 跳过/影响面选闸 SKIP)——没有结果可比,不判闸况变化。
# SKIP 不得落进「基线绿 → 现红」分支——否则任何 --quick 回执都自带假红。
not_run.append(n)
continue
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# worktree 素材同步:主工作区 → 本 worktree 增量拷贝 gitignore 面(game/assets/art/ 终稿与 .import 边车)。
# 素材不入 git,worktree 冷检出与主区新收素材之间必然缺件——呈现/样张断言红的环境成因。
# 只在 worktree 生效(主工作区直跑=无操作);有新拷件或缺 .godot 时跑一次编辑器导入刷新
#(headless --script 模式不触发 import,素材在盘而 .ctex 缺位照样加载失败)。
# 用法:tools/sync_worktree_assets.sh(闸机 code 域起手自动调用;可单独手跑)
# 纪律:拷贝一律相对路径定位(在源根 cd 后按相对路径落盘)——绝对路径 --parents 会在
# 目标仓顶层误产盘符目录树(杂散目录即此误产,熵增闸判红)。
set -u
REPO="$(cd "$(dirname "$0")/.." && pwd)"
GITDIR="$(git -C "$REPO" rev-parse --git-dir 2>/dev/null)"
COMMON="$(git -C "$REPO" rev-parse --git-common-dir 2>/dev/null)"
if [ -z "$GITDIR" ] || [ -z "$COMMON" ] || [ "$GITDIR" = "$COMMON" ]; then
echo "[assets] 主工作区:无同步面"
exit 0
fi
MAIN="$(git -C "$REPO" worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p' | head -1)"
if [ -z "$MAIN" ]; then
echo "[assets] 取不到主工作区路径(git worktree list 为空),跳过同步"
exit 0
fi
MAIN="$(cd "$MAIN" && pwd)"
[ "$MAIN" = "$REPO" ] && exit 0
_copied=0
if [ -d "$MAIN/game/assets/art" ]; then
_new="$(comm -13 <(cd "$REPO/game/assets/art" 2>/dev/null && find . -type f | sort) \
<(cd "$MAIN/game/assets/art" && find . -type f | sort) | wc -l)"
if [ "$_new" -gt 0 ]; then
mkdir -p "$REPO/game/assets/art"
cp -ru "$MAIN/game/assets/art/." "$REPO/game/assets/art/"
_copied=$((_copied + _new))
fi
fi
_imports="$(comm -13 <(cd "$REPO/game" 2>/dev/null && find . -name '*.import' -type f | sort) \
<(cd "$MAIN/game" && find . -name '*.import' -type f | sort) | wc -l)"
if [ "$_imports" -gt 0 ]; then
(cd "$MAIN/game" && find . -name '*.import' -type f -print0) | while IFS= read -r -d '' f; do
if [ ! -f "$REPO/game/$f" ]; then
mkdir -p "$REPO/game/$(dirname "$f")"
cp "$MAIN/game/$f" "$REPO/game/$f"
fi
done
_copied=$((_copied + _imports))
fi
if [ "$_copied" -gt 0 ] || [ ! -d "$REPO/game/.godot" ]; then
echo "[assets] worktree 素材同步:补拷 $_copied 件(含 .import 边车),刷新导入缓存"
"${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}" \
--headless --editor --quit --path "$REPO/game" >/dev/null 2>&1
echo "[assets] 导入刷新完成"
else
echo "[assets] worktree 素材同步:无缺件"
fi
exit 0
+14 -1
View File
@@ -293,7 +293,7 @@
## ㉘ 未开格类型徽章批——类型微光实机目检(迭代 30 卡①顺带登记;能力验证半=双帧 glow 作用集取证 2/4/4·outside=0+golden off 帧 0.10–0.14%+探针 case⑰+单测五段 51 检查)
> 类型微光实装(默认开、组合根布尔可切)后,玩家可见面=棋盘格内五组类型微光。机检只能证明「作用集正确/开帧一致/关帧逐位一致」,真机上「五组分不分得清、看着脏不脏」仍需人眼。**群伤组明度档为 provisional 值**(art-bible v3.15 登记,明细见其 git 提交史),观感复核归战斗一屏样张批(迭代 30 卡②)——本节第 5 项读数作该复核的输入。登记不阻塞(2026-10-12 真人检测立规)。
> 类型微光实装(默认开、组合根布尔可切)后,玩家可见面=棋盘格内五组类型微光。机检只能证明「作用集正确/开帧一致/关帧逐位一致」,真机上「五组分不分得清、看着脏不脏」仍需人眼。**群伤组明度档为 provisional 值**(art-bible v3.15 登记,明细见其 git 提交史),观感复核归战斗一屏样张批(该批 2026-10-30 已合回——迭代 43,方向稿目检登记 ㊶、复核随其一并执行)——本节第 5 项读数作该复核的输入。登记不阻塞(2026-10-12 真人检测立规)。
| # | 目检项 | 判据 |
|---|---|---|
@@ -479,3 +479,16 @@
| 4 | 营地三柜台与同调交互回归 | 树淬炼/锻造工坊/秘宝匣三柜台可购可交互,同调充能与改前一致 |
- **红灯信号**:任一屏/浮层布局错位、交互无响应或读数与改前不一致即报缺陷。
- **结论回流**:呈现缺陷归待办清单登记新条目。
## ㊶ 迭代 43——战斗一屏样张批方向稿目检(2026-10-30 随批后收尾登记)
> 单卡合回(2026-10-30,git 提交 3b66d1a6):战斗一屏样张批——五项实装定值(血条危险红/条底收常量/顺位徽标提档/护甲三枚 24×24 PNG/名字行素底牌不压地台+地台件九宫格接装)+样张脚本扩展(两版画布出口、整屏四项与战斗表现五项机检 30/30 全绿、单元套件 18 checks 全绿)。**方向稿目检先行**(art-bible §1.4 闸形)——机检已全绿,气质终审通过才算样张整体过闸;通过前终稿资产与动画设计不启动。登记不阻塞(2026-10-12 真人检测立规);读数与论证详见实现报告(git 提交 3b66d1a6 一线可考)。
| # | 目检项 | 判据 | 材料 |
|---|---|---|---|
| 1 | 方向稿气质与构图 | 气质=明亮地窟看台、立绘最抢眼、无压抑;构图/分区/明度三档带/首焦落点与设计意图一致 | `artwork/mockups/sample_battle_screen_1280x800.png`、`sample_battle_screen_1920x1080.png`(归档位 `artwork/archive/战斗一屏样张批-2026-10-29/`) |
| 2 | 改前/改后五项定值对照 | 血条色/素底牌/徽标/护甲图标/地台件五项前后差异可辨 | `sample_battle_screen_before_after.png` |
| 3 | 类型微光 provisional 明度档观感复核(原归本批承接,见 ㉘ 节) | 五组微光在样张渲染下分得清、看着不脏;复核结论回流 art-bible §4.2 微光色带行 | 同 1(样张含五组微光渲染) |
- **红灯信号**:气质否决(压抑/抢焦/看台感不成立)、任一定值前后不可辨或微光混读即报缺陷回炉。
- **结论回流**:呈现缺陷归待办清单登记新条目;目检通过=样张整体过闸,art-bible §1.4 与战斗表现.md 的过闸状态随之换代。
+66 -85
View File
@@ -2,6 +2,39 @@
本清单是流水线的唯一登记处:要做的事先在此登记一条,随流水线段推进改状态,做完整条删除;各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。
#### 迭代 42 批末移交小件——工具与注释四条(2026-10-30 登记,来源=批次移交)
**实况**:证据=迭代 42 实现报告 §5-1/§5-5/§5-6 与验收审计报告表述性瑕疵 3/4(已按吸收即死退役,git 历史可考)。逐条:
| # | 事项 | 要点 |
|---|---|---|
| 1 | run_all.gd --only 逗号形态解析缺陷 | 头注文档形态 `--only=<词[,词…]>` 与闸脚本传参形态(tools/run_all_gates.sh 关联档选跑逗号拼接单旗标)自 6caf56b5 起与解析不符——整串当一个词对套件路径做子串匹配,多词必零命中 quit(2);修法二选一:run_all.gd 补逗号拆分,或闸脚本改逐词重复 `--only=` 传参(迭代 42 施工曾以逐词重复形态绕过) |
| 2 | 坑速查补条 | .zcode/skills/iter/SKILL.md「坑速查」节补条:worktree 冷检出 --editor --quit 首跑 --path 误指仓库根(未建 .godot 类缓存致首跑解析红),改指 game 切片目录重刷后正常 |
| 3 | level_runner.gd:12 node_entered 信号注释陈旧 | 注释 `shop={gp}` 与 pool 态实际载荷 `{gp, shelf:true}` 不符(迭代 42 改动前即如此,本批未触载荷) |
| 4 | static_scan.sh S8 ok 消息陈旧字面量 | :337「扫描面 B1 文件集 9 个 .gd」——S8_FILES 实含 10 文件(board_installer.gd 随迭代 42 入集),扫面无缩小,仅消息文本过期 |
**改动面**:游戏代码+工具代码
**状态**:待执行
#### 迭代 43 批后待裁三点——B 区高度对齐/名字行字号档位/换色常量退役(2026-10-30 登记,来源=批次移交)
**实况**:证据=迭代 43 反哺点清单 1/8/10(已按吸收即死退役,git 历史可考)。①**B 区高度 270 vs 252**——battle.md 区表文 270、代码 252,18px 差异;样张按代码 252 出(样张反映实机)。②**站桩名字行 12px 档位归属**——standee_body.gd 名字行取 `UiKit.FS_CAPTION`(12px),art-bible §7.4 字号档位表下属「清单外 11–13px」。③**art-bible §4.1 换色合并残余三处**——`STAGE_BG` 换值(#3E342A→#3E362C)未执行;`STAND_STONE_RIM`(#C4AC84)仍被 board_cell_unit 口沿高光在役引用;`STAND_STONE_EDGE`(#5A4C3A)由地台缺位回退路径在役引用。
**缺口**:三点各差一次用户裁决——①B 区高度对齐方向(改实装=需求改动走写卡/改文档=battle.md 区表落 252 归文档批落文);②名字行 12px 提档 `FS_HINT`(14,与顺位徽标同款)或增补装饰 caption 在册面;③三常量彻底退役与否(涉实装件改指与 art-bible §4.1 登记句换代)。裁后各归其段。
**改动面**:文档+游戏代码
**状态**:待文档设计
#### 迭代 43 批后移交小件——能力清单补行+四色墨断言(2026-10-30 登记,来源=批次移交)
**实况**:证据=迭代 43 反哺点清单 9/13(已按吸收即死退役,git 历史可考)。逐条:①**能力清单补行**——game/tests/README.md 能力清单无战斗一屏样张批 V5–V9 代表用例行(卡片成文早于清单 2026-10-29 改版定稿),归写卡段随下一写卡会话按 test-conventions §8.3 补行(能力=站桩呈现定值与素材绑定;代表用例=`game/tests/unit/presentation/standee_present_values_test.gd:_init`,真实入口=`UiKit.load_tex()`/`StandeeBody.name_line_probe()`),并同步覆盖域表「战斗表现.md」行。②**意图徽标四色墨补机检断言**——样张脚本 S1 文字对比为绘图用色值对表,意图徽标四色墨(对 PANEL_DATA 垫牌)未入 CHECKS;独立审计复算 4.97–5.62 全 ≥4.5 事实成立仅缺机检断言,`tools/render_sample_sheet_v3.py` CHECKS 补一条对表断言(values 全在册,零新增色)。
**改动面**:游戏代码+工具代码
**状态**:待执行
#### 架构审查优化讨论——全库架构审计读数的逐条裁决与整改分批(2026-10-29 登记,来源=审计发现)
**实况**:2026-10-29 全库架构静态审计完成,读数见 reports/审计-全库架构审查-2026-10-29.md(七节:总评/健康面/P0/P1/P2/待裁点/取证口径;零测试执行零引擎运行,全部结论带 文件:行号)。健康面已逐条核干净:Core 零 UI 八项判据全零命中、core 内 RNG 单点 100% 成立、字符串式信号连接全库 0 处、core 层零超长函数、19 表 ↔ 19 schema 一一对应、无循环 preload。问题集中在三条「写了没闸守」的纪律被实际破坏:①数值单一事实源被 18 处硬编码击穿,最危险两处是 `presentation/main.gd:787` 与 `:790` 把 `rap_refresh_cost=8` 与 `refresh_cap_per_round=2` 写死进提示文案(紧邻 783 行是正确读表的),表改文案不改、玩家直接受错误数字;另有 4 组代码自标「机制结构常量/产出约束」的字面量(`node_map_generator.gd:12` `MAX_WIDTH`、`action_economy.gd:14` `CHARGE_LAYER_CAP`、`board_data.gd:9` `SPAWN_AREA_CELLS`、`node_map_generator.gd:83-94` 域字面量)是否入表待裁。②`game/sim/` 与 `game/core/` 四组平行实现——扫雷推理引擎双份(`board_generator.gd` `measure_inferability` 族 vs `auto_player.gd` `_deduce` 族,输入集口径不同:前者含 safe_set oracle、后者 no-peek)、伤害掷点期望重算(`damage_probe.gd:196-201`)、档位排序与驱动循环双份(`level_autoplay.gd` vs `treasure_item_table.gd:44` 与 `battle_driver.gd:17`)、FakeDB 伪造表喂生成器——直接威胁「模拟读数 = 游戏真实数值」这一数值验证前提。③RNG 单点纪律在 core 外失守(sim 4 处/probe 1 处/tests 3 处自建 `RandomNumberGenerator`,均显式定种、确定性尚存),而 `static_scan.sh` 的 S1/S2 扫描面只含 `DIR_CORE` + `DIR_PRES`。守卫面另有三缺口:S8 是 9 文件显式清单(新增结算文件忘登记=静默不扫,缺位自检只防删改不防增)、S9 类型闸与 S13 质量闸扫描面止步于 core+data+presentation(sim/probe/tests 三层除 S11/S12 外零闸)、S13 三处豁免常态化到最大户文件(`battle_orchestrator`/`main.gd`/`tune_sim_controller`);另有四个 core 文件零测试点名(`attune_loadout`/`battle_fx_host`/`meta_state_store`/`slot_store`)。P2 级:`core/settlement/` 五段类直读写编排器私有成员 20+ 处(`oc._damage.*`/`oc._enemy_system.*`,GDScript 无编译期检查、改名静默断裂);架构文档三处漂移(`architecture.md:110` 登记的 `core/hero_ledger.gd` 盘上不存在且 `hero_ledger_test.gd:171` 显式断言它必须不存在、`:135` 行数过期、EC8 热重载门后 `ConfigDb.reload()` 零调用方且走 `load()` 拿不到磁盘新值);class_name 级双向依赖(`core/config_db` → `data/schemas/board_gen_table` → `core/board_data.SPAWN_AREA_CELLS`)与 `NODE_TYPES` 词表双份;`MetaProgression` 是唯一用 `get_node("/root/...")` 取依赖、唯一跨 Autoload 直调的单例。
@@ -59,22 +92,14 @@
**状态**:待执行
#### 闸基线 02/09 两键换代——存量红指纹已随迭代 41 测试债批归绿(2026-10-29 登记,来源=批次移交)
#### 闸基线 02/05/09 三键换代——存量红指纹已消除或现行不复现(2026-10-29 登记、2026-10-30 并入 05,来源=批次移交)
**实况**:证据=迭代 41 测试债批代码域闸实测(worktree 分支 `testdebt-20261029`,回执 commit afd1a41f、域=code、scope=full、零新增红),02 单元与集成与 09 静态扫描两闸判「基线红 → 现绿(改善)」;基线 `game/tests/.gates/baseline.json` 里这两键仍记着已消除的存量红指纹(02 fp f9865f1a5a44=9 套件失败;09 fp 1a9ad773489f=S9 24 处+S10 4 处+S11 1 处),留着不动的后果=日后同类红回归会被判「存量红·对基线零新增」静默放行。待做=两键按归绿实测重生成(写 exit=0/绿指纹),改的是 `game/` 下非 .md 文件=代码面,须随下一个代码批在 worktree 内一并落、禁主区直改;人工确认凭据=上述回执与 reports/迭代41测试债批-实现报告-2026-10-29.md 第三节。本条目无设计段(机械重锚),不走写卡与编排。
**实况**:证据=迭代 41 测试债批代码域闸实测(worktree 分支 `testdebt-20261029`,回执 commit afd1a41f、域=code、scope=full、零新增红),02 单元与集成与 09 静态扫描两闸判「基线红 → 现绿(改善)」;05 渲染闸在 2026-10-30 脚本错误扫描批主区真跑 `bash tools/render_gate.sh` exit=0 全绿(shot/fx/board 三段扫描零命中、残留 Godot=0、golden changed 0.00%),其红键在现行主区不复现。基线 `game/tests/.gates/baseline.json` 里这三键仍记着历史红指纹(02 fp f9865f1a5a44=9 套件失败;05 fp de89589e49ed;09 fp 1a9ad773489f=S9 24 处+S10 4 处+S11 1 处),留着不动的后果=日后同类红回归会被判「存量红·对基线零新增」静默放行。待做=三键按归绿实测重生成(写 exit=0/绿指纹),改的是 `game/` 下非 .md 文件=代码面,须随下一个代码批在 worktree 内一并落、禁主区直改;05 须在素材齐盘预检后跑出真值(冷检出空盘态下的渲染红不作数)。人工确认凭据=上述回执、reports/迭代41测试债批-实现报告-2026-10-29.md 第三节与本批提交说明。本条目无设计段(机械重锚),不走写卡与编排。
**改动面**:游戏代码
**状态**:待执行
#### gdstyle 尾波清偿批——基线余量 11 键定向整改(2026-10-29 登记,来源=审计发现)
**实况**:证据=`tools/gdstyle_baseline.txt`(2026-10-29 人工重锚后余量 16 键)+reports/审计-gdstyle存量债清点与清偿方案-2026-10-26.md(§三波次方案与 §六文档设计段裁定记录,裁定全部继续有效)。迭代 39/40/41 三批清偿后余 16 键:本批承接 11 键——board_view 6(_paint_cell 长度 139/分支 24/局部 22、循环内 new、类变量 32、公开方法 27)/level_runner 3(公开方法 50、类变量 30、_enter_node 返回 7)/battle_orchestrator 文件长 1035/adventure_runner 类变量 28;另 5 键走豁免通道另行讨论不在本批(main.gd 类变量 49、tune_sim_controller 循环内 await、board_cell_unit 类变量 30、standee_body 类变量 19、battle_orchestrator 类变量 38——其中三条已有架构豁免登记)。需求卡片 v2.0 定稿(零待裁点):reports/需求卡片-gdstyle尾波清偿批-2026-10-29.md 设计段裁决 2026-10-29 用户裁闭合(D1 装板域拆新件 BoardInstaller 两修正·architecture v1.24 轴丙已落文/D2 悬停域不拆/D3 商店域候选 A/D4 断点域不拆/D5 五降值键豁免终局/D6 商店域并排编队——执行合同=并排卡「level_runner 商店委托层拆除」v1.1,本卡不重复执行),零待裁点。预期两档:归绿 6 键(_paint_cell×3、循环内 new、_enter_node、文件长);豁免终局 5 键(board_view 32/27、level_runner 50→33/30、adventure 28——批末按实测值落 architecture 豁免登记,键 7 实测值视并排卡时序)。本卡估时 3–4.25 工作日(并排卡 ≈1.5 工作日另计)。MC/数值零触碰(结构整改不改数值与 RNG 消费序);battle_orchestrator 触 PARITY_TOUCH_FACES——实跑对拍须经用户批准、不得借机重锚。S13 键账对账在批末,基线重锚由用户发令人工 --refresh(预期降幅逾 10% 须 --force)。2026-10-29 用户再裁(零豁免延伸):D5 五降值键改留债——本卡批末「落 architecture 豁免登记」步骤**不执行**(键留基线冻结禁恶化、不落豁免登记);D2 悬停域与 D4 断点域改判拆件,由新条目「gdstyle 尾波补件批」承接(需求卡片已写),本卡对应任务不施工。
**改动面**:游戏代码
**状态**:文档设计已闭合+已写卡+已编迭代 42
#### gdstyle 尾波补件批——悬停域与断点域拆件(2026-10-29 登记,来源=会话提及)
**实况**:尾波卡 v2.0 裁去的 D2/D4 两拆件项经用户再裁恢复(2026-10-29,零豁免延伸+D5 留债)。设计闭合凭据=architecture.md v1.25(呈现层表 board_hover 行/核心层表 breakpoint_store 行/「冒险域拆分」节随动句)。需求卡片:reports/需求卡片-gdstyle尾波补件批-2026-10-29.md(v1.0 定稿零待裁点)。范围:board_view 拆 board_hover(11 函数族+2 成员,`_r0` 留宿主)+adventure_runner 拆 breakpoint_store(6 成员+`BREAKPOINT_FIELDS` 27 键冻结函数族,`resume_adventure` 留宿主薄壳)。键账预期:board_view 类变量 32→约30、公开方法 27 持平、adventure_runner 类变量 28→约22——三键**留债**(键留基线冻结禁恶化、禁落豁免登记)。编队:待迭代 42 合回后编入;与呈现层状态件批同代时其先行(u.st 写入面改型后 hover 拆件在新面上施工)。估时 2.5–3.5 工作日。
@@ -85,15 +110,15 @@
#### 零豁免五键清偿批——S13 余量五键真修四卡(2026-10-29 登记,来源=会话提及)
**实况**:用户 2026-10-29 裁「不允许任何豁免」——S13 三条豁免注销(architecture.md v1.25 落文),五键真实修复至阈值内:main.gd 类变量 49→15(组合根注册面:ui_registry 一门面+三域容器,约 867 处探针/系统测试引用改址 `main.ui.<域>.<句柄>`,7 别名/转发成员消灭)/battle_orchestrator 类变量 38→15(RoundCycle+BattleFacts 两件,叠加迭代 42 轴丙 BoardInstaller;encounter 归 BattleFacts、attune 改判状态两成员随件)/board_cell_unit 类变量 30→10+standee_body 类变量 19→10(各拆两独立文件,gdstyle inner-class 展平计数口径)/tune_sim 循环内 await 消除(process_frame 信号步进状态机;含**申报行为改动**一处:运行态收尾由每局置假还原为整批收尾单次置假——消除与 wd8/ui_tune_probe 断言面的静态矛盾,T0 实证定谳)。设计闭合凭据=architecture.md v1.25+五份设计文档(reports/ 下五件 设计-组合根注册面-2026-10-29.md/设计-战斗编排类变量归约-2026-10-29.md/设计-单格绘制状态件-2026-10-29.md/设计-站桩状态件-2026-10-29.md/设计-测速循环去await-2026-10-29.md)。四卡已成文(均零待裁点开工门 U0):
**实况**:用户 2026-10-29 裁「不允许任何豁免」——S13 三条豁免注销(architecture.md v1.25 落文),五键真实修复至阈值内:main.gd 类变量 49→15(组合根注册面:ui_registry 一门面+三域容器,约 867 处探针/系统测试引用改址 `main.ui.<域>.<句柄>`,7 别名/转发成员消灭)/battle_orchestrator 类变量 38→15(RoundCycle+BattleFacts 两件,叠加迭代 42 轴丙 BoardInstaller;encounter 归 BattleFacts、attune 改判状态两成员随件)/board_cell_unit 类变量 30→10+standee_body 类变量 19→10(各拆两独立文件,gdstyle inner-class 展平计数口径)/tune_sim 循环内 await 消除(process_frame 信号步进状态机;**零行为改动**——v1.0 申报的收尾置假还原已被迭代 41 测试债批 215da315 先行完成并申报,2026-10-30 实现段按卡片预设停机点停卡、三重证据定谳前提已消化,按停卡呈报建议案重裁剥离行为改动申报:iter44 worktree 分支 reports/iter44-实现报告-2026-10-30.md §5)。设计闭合凭据=architecture.md v1.25+五份设计文档(reports/ 下五件 设计-组合根注册面-2026-10-29.md/设计-战斗编排类变量归约-2026-10-29.md/设计-单格绘制状态件-2026-10-29.md/设计-站桩状态件-2026-10-29.md/设计-测速循环去await-2026-10-29.md)。四卡已成文(均零待裁点开工门 U0;测速循环批卡 2026-10-30 停卡重裁 v1.1——前提消化后剥离行为改动申报、T0 改现状复核、夹具申报差异两处减一处,仍零待裁点):
关联文档清单:
- 组合根注册面批 reports/需求卡片-组合根注册面批-2026-10-29.md
- 呈现层状态件批 reports/需求卡片-呈现层状态件批-2026-10-29.md(单格+站桩两设计并一卡)
- 测速循环批 reports/需求卡片-测速循环批-2026-10-29.md
- 测速循环批 reports/需求卡片-测速循环批-2026-10-29.md(v1.1,2026-10-30 停卡重裁)
- 战斗编排归约批 reports/需求卡片-战斗编排归约批-2026-10-29.md(接收方=在册条目「战斗域结构深拆批」宿主瘦身子项,不在本条目记账)
编队待迭代 42 合回(battle_orchestrator/board_view 触面碰撞约束),具体迭代归属归编排段;编入前置=各卡过 grilling 复核门。gdstyle inner-class 计数截断缺陷已在册(architecture v1.25)——基线登记值欠估,各批按真实口径施工;各批全部合回后一次人工 --refresh --force 收账。2026-10-29 编排(用户裁编法):卡 A 组合根注册面批+卡 B 战斗编排归约批+卡 D 测速循环批编入**迭代 44**(≈7.875 个工作日,中值合计);卡 C 呈现层状态件批编入**迭代 45**(与补件批同批,卡 C 先于其 board_hover 半边施工)。grilling 复核门已过(reports/复核-grilling-零豁免五卡-2026-10-29.md,五卡有条件成立、修正全落,提交 9855bf0a)。
编队待迭代 42 合回(battle_orchestrator/board_view 触面碰撞约束),具体迭代归属归编排段;编入前置=各卡过 grilling 复核门。gdstyle inner-class 计数截断缺陷已在册(architecture v1.25)——基线登记值欠估,各批按真实口径施工;各批全部合回后一次人工 --refresh --force 收账(一并清迭代 42 归绿 6 键离账——批末实测已归绿而基线仍记 16 键:board_view 四键+level_runner `_enter_node`+battle_orchestrator 文件长;各留债键按重锚时实测重登记,冻结禁恶化口径不变)。2026-10-29 编排(用户裁编法):卡 A 组合根注册面批+卡 B 战斗编排归约批+卡 D 测速循环批编入**迭代 44**(≈7.875 个工作日,中值合计);卡 C 呈现层状态件批编入**迭代 45**(与补件批同批,卡 C 先于其 board_hover 半边施工)。grilling 复核门已过(reports/复核-grilling-零豁免五卡-2026-10-29.md,五卡有条件成立、修正全落,提交 9855bf0a)。
**改动面**:游戏代码
@@ -101,7 +126,7 @@
#### 数值标定窗口总挂账——原九条数值类条目合并(2026-10-28 登记,来源=会话提及)
**实况**:2026-10-28 用户令把九条数值类挂账合并为一条:削弱档位的最终数值/迭代 10 奖励重构联动欠账/调参批数值点评登记/三方向各自的解锁清单与定价/GP 经济换算失衡/天赋树实装批登记欠账余两件/商店独立刷新机制两参数/基线通关率越带加难杠杆扫描/成长系数节奏——原九条同步删除,内容全量迁入汇总文档,git 历史可考(本提交)。共同背景:M4 数值批卡已随 2026-10-16 删卡令退役;2026-10-18 用户裁=各条留待办清单、未设计未启动、不另行归属,未来数值标定窗口重开时重走设计段再写卡——本条沿用该裁定,重启条件=用户发令开窗口。合并动机=九条共享同一重启条件与同一执行形态(窗口内一次 MC 批量统一覆盖各条对比需求:全冒险期望+奖励期望+对拍锚重锚),分散登记徒增维护面。逐条挂账明细、裁定史与相关文档见汇总文档。
**实况**:2026-10-28 用户令把九条数值类挂账合并为一条:削弱档位的最终数值/迭代 10 奖励重构联动欠账/调参批数值点评登记/三方向各自的解锁清单与定价/GP 经济换算失衡/天赋树实装批登记欠账余两件/商店独立刷新机制两参数/基线通关率越带加难杠杆扫描/成长系数节奏——原九条同步删除,内容全量迁入汇总文档,git 历史可考(本提交)。共同背景:M4 数值批卡已随 2026-10-16 删卡令退役;2026-10-18 用户裁=各条留待办清单、未设计未启动、不另行归属,未来数值标定窗口重开时重走设计段再写卡——本条沿用该裁定,重启条件=用户发令开窗口。合并动机=九条共享同一重启条件与同一执行形态(窗口内一次 MC 批量统一覆盖各条对比需求:全冒险期望+奖励期望),分散登记徒增维护面。逐条挂账明细、裁定史与相关文档见汇总文档。
关联文档清单:
- 汇总文档 reports/汇总-数值标定窗口挂账-2026-10-28.md(2026-10-28,九条全量内容+统一执行形态建议序:设计段逐条裁决→按域拆卡→实现批三处同步→测量批一次跑批)
@@ -127,14 +152,6 @@
**状态**:待文档设计+已写卡+已编迭代 44
#### level_runner 商店委托层拆除——迭代 37 冻结契约期过渡件(2026-10-26 登记,来源=批次移交)
**实况**:证据=迭代 37 卡④ ShopSession 拆分时立「冻结公开面」契约,level_runner 保留 buy_product/shop_refresh 等同签名委托方法(架构模块表登记为「外部调用面由宿主同签名委托承载」)。2026-10-26 AdventureRunner 真拆分设计会话裁定新原则:纯转传委托=Middle Man 反模式,新件一律调用方直连不留壳(godot-master 技能库审查同判);本批不动该委托层(真拆分条目范围仅撤 8 条白盒转发属性),其拆除与调用文件改址由本条承接。需求卡片:reports/需求卡片-level_runner商店委托层拆除-2026-10-29.md(v1.1,2026-10-29,复核门通过零待裁点)。
**改动面**:游戏代码
**状态**:已写卡+已编迭代 42
#### 超预算图策略升级路径——等比缩放与滚动视口两案待裁(2026-10-24 登记,来源=批次移交)
**实况**:迭代 34 卡④(信息迷雾可见性批,已合回 master,提交 16a7ec80)按「参数限图」交付,图包络约 10 层×8 槽即当前冻结画布的屏预算上限。若后续定值批需要更大的完整可见图,两条升级路径都要动冻结件,均未设计:①等比缩放——动「节点图标 20px 烘焙 1:1」与「通道折线冻结 2/3/4px」两条冻结值;②滚动视口——新增视口交互范式。迭代 35 生成图实测深度域 L3 达 0–13,已超出该上限(level-map 已登记随动缺口,本批不裁方案)。
@@ -170,23 +187,6 @@
**状态**:待美术设计(备选)
#### 战斗一屏样张 P0 未开工——界面成品是否过审的那把尺子还没建(2026-10-13 登记,来源=审计发现)
**实况**:[美术圣经](design/art/art-bible.md) §7.1 明文写着"程序化占位=临时态,任何屏的终稿闸=§1.4 整屏样张过闸;占位态禁止跨里程碑"。而 [美术素材状态清单](design/art/art-status-checklist.md) 把「战斗一屏样张」这件最高优先级的素材标的是**未开始**——也就是说,**判定界面美术算不算过关的那把尺子到现在还没造出来**。2026-10-16 曾编入迭代 30 卡②(UI 呈现三卡批序位②),2026-10-17 用户裁定不满足「迭代=代码实现批」的开发条件、剔除回待设计(理由=样张交付物本身就是设计规格,正式字体与素材均未定稿;同日样张批开工已被用户终止)。v1.4 旧卡留 reports/需求卡片-战斗一屏样张批-2026-10-16.md 作素材,非已写卡状态,重启不得直接开工。Alpha 出口裁决:列入 Alpha 硬前置——站桩帧动画的定版前置与主角+三 Boss 重产都依赖它。
**2026-10-28 美术设计段闭合**:差的那轮「设计讨论→需求澄清→美术定稿」已完成并落文——样张即设计目标图,五张规格表(尺寸档/色板/明度档/字号档/素材清单)全定、样张构图与内容清单全定、验收判据清单全定、16 条待裁点全数清零(最后一条 B-1 色板第二道门口径经 2026-10-28 用户裁取乙案宽松口径);样张的产出手段与素材输入级别已定,样张开工门复判 PASS。原缺口三件的前两件(目标规格清单、屏内素材规格)由此闭合,第三件(重启须重新立卡)转为本条的下一动作。
关联文档清单:
- 美术线闭合凭据(art-bible 条款版本):[美术圣经](design/art/art-bible.md) v3.22(2026-10-28)——§1.4 样张产出手段与素材输入级别登记句、§2 世界底层两子带、§4.1 第二道门口径改写与色板转正/换色合并登记、§4.2 血条填充显式句与蓝族违例值退役、§6.1 舞台背景路径订正与近景地台独立件、§6.3 三关色板终值取值程序、§7.2 文字禁直接压地台、§7.4 顺位徽标不增列与字重重开钩子注销、§7.6(三) 氛围两派维持不统一、§8.2 护甲图标与地台件规格与棋盘小人 32px 源档终值、§8.4 方向集口径追认
- 美术线设计轮定值与依据:2026-10-28 设计轮(过程稿已按「吸收即死」删除、不入库,结论已吸收进上一条,推导沿革见 commit 50702ea5 与 10d7c82b)
- 配套资产登记:[美术资源依赖清单](design/art/art-assets-checklist.md)(2026-10-28,§3.7 护甲类型图标三枚与 §4 舞台背景/地台件两行)· [素材路径分工约定](design/art/asset-path-map.md)(2026-10-28,`scene/` 子目录现役列)
- 相关文档:美术线(战斗一屏样张的验收标准归 [战斗表现](design/gdd/systems/战斗循环/战斗表现.md) 与 [美术圣经](design/art/art-bible.md) §1.4 共裁)
- 需求卡片:reports/需求卡片-战斗一屏样张批-2026-10-28.md(2026-10-28 重新立卡,十二节齐备,旧卡 v1.4 已作废;待编入迭代,编入后由编排段记「已编迭代 N」)
**改动面**:美术素材
**状态**:已写卡+已编迭代 43
#### UI 氛围层与九宫格切片观感修——光柱硬边与面板切片错位(2026-10-13 登记,来源=审计发现)
**实况**:原两处,一处已修一处存续。①光柱硬边**已修**(2026-10-14 迭代 26 T4:`ui_kit.gd` 第 263 行起的光柱贴片改带内 alpha 0→0.05→0 三角过渡+纵向衰减上亮下暗,grilling Q7 裁连续 alpha 方案;LINEAR 白名单已随反哺入册 art-bible v3.14 §8.2)。②九宫格切片错位**存续**——结算屏与图例屏的面板边缘可见错位小方块,成因是九宫格切片参数与素材尺寸不匹配(仪式石框 258 字节小图按 12 像素切片、石板亮面板 314 字节按 16 像素切片,铺到四百多像素宽面板中段拉出错位),须先核素材尺寸能否撑住新切片值(border_stone/panel_stone 均 48×48),可能要重出素材,归「UI 贴图重构批」后续贴图批次。
@@ -316,7 +316,7 @@
**实况**:来源=playnote 用户试玩记录「UI 优化,第二第三关的棋盘看着太累了;不知道是因为亮度还是对比度的原因,看着非常费劲,非常不舒服」。2026-10-13 用户裁定:**这是需要做的**,且本就属版本标准的覆盖范围——[08-Alpha版本标准](design/gdd/08-Alpha版本标准.md)「五 · 表现」已新增「棋盘观感(第二 / 三关亮度与对比度)」行,标待做。**开工前须先定性(沿用既有裁决)**:是**素材轴**问题(棋盘瓦片与格面图标本身的明度与色相),还是 **UI 轴**问题(舞台底色与叠加层的对比关系)——两轴修法完全不同,定性错了等于白改一轮;该定性要求(原备忘 (e) 项)是本条的开工前置。2026-10-16 曾编入迭代 30 卡③(UI 呈现三卡批序位③),2026-10-17 用户裁定剔除回待设计(自身挂「素材轴/UI 轴」定性人闸+甲/乙〔recolor/远程生图重产〕素材裁点;若裁乙,其瓦片规格依赖战斗一屏样张批的目标规格清单产出)。v1.4 旧卡留 reports/需求卡片-二三关棋盘观感批-2026-10-16.md 作素材,非已写卡状态。与已完成注销的「呈现层残余 <4.5:1 文字位」分工:本条管**棋盘区域整体的观感**(亮度/对比度/费劲感,体感问题),彼条管**具体文字位的对比度读数**(机检可判硬指标),两条不同面不互相替代。Alpha 出口裁决:Alpha 必须,先做素材轴/UI 轴对照定性;出口=用户实机目检不再判「看着费劲」。
**缺口**:还差「素材轴 / UI 轴」定性设计讨论与甲/乙素材裁点裁决;素材轴若走重产,还依赖战斗一屏样张批的目标规格清单产出。旧卡 v1.4(reports/需求卡片-二三关棋盘观感批-2026-10-16.md)在册但非已写卡状态,其前置未完成三件:①「素材轴 / UI 轴」定性未做,这是既有条款规定的开工前置;②甲(recolor 现瓦片)/乙(远程生图重产)素材裁点未裁;③若裁乙,瓦片规格压在样张批的目标规格清单上,而那份清单的前置正是上一条未完成的样张设计轮——本条第三项压在卡②身上。
**缺口**:还差「素材轴 / UI 轴」定性设计讨论与甲/乙素材裁点裁决;素材轴若走重产,还依赖战斗一屏样张批的目标规格清单产出。旧卡 v1.4(reports/需求卡片-二三关棋盘观感批-2026-10-16.md)在册但非已写卡状态,其前置未完成三件:①「素材轴 / UI 轴」定性未做,这是既有条款规定的开工前置;②甲(recolor 现瓦片)/乙(远程生图重产)素材裁点未裁;③若裁乙,瓦片规格压在样张批的目标规格清单上——该清单已随战斗一屏样张批落定(art-bible v3.22 五张规格表,2026-10-30 合回)。
- 相关文档:[HUD框架](design/gdd/systems/表现与支撑/HUD框架.md)(UI 轴:舞台底与叠加层)+ [美术圣经](design/art/art-bible.md)(素材轴:瓦片与格面图标的明度色相)——定性前两方共裁
**改动面**:美术素材+游戏代码
@@ -327,9 +327,11 @@
**实况**:扩板已落地(四档板型 10/12/13/14 在跑),2026-10-26 文档面已按单点维护归一:①格边长口径的唯一出处收敛到 [战斗屏规格](design/gdd/ux/battle.md)「棋盘格尺寸公式」——公式含格间距与垂直预留,可读下限 30px,1280 档算例 43/36/33/30 标注为随布局变化的非契约值;②[美术圣经](design/art/art-bible.md) §8.1 改为指路 battle.md、只锁资产侧两条比例,§8.2 甲类整数倍纪律已剔除砖面 tile(瓦片随格边长整格铺满,允许非整数倍);③[HUD框架](design/gdd/systems/表现与支撑/HUD框架.md) 只留装板可行性不等式,删去 36/45 定值档与旧算例;④demo 原型 44px 那一路随 `src/` 退役消失。剩余为代码面两处与口径不同步:`game/probe/layout_probe.gd:15` 的格子可读门槛 `MIN_CELL := 36.0` 仍取 12×12 的旧锚点值,比 battle.md 的可读下限 30px 严——现探针只进 10×10 的 L1 故仍全绿,一旦覆盖 13×13(33px)与 14×14(30px)即会误报红;`game/tests/unit/core/board_gen_reward_test.gd:53` 的格边长冒烟只跑 L1/L2,13×13 与 14×14 无覆盖。断言改动后按 `game/tests/README.md`「能力清单」核对受影响行(验证集清单已退役——test-conventions §8.3)。
- 需求卡片:[需求卡片-棋盘格边长口径收口批-2026-10-30](reports/需求卡片-棋盘格边长口径收口批-2026-10-30.md)(2026-10-30 已写卡,零待裁点——算式单点提取经 `BoardView._cell_for`/探针门槛 36→30 对齐 battle.md 可读下限/冒烟扩 13×13 与 14×14;探针不扩到 L3,最大板可读面改由实板面积推定断言承担)
**改动面**:游戏代码
**状态**:待写卡
**状态**:已写卡
#### 立绘清晰度回退评估——是否改用高清图(2026-10-13 登记,来源=playnote)
@@ -364,15 +366,6 @@
**状态**:待美术设计(备选)
#### 在途草稿整理批——3349 个失管草稿(2026-10-06 登记,来源=审计发现)
**实况**:审计 P2-1——`artwork/ui-reuse-2026-09-23/` 独占 3043 个草稿(91%),vN 命名率 0%,冗余 41 文件,残留非素材文件(.url ×8 / .ogg ×6 / .ai ×2 / .swf ×1);根因=脚本链覆盖「生成→验证」但**缺「草稿整理」段**。整理涉及**批量文件移动/删除**,按用户惯例须先 dry-run 呈报清单、经确认再分批执行。执行窗口随实现批;开工=用户点名。
- 相关文档:[art-bible](design/art/art-bible.md) §8.3 归档纪律(原则已写入)· 资产管线批(工具+文件操作)
**改动面**:美术素材+工具代码
**状态**:待写卡
#### 战斗区背景图替换——B 区舞台插画 + 临时光柱退役(2026-10-07 登记,来源=会话提及)
**实况**:B 区舞台现行=程序化平色底(`UiKit.STAGE_BG`,`battle_stage_view.gd:47`)+ **顶光柱氛围层**(`:55-63` 四柱,x 基准 110/900/1055/1210@1280,60 段叠层矩形自顶向下渐宽窄渐隐〔顶 α26→0〕,层序=bg < 光柱 < 世界)——二者均系 §6.1 方向稿的**临时占位**,用户口径「光柱只是临时方案,换正式战斗背景后效果更好」。正式件已立资产契约:`stage/stage_bg.png`(源 1280×270,B 区全高含明细带底;三关换挡走 §4.3 `wall_ambient` tint 槽,不按板出三张),属审计 P1-2 查得的「B 区舞台背景插画 0 张」缺口。**光柱在背景到位后顺带退役或吸收进背景绘制**(不再作为独立程序层)。**与立绘探顶解耦**:大体档「允许向上溢出盒顶 N px」的判定(2026-10-07 用户裁甲案)**不依赖光柱是否存在**,换背景零返工。
@@ -384,6 +377,29 @@
**状态**:待美术设计
#### 棋盘封印印记图形换代——现件仍是扫雷旗形,改法阵或锁链待设计(2026-10-30 登记,来源=会话提及)
**实况**:用户口径=棋盘封印图标现在还是按旧扫雷的做法画成一个旗子,希望换成法阵那样的封印、或锁链;形未定,先做设计讨论再出图。现状证据:
- 现件 `game/assets/art/board/seal_mark.png`(32×32 纯黑剪影)=经典扫雷旗形(旗面 + 旗杆)。2026-09-28 那次只定了色(旗朱→封印紫→封印黑 `#000000`),图形一直没动。
- 消费面两处,换形同时受影响:棋盘侧 `game/presentation/views/tile_palette.gd:19`(`TILE_SEAL_MARK` 常量)→ `board_view.gd:616` 未开格封印槽绘制,盘上缺图时回落单字「封」(`board_view.gd:654-658`);战末弹层的「金蜡封徽记」复用同一张图(金底 52×52 面板 `battle_end_overlay.gd:65-69`,叠 32px 印记 `:84`)——蜡封徽记要不要跟着换是独立一裁。
- 文档侧口径仍以旗为形:art-bible §4.1 封印黑条内含「旗杆米白保可读」这一旗形前提、§3.2 封印印记条只锁色与格内栈位;art-assets-checklist §4 封印特效行写「印记旗面走封印黑」「印记常显形态可与 §3.3 旗面像素化合并出」;ux/battle.md:128 呈现术语注记与 :270 悬停三态光标「封印(印记形)」。
- 术语层已闭合,本条只欠呈现层:「旗」概念 2026-10-18 用户裁全库退役(动作=封印、印记=封印印记、板面单字=「封」),图形停在退役前的形态。
关联文档清单:
- 相关文档 [美术圣经](design/art/art-bible.md) v3.23——§3.2 封印印记条(:167)· §4.1 封印黑条(:202)· §1.2 格内优先级栈 · §1.4 格子样张
- 相关文档 [美术资源依赖清单](design/art/art-assets-checklist.md)——§3.3 雷/封印印记/数字格面像素化(:118)· §4 封印特效行(:162)
- 相关文档 [美术素材状态清单](design/art/art-status-checklist.md)——P2「雷/封印印记/数字砖面全套」◐ · P3「封印特效」⬜
- 相关文档 [战斗屏 UX](design/gdd/ux/battle.md)(Last Updated 2026-10-29)——:128 呈现术语注记 · :270 悬停预承诺三态光标 · :261 未开格封印交互行
- 相关文档 [术语表](design/gdd/术语表.md) v1.25——「封印」条(:15)· 用语规范禁用字面条(:184)
- 相关目检项 [体验验证清单](体验验证清单.md) 第 4 条「封印印记贴图三关加载」(:317,现判据只验贴图是否加载、不验形状)
**缺口**:还差一次封印印记图形的设计讨论(形未定,法阵/锁链两案并列,禁代裁),讨论须一并裁五点:①形=法阵(圆形阵纹)还是锁链(束缚感),或第三种方案;与危险格巢穴纹、奖励徽章的剪影语言(art-bible §3.1 直线几何体)互辨是否成立。②色=现封印黑 `#000000` 是 §4.1 不变槽成员,法阵若带发光或色相即与该条口径冲突,须重登记不变槽与对比度机检判据(现判据=未开格墙顶对纯黑 ≥4.4:1)。③尺寸与遮挡=32×32 内保持可读,格内优先级栈「数字 > 小人 > 封印印记」下法阵铺满整格是否侵入数字字形区(§3.2 验收句禁止)。④连带件=战末金蜡封徽记与悬停「封印(印记形)」光标是否同步换,还是各留一件。⑤与「封印特效=施印瞬间特效 + 印记常显形态」两件的关系=合并出一件还是分两件。裁决写入 art-bible §3.2/§4.1 与两份美术清单条款后,才可出样张与终稿(终稿文件名沿用代码消费名 `seal_mark.png`,除非裁决换名)。
**改动面**:文档+美术素材
**状态**:待美术设计
### 教学引导
> **节头注记(2026-10-04 用户拍板)**:教学引导定稿批整体改排 **Beta 阶段**——当前 Alpha 版不做教学相关工作,本节五条全部随之改排(2026-09-19 的 Alpha 排期拍板被本拍板取代)。同日五问讨论已形成初裁与设计提案,提案已归档 `archive/` 目录(磁盘留存、不入 git,Beta 批立项底稿,引用以其「归档状态与初裁记录」节为准)。初裁摘要:①教学形态=丙1 混合(首次冒险编排式首遇提示〔挂 first_script 锚、不锁操作〕+其余全程情景触发);②「因果教学顺序」问题**注销**——只教扫雷操作,结算读法不设教学(用户裁:结算只是表达、展现、演出,无策略内容;展示层每行带来源标注已自带归因;教学引导.md 旧前提〔设计目的句/P0 候选归因提示/验收第 2 条〕随 Beta 批文档定稿清除);③图例面板按循环段落分组、无结算段;④不做「再教一次」——改走独立「帮助」手册面(全部内容放一个文档);⑤教学文案内容权归设置与本地化(两文档负责表对齐随 Beta 批)。**未裁三疑点**(Beta 立项时先裁):增益叠层节奏条目去留/帮助入口位置(设置面为推荐)/图例面板与帮助手册的内容分工。
@@ -453,17 +469,6 @@
### 文档与结构
#### qa/ 实现报告收尾·孤儿遗留七项余二项(2026-10-14 登记,来源=批次移交)
**实况**:qa 收尾批核查 iter19 系列出口报告遗留移交项时发现**不在待办清单在册**的七项(acceptance-package §三.2/card0 §5.3/followup §七.3/uibackfill §6.2-4/verify §6.1/3/7)。已结案的(tree_rules 受理闭环、X/Y 双源、S1 七项违例修复、提灯目检项、tests.yml 休眠等)已在各报告标注结案证据,不入本条。2026-10-24 处置:第②③④项随迭代 36 卡C 落地销号(营地 STONE 深档六处、迷雾三关派生行、开场后缀条件加算区),第⑥项按用户 2026-10-24 裁整条注销(TG_* 装饰带非文字,不另立探针)。余二项均为挂号备查/顺路件,无排期承诺(第①项 design/diagrams 越界 2026-10-28 实查核销——目录磁盘与 git 均已不存在,越界实体消失,白名单甲/乙两案失去对象):
5. **字面量墨收测前置**(iter19 卡②遗留):bcard 属性描述/类型脚注/✓章/primary_button 字四处内联 Color8 无常量形态,收测需先在 ui_kit 立常量(呈现批顺路件,改 ui_kit 越当批面)。落点=呈现优化批(ui_kit 常量面)
7. **商店房截图纳屏**(iter19 卡①遗留):商店房截图纳屏留后续呈现批(考证结论在 shot_probe 头注)。本项原并列的「`数据配置.md:223` 历史行死指」半句已于 2026-10-21 实查核销——禁写变更记录规约生效后该文件的版本史行整段已不存在(现全文 212 行,文内零版本史行),被指对象随之消亡。落点=呈现批
**改动面**:游戏代码+工具代码
**状态**:待写卡(备选)
#### 主角棋盘小人像素管线重产——原「美术资产链路审计两项 P0」之 P0②(2026-10-09 登记,来源=审计发现)
**实况**:主角棋盘小人未走像素管线(插值糊图),重产事项未在任何登记处在册;审计报告 HTML 件从未进版本库、现已删除,现状以本条正文为准。2026-10-13 用户令随「UI 美化三层诊断」登记:本条属第三层「观感提升」,Alpha 阶段需完成。原 P0①(文档面:两处引用已丢失的冻结样张路径)2026-10-28 按用户裁乙销号——art-bible §1.4 冻结记录与 art-status-checklist P0 样张行删路径、留「产物原件已丢失」,机检报告凭据(git 提交 7a847c7)保留。
@@ -474,30 +479,6 @@
**状态**:待美术设计
#### 技术债观察登记——长函数余量与 lambda 连接防回归(2026-10-15 登记,来源=批次移交)
**实况**:证据=迭代 27 需求卡片两项「登记待办清单观察」义务批内漏登、卡片核对时补登——①**长函数 17 个**中非结算段的装配/校验函数(`level_map_screen.gd` 选路装配 155 行/`camp_overlay_view.gd` 152 行/`debug_overlay.gd` 114 行/`treasure_item_table.gd` schema 校验群等;`main._build_ui` 已随场景拆分自然解体、`_settle`/`_resolve_hero_attack` 已随波二拆段=17 中 3 件已消解,余 14 件);②**lambda 连接 76 处**(presentation 6/smoke 62/tests 8——实读无泄漏,登记防回归)。长函数拆分无独立需求驱动不动;lambda 连接数若有审计工具化需求归类型闸后续。
**改动面**:游戏代码
**状态**:待写卡(备选)
#### 测试域遗留(2026-10-16 登记,来源=批次移交)
**实况**:证据=迭代 29 批内未收余量(交付报告与验收审计申报,报告已按吸收即死退役、git 历史可考)。迭代 29 把验收语义从「跑一遍没崩=通过」扭到「三层完备+判定可靠」,三硬闸(退出码/脚本错误扫描/断言数下限)已落地,L2/L3 从零建起承载。本批未收的余量集中登记在此,均不阻塞、不排期:
| # | 事项 | 落点与要点 |
|---|---|---|
| 1 | **余下 3 个 core 模块仍零指名单测**:`board_data`/`reward_economy`/`skill_lottery` | 测试基建批。本批补了 minesweeper_rules/board_generator/config_db_resolver/config_db_cross_validator 四件;余三件按同一手法补(判据=能否在 `--script` 模式不加载 autoload 验完) |
| 2 | **render_gate 的 SCRIPT ERROR 扫描仍缺失** | 测试基建批。判定主体是 golden 容差与取证文件存在性,脚本错误不改退出码时无扫描面;本批只把矩阵状态如实记为「缺失」,收它的写法待裁 |
| 3 | **E 级卫生件**(test_kit static var 串台风险、4 套件自建断言出口、157 处 untyped 容器、76 处 lambda) | 工程债批。本批只统一了**新写**断言的出口,存量未动;test_kit 的 static var 是「并发跑批」的前置障碍(并发分层方案的前提) |
| 4 | **行覆盖率度量缺失** | 基建评估项。Godot 无内建、第三方属新基建;现行补洞靠「零指名清单 + 人工判定」,是否引入度量待裁 |
| 5 | **parity 逐位一致比对的触面判定为粗粒度路径匹配** | 测试基建批。触面表含 `battle_orchestrator.gd`,改该文件即要求批准发起 MC;若嫌过重可精化到「是否改动取数/结算数值」的语义面 |
**改动面**:游戏代码
**状态**:待写卡(备选)
#### 营地交互重做·五张需求卡片(2026-10-18 登记,来源=会话提及)
**实况**:2026-10-17~18 需求讨论澄清会话产出结论文档,用户令按工作内容维度拆卡、卡片不预占迭代号。五卡已写(顺序=执行顺序):①存档制数据层改造批 ②主菜单三入口化批 ③选档界面新建批 ④营地场景图新建批 ⑤Esc 浮窗与营地顶栏批(各卡估时与内容见卡片自身)。三道复核:grilling 子 Agent 对抗审查(五卡各一)→v1.1;用户裁定「事项按推荐的来」→v1.2;用户两次订正(**卡④留在批内不踢出**;前置归用户自立批,Agent 不代跑不代产——曾越权起草的点选反馈设计稿 v0.1 已撤回删除)→卡④ v2.2;**卡④ 2026-10-21 用户令退回美术设计段(v2.3,非有效卡,美术设计闭合后重立卡)**。①②③⑤ 零待裁点可编工程波次:②③ 硬耦合同波(「进入存档」钮落点=选档界面),① 是 ③ 前置;⑤ 随 ④ 代码波同行。Alpha 出口裁决(2026-10-21 用户裁):五卡整体列入 Alpha——08「三 · 界面」已开条目,Alpha 出口依赖五卡完工与卡④三个用户侧前置销号。**工程波次一已落地(2026-10-22)**:卡①②③ 随迭代 33 实现批合回 master(合并提交 ebf40947/5c2a7bcf/508f190b,总收口 6e8f72b2),三卡需求卡片与批次产出物已按「吸收即死」退役(设计口径已吸收进 存档序列化.md/生涯.md/camp.md/08-Alpha版本标准.md v1.13/art-bible.md v3.18,反哺落文凭据=文档批 git 提交 1864f157+9f7b3e8a);卡④(营地场景图)退回美术设计段待重立卡、卡⑤(Esc 浮窗)随卡④ 代码波同行,两卡未编排——编入哪一迭代由用户经迭代编排段发令。关联文档: