diff --git a/design/art/art-bible.md b/design/art/art-bible.md index 9e819ac8..d4be1e2c 100644 --- a/design/art/art-bible.md +++ b/design/art/art-bible.md @@ -63,7 +63,7 @@ > **样张 v2 记录(2026-09-28,四冻结生效)**:机检 16/16 全绿(扩展断言集:A3 图底分离 Δ≥55/F6 墙格顶面立面双明度带 Δ≥18/F7 整板悬崖 DEPTH_PX 与游戏渲染常量同源/A6 观感密度梯度均值 ≥4.70=v1 实测 4.09×1.15)。**四冻结**——①砖面形态:方案甲 21 瓦片+高墙感 D 投影档+整板悬崖(§3.2 高墙瓦片体系条定稿参数);②色板:小人蓝 #2E86DE/接触带微光 #D6ECFF/类型微光五组定稿(§4.1/§4.2);③动画方案:§8.4 帧数档冻结;④数字形制:色牌形制定稿(#F5EAD3 素色直角小牌+Fusion Pixel+字号 max(18, ceil(格边×0.5))+墨三档,§7.4)。产物=`artwork/mockups/sample-batch-v2-2026-09-28.png`,机检报告=git 提交 f9b0765;气质终审=批末用户目检(闸形 A,2026-09-27 拍板)。**(v3.0 注:四冻结之①砖面形态的数字承载面与④数字形制随色牌降级解冻,待样张 v3 重冻;②色板③动画维持冻结不变)** -> **样张 v3 冻结记录(2026-09-30,迭代 9 T1 批 A——四冻结①④重冻生效,圣经升 v3.1)**:机检 17/17 全绿,断言定值两件落锁——A3b 图底分离**定值 Δ≥30**(方向稿实测 38;v2 阈值 55 系色牌制地板 L0.036 时代校定,与格底分档带数学冲突退役)/A4 尺寸基线**硬等冻结**(立绘 46×48+棋盘小人源 32×32;资产升档批换代须显式解冻重校);F6 沿用 v3.4 双明度带口径 Δ≥18。**重冻两件**——①砖面形态:E 格高墙 30% 承接词表扩展(南邻墙地面格南部 30% 承接墙面、墙顶纹理**原亮度**直贴零暗档零阴影、(b)-(c) 连接处无边框〔渲染格线跳段〕、连续墙体内侧不出立面;南向厚度面+v3.4 暗档 D 投影保留)+**格底明度分档**(已开格底=世界底层档 L≈0.10,候选带 0.08–0.14;落码程序归一,地板素材重产归资产批)+装饰物件级解禁(专属形状家族,零信息纪律不变)/④数字形制:**亮墨三档直画**(1–3 冷亮 #D8ECF0/4–5 暖白 #F0E2C8/6–8 金亮 #F8E3A8,直画已开格底,实测对比 6.0–6.3:1)+**色牌后备**(#F5EAD3 仅复杂格底——物件纹样/装饰变体格——垫牌兜底,牌上深墨族 12:1)。②色板③动画维持 v2 冻结不变。产物=`artwork/mockups/sample-batch-v3-final-2026-09-30.png`,机检报告=git 提交 7a847c7【冻结】;气质终审=2026-09-29 用户验收豁免(新 v3.5 方向稿验收通过,状态记录 281)。 +> **样张 v3 冻结记录(2026-09-30,迭代 9 T1 批 A——四冻结①④重冻生效,圣经升 v3.1)**:机检 17/17 全绿,断言定值两件落锁——A3b 图底分离**定值 Δ≥30**(方向稿实测 38;v2 阈值 55 系色牌制地板 L0.036 时代校定,与格底分档带数学冲突退役)/A4 尺寸基线**硬等冻结**(立绘 46×48+棋盘小人源 32×32;资产升档批换代须显式解冻重校);F6 沿用 v3.4 双明度带口径 Δ≥18。**重冻两件**——①砖面形态:E 格高墙 30% 承接词表扩展(南邻墙地面格南部 30% 承接墙面、墙顶纹理**原亮度**直贴零暗档零阴影、(b)-(c) 连接处无边框〔渲染格线跳段〕、连续墙体内侧不出立面;南向厚度面+v3.4 暗档 D 投影保留)+**格底明度分档**(已开格底=世界底层档 L≈0.10,候选带 0.08–0.14;落码程序归一,地板素材重产归资产批)+装饰物件级解禁(专属形状家族,零信息纪律不变)/④数字形制:**亮墨三档直画**(1–3 冷亮 #D8ECF0/4–5 暖白 #F0E2C8/6–8 金亮 #F8E3A8,直画已开格底,实测对比 6.0–6.3:1)+**色牌后备**(#F5EAD3 仅复杂格底——物件纹样/装饰变体格——垫牌兜底,牌上深墨族 12:1)。②色板③动画维持 v2 冻结不变。产物原件已丢失(未入 git、主工作区不可查),机检报告=git 提交 7a847c7【冻结】;气质终审=2026-09-29 用户验收豁免(新 v3.5 方向稿验收通过,状态记录 281)。 --- diff --git a/design/art/art-status-checklist.md b/design/art/art-status-checklist.md index cb9ebd80..86a3a372 100644 --- a/design/art/art-status-checklist.md +++ b/design/art/art-status-checklist.md @@ -10,7 +10,7 @@ | 素材 | 状态 | 需求文档 | |---|---|---| -| 格子样张 | ✅ 样张 v3 冻结(2026-09-30 机检 17/17 全绿,art-bible §1.4 v3.2 记录;v2/v3 两次冻结通过;产物 `artwork/mockups/sample-batch-v3-final-2026-09-30.png`) | [art-bible](art-bible.md) §1.4 | +| 格子样张 | ✅ 样张 v3 冻结(2026-09-30 机检 17/17 全绿,art-bible §1.4 v3.2 记录;v2/v3 两次冻结通过;产物原件已丢失、未入 git) | [art-bible](art-bible.md) §1.4 | | 战斗一屏样张 | ⬜ | [战斗表现](../gdd/systems/战斗循环/战斗表现.md)(一屏样张验收标准)+ [art-bible](art-bible.md) §5.1 | ## P1 角色级(样张通过后开工) diff --git a/design/engineering/process-conventions.md b/design/engineering/process-conventions.md index 660701d0..32335b8f 100644 --- a/design/engineering/process-conventions.md +++ b/design/engineering/process-conventions.md @@ -54,7 +54,7 @@ 11. **文档同步闸(实现批禁写文档)**——实现批**禁写 `design/` 整个目录与根规则文件**,要往外交代的东西一律落 `reports/`:实现报告 `reports/iter-实现报告-<日期>.md`(各卡状态+实机目检总清单+遗留读数义务+实现决定记录——超白名单/超卡片明列范围事项逐条记「决了什么/为什么/代价/撤销方式」,全部按卡执行时只写一行「零拍板」)、GDD 反哺点清单 `reports/iter-反哺点-<日期>.md`(每条写清哪个文件哪一节、改成什么、依据是什么)。设计权威文档(GDD/art-bible)**任何批都不直改**:用户在会话中逐点裁决后,由工程/文档设计改动批(直改类)写入 `design/`(执行入口=doc-design 技能),并把反哺条目从 `reports/` 清掉(吸收即死;退役执行=batch-closure 技能)。机械面文档(roadmap 行/术语表/README 工具注)同样归工程/文档设计改动批在合并前更新。反哺点未落地期间相关 GDD 节视为待反哺,读前先查 `reports/` 与待办清单;需求卡片必列「关联文档同步面」按两层标注 12. **实机目检归用户,只登记不阻塞**——真人检测项登记进实现报告目检总清单与体验验证清单即完成批内义务;禁把等目检当停工理由或完成前置 13. **中间产物吸收即死**——细则见 AGENTS.md 硬性红线与 doc-conventions 第 11 节 -14. **改动级验收闭合**——写卡期列需求侧对照表,实现期补实际对照表(测试文件:行),每个行为改动做撤销证明;合回前独立验收审计。细则=test-conventions §8 +14. **改动级验收闭合**——写卡期列需求侧对照表,实现期补实际对照表(测试文件:行),每个行为改动的验收断言从真实入口进入、在当轮真实跑批中执行并通过(判据绑定面口径=test-conventions §8.4;禁变异类测试与撤销类取证,2026-10-24/28 用户两裁);合回前独立验收审计。细则=test-conventions §8 ## 4. 测试执行两入口铁规(细则) diff --git a/design/gdd/02-系统清单.md b/design/gdd/02-系统清单.md index 01a69a23..2f74f45f 100644 --- a/design/gdd/02-系统清单.md +++ b/design/gdd/02-系统清单.md @@ -15,7 +15,7 @@ | `术语表.md` | 全库专用术语的唯一对照表——危险格/安全格/同调位/RAP/GP/真视等定义+退役词对照(2026-10-11 建立,v1.23) | | `待办清单.md`(仓库根,不属本树) | 全项目待办事项的唯一登记处;各系统文档的「待办」节只留指路存根 | | `体验验证清单.md`(仓库根,不属本树) | 需要真人试玩观测的验证项登记处(与待办清单分工:待办清单是"要拍板的事",本篇是"要玩过才知道的事");每条注明结论回流路径 | -| `08-Alpha版本标准.md` | Alpha 版本包含哪些东西的清单——里程碑 M3 的完成标准;只列条目不写内容,定义一律指路各权威文档(**已定稿 v1.17**,2026-10-13) | +| `08-Alpha版本标准.md` | Alpha 版本包含哪些东西的清单——里程碑 M3 的完成标准;只列条目不写内容,定义一律指路各权威文档(**已定稿 v1.19**,2026-10-13) | ## systems/ 五个循环 diff --git a/design/gdd/08-Alpha版本标准.md b/design/gdd/08-Alpha版本标准.md index f4bc9b92..ea7461dd 100644 --- a/design/gdd/08-Alpha版本标准.md +++ b/design/gdd/08-Alpha版本标准.md @@ -2,7 +2,7 @@ > **一句话定位**:本文定义这个项目的 **Alpha 版本包含哪些东西**——它是里程碑 M3 的完成标准,也是判断"还差什么"的对照表。 > **本文只列条目,不写内容**:每样东西是什么、规则怎么走,一律归各自的权威文档,本文只给指路链接。想知道某样东西的定义,请点链接,不要在本文里找解释。 -> **状态**:**已定稿(v1.18)**——2026-10-13 用户令定稿,历次升版明细见 git 提交史。Alpha 的内容范围自此冻结:增删条目一律须经用户裁决。 +> **状态**:**已定稿(v1.19)**——2026-10-13 用户令定稿,历次升版明细见 git 提交史。Alpha 的内容范围自此冻结:增删条目一律须经用户裁决。 > **成文**:2026-10-13。 ## 本文怎么用 @@ -48,7 +48,7 @@ Alpha 内以玩法循环本身的内容与表现为主(棋盘、关卡、战 | 冒险结构 | [冒险结构](systems/冒险循环/冒险结构.md) | 在役 | | 关卡节点地图 | [关卡节点地图](systems/冒险循环/关卡节点地图.md) | 在役(信息迷雾类型可见性已随迭代 34 卡④ 实装,2026-10-22) | | 关卡节点图重构(增加可选节点) | [关卡节点地图](systems/冒险循环/关卡节点地图.md)+[关卡节点图生成](systems/冒险循环/关卡节点图生成.md) | 在役(2026-10-25 迭代 35 卡①② 合回:图源切为参数档×种子生成图、模板链路退役;真人目检选路项已登记体验验证清单,登记不阻塞) | -| 关卡节点图生成 | [关卡节点图生成](systems/冒险循环/关卡节点图生成.md)(参数化长图:产出规格与生成参数) | 在役(2026-10-24 迭代 35 卡① 生成器核心合回;enemy.tres 与 board_gen.tres 已覆盖到第三关深度 13) | +| 关卡节点图生成 | [关卡节点图生成](systems/冒险循环/关卡节点图生成.md)(参数化长图:产出规格与生成参数) | 在役(2026-10-24 迭代 35 卡① 生成器核心合回;enemy.tres 与 board_gen.tres 已覆盖到第三关深度 13;生成图 L3 深度域 0–13 超出关卡地图屏画布屏预算约 10 层——等比缩放/滚动视口两升级路径未设计待裁,[待办清单](../../待办清单.md)「超预算图策略升级路径」条在册) | | 难度曲线与 Boss | [难度曲线与Boss](systems/冒险循环/难度曲线与Boss.md) | 在役 | | 奇物 | [奇物](systems/冒险循环/奇物.md) | 在役 | diff --git a/game/core/action_economy.gd b/game/core/action_economy.gd index a90a66ff..5a49f05b 100644 --- a/game/core/action_economy.gd +++ b/game/core/action_economy.gd @@ -62,7 +62,7 @@ func _phase_text(p: int) -> String: func _cost(kind: String) -> int: - var ae = db.action_economy + var ae = db.run.action_economy match kind: "dig": return ae.dig_ap_cost # 基础挖格成本恒表值(钻头对价挂付费开掘侧) diff --git a/game/core/effect_engine.gd b/game/core/effect_engine.gd index 9388a003..4033eb64 100644 --- a/game/core/effect_engine.gd +++ b/game/core/effect_engine.gd @@ -101,7 +101,7 @@ func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void: ## 特性订阅装载域(feats 表序→按持有集过滤→行内数组序——装载声明序第一段) func _load_feat_triggers(feat_ids: Array) -> void: - if db != null and db.feats != null: + if db != null and db.crossrun.feats != null: for row in db.crossrun.feats.rows: var fid := String(row.get("feat_id", "")) if not feat_ids.has(fid): diff --git a/game/presentation/battle_narrator.gd b/game/presentation/battle_narrator.gd index d8874ef6..ee8671ca 100644 --- a/game/presentation/battle_narrator.gd +++ b/game/presentation/battle_narrator.gd @@ -98,31 +98,36 @@ static func curio_pick_header(payload: Dictionary) -> String: # 对齐 v2.2 追认 b 口径:小/大前缀(小GP/大GP 族)、恢复/AP 不带档位词;道具件掷定前置时 # treasure_line = 具体道具「名 · 效果摘要」行——常空走通用分支) static func bonus_card_text(id: String, tier: String, value: int, treasure_line: String = "") -> String: - var ts := "" - if tier == "common": - ts = " · 小" - elif tier == "rare": - ts = " · 大" - match id: - "attack": - return "攻击力提升%s\n攻击 +%d" % [ts, value] - "max_hp": - return "生命上限提升%s\n生命上限 +%d" % [ts, value] - "armor": - return "护甲等级提升%s\n护甲 +%d" % [ts, value] - "gp": - return "GP 袋%s\nGP +%d" % [ts, value] - "heal": - return "恢复药剂\n生命 +%d(当即回复)" % value - "treasure_item": - if treasure_line != "": - return "秘宝\n%s" % treasure_line - return "秘宝\n随机获得一件" - "ap": - return "行动点提升\n每战首轮 AP +%d" % value + if id == "treasure_item": + return _bonus_treasure_line(treasure_line) + var ts := _tier_suffix(tier) + var templates := {"attack": "攻击力提升%s\n攻击 +%d", "max_hp": "生命上限提升%s\n生命上限 +%d", + "armor": "护甲等级提升%s\n护甲 +%d", "gp": "GP 袋%s\nGP +%d"} + if templates.has(id): + return templates[id] % [ts, value] + if id == "heal": + return "恢复药剂\n生命 +%d(当即回复)" % value + if id == "ap": + return "行动点提升\n每战首轮 AP +%d" % value return id +## 档位尾注域(common=小/rare=大——旧载荷防御口径) +static func _tier_suffix(tier: String) -> String: + if tier == "common": + return " · 小" + if tier == "rare": + return " · 大" + return "" + + +## 秘宝卡文案域(treasure_line 空串=随机获得一件) +static func _bonus_treasure_line(treasure_line: String) -> String: + if treasure_line != "": + return "秘宝\n%s" % treasure_line + return "秘宝\n随机获得一件" + + # 三选一兑付反馈行(选中件效果 + 丢弃提示;tier 缺省普通档——旧载荷防御口径) static func bonus_result_text(result: Dictionary) -> String: var name := "" diff --git a/game/presentation/camp_flow_context.gd b/game/presentation/camp_flow_context.gd index e1f384f8..e8dd0b47 100644 --- a/game/presentation/camp_flow_context.gd +++ b/game/presentation/camp_flow_context.gd @@ -4,33 +4,16 @@ extends RefCounted # 原 13 参长签名收一处命名面——单测可直接 new() 本 context + 控制器获得独立实例化缝)。 # 字段即原构造参数逐一同名迁移;依赖仅视图句柄 + 查询/编排 Callable(组合根注入)。 +## 三束按域收拢(u21——同 u20 形;views=视图句柄/getters=查询 Callable 活引用/flow=编排 Callable): +## 原 12 参长签名收 4 参——字段即原构造参数逐一同名迁移,键面=消费方改址合同。 var db -var meta_get: Callable # () -> MetaProgressionCore(现值回调——组合根 lambda) -var save_get: Callable # () -> SaveManagerCore -var camp_overlay: CampOverlay -var shop_overlay: ShopOverlay -var toast: ToastStack -var start_overlay: Control -var logger # PlaytestLogger(试玩日志验收取数面) -var runner_get: Callable # () -> LevelRunner -var open_modal: Callable -var close_modal: Callable -var refresh_presentations: Callable +var views: Dictionary = {} # {camp_overlay, shop_overlay, toast, start_overlay, logger} +var getters: Dictionary = {} # {meta_get, save_get, runner_get} +var flow: Dictionary = {} # {open_modal, close_modal, refresh_presentations} -func _init(db_ref, meta_get_cb: Callable, save_get_cb: Callable, camp_overlay_ref: CampOverlay, - shop_overlay_ref: ShopOverlay, toast_ref: ToastStack, start_overlay_ref: Control, - logger_ref, runner_get_cb: Callable, open_modal_cb: Callable, close_modal_cb: Callable, - refresh_presentations_cb: Callable) -> void: +func _init(db_ref, views_bundle: Dictionary, getters_bundle: Dictionary, flow_bundle: Dictionary) -> void: db = db_ref - meta_get = meta_get_cb - save_get = save_get_cb - camp_overlay = camp_overlay_ref - shop_overlay = shop_overlay_ref - toast = toast_ref - start_overlay = start_overlay_ref - logger = logger_ref - runner_get = runner_get_cb - open_modal = open_modal_cb - close_modal = close_modal_cb - refresh_presentations = refresh_presentations_cb + views = views_bundle + getters = getters_bundle + flow = flow_bundle diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index fea7cb36..174ed80b 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -21,17 +21,17 @@ var _refresh_presentations: Callable func _init(ctx: CampFlowContext) -> void: # 构造参数收拢 context 对象(13 参长签名收一处命名面) db = ctx.db - _meta_get = ctx.meta_get - _save_get = ctx.save_get - camp_overlay = ctx.camp_overlay - shop_overlay = ctx.shop_overlay - toast = ctx.toast - start_overlay = ctx.start_overlay - logger = ctx.logger - _runner_get = ctx.runner_get - _open_modal = ctx.open_modal - _close_modal = ctx.close_modal - _refresh_presentations = ctx.refresh_presentations + _meta_get = ctx.getters.meta_get + _save_get = ctx.getters.save_get + camp_overlay = ctx.views.camp_overlay + shop_overlay = ctx.views.shop_overlay + toast = ctx.views.toast + start_overlay = ctx.views.start_overlay + logger = ctx.views.logger + _runner_get = ctx.getters.runner_get + _open_modal = ctx.flow.open_modal + _close_modal = ctx.flow.close_modal + _refresh_presentations = ctx.flow.refresh_presentations # 打开营地(主菜单入口——开合与开始层互斥仿设置面先例:开始覆盖层非模态且树序在后, diff --git a/game/presentation/curio_flow_context.gd b/game/presentation/curio_flow_context.gd index 480fb587..1e9ac5bf 100644 --- a/game/presentation/curio_flow_context.gd +++ b/game/presentation/curio_flow_context.gd @@ -4,42 +4,16 @@ extends RefCounted # 原 16 参长签名收一处命名面——单测可直接 new() 本 context + 控制器获得独立实例化缝)。 # 字段即原构造参数逐一同名迁移;依赖仅视图句柄 + 查询/编排 Callable(组合根注入)。 +## 三束按域收拢(u20——views=视图句柄/getters=查询 Callable 活引用〔禁快照〕/flow=编排 Callable): +## 原 16 参长签名收 4 参——字段即原构造参数逐一同名迁移,键面=消费方改址合同。 var db -var meta_get: Callable # () -> MetaProgressionCore(现值回调——组合根 lambda) -var curio_overlay: CurioPickOverlay -var end_overlay: BattleEndOverlay -var board_view: BoardView -var toast: ToastStack -var save_get: Callable # () -> SaveManagerCore -var adventure_get: Callable # () -> AdventureRunner(会话现值) -var runner_get: Callable # () -> LevelRunner -var adventure_mode_get: Callable # () -> bool -var open_modal: Callable # (Control) -> void -var open_modal_after_playback: Callable -var close_modal: Callable -var refresh_presentations: Callable -var fatal_screen: Callable -var gp_summary: Callable # (Dictionary) -> String +var views: Dictionary = {} # {curio_overlay, end_overlay, board_view, toast} +var getters: Dictionary = {} # {meta_get, save_get, adventure_get, runner_get, adventure_mode_get} +var flow: Dictionary = {} # {open_modal, open_modal_after_playback, close_modal, refresh_presentations, fatal_screen, gp_summary} -func _init(db_ref, meta_get_cb: Callable, curio_overlay_ref: CurioPickOverlay, end_overlay_ref: BattleEndOverlay, - board_view_ref: BoardView, toast_ref: ToastStack, save_get_cb: Callable, - adventure_get_cb: Callable, runner_get_cb: Callable, adventure_mode_get_cb: Callable, - open_modal_cb: Callable, open_modal_after_playback_cb: Callable, close_modal_cb: Callable, - refresh_presentations_cb: Callable, fatal_screen_cb: Callable, gp_summary_cb: Callable) -> void: +func _init(db_ref, views_bundle: Dictionary, getters_bundle: Dictionary, flow_bundle: Dictionary) -> void: db = db_ref - meta_get = meta_get_cb - curio_overlay = curio_overlay_ref - end_overlay = end_overlay_ref - board_view = board_view_ref - toast = toast_ref - save_get = save_get_cb - adventure_get = adventure_get_cb - runner_get = runner_get_cb - adventure_mode_get = adventure_mode_get_cb - open_modal = open_modal_cb - open_modal_after_playback = open_modal_after_playback_cb - close_modal = close_modal_cb - refresh_presentations = refresh_presentations_cb - fatal_screen = fatal_screen_cb - gp_summary = gp_summary_cb + views = views_bundle + getters = getters_bundle + flow = flow_bundle diff --git a/game/presentation/curio_interact_controller.gd b/game/presentation/curio_interact_controller.gd index f5cbf94f..c59ff1c1 100644 --- a/game/presentation/curio_interact_controller.gd +++ b/game/presentation/curio_interact_controller.gd @@ -8,21 +8,8 @@ extends RefCounted const Narrator := preload("res://presentation/battle_narrator.gd") var db -var _meta_get: Callable # () → 元进度账本现值(组合根 lambda——探针 isolate_save 换注面) -var curio_overlay: CurioPickOverlay -var end_overlay: BattleEndOverlay -var board_view: BoardView -var toast: ToastStack -var _save_get: Callable # () → 存档承载现值(同上——Attunement.slots 读数面) -var _adventure_get: Callable # () → 冒险结构现值(组合根 lambda——随局更替) -var _runner_get: Callable # () → 关卡推进器现值(组合根 lambda——reward_node 继续推进) -var _adventure_mode_get: Callable # () → 冒险模式标记现值(组合根 lambda) -var _open_modal: Callable # 模态栈单口(组合根) -var _open_modal_after_playback: Callable # 演播后排窗单口(组合根) -var _close_modal: Callable -var _refresh_presentations: Callable -var _fatal_screen: Callable -var _gp_summary: Callable # () → 战斗总结 GP 行文案(组合根 lambda——_last_gain_amount 取数面在根) +# u13 引用束注入:16 个 _init 抄写成员删除——正文读点改址 ctx 束(u20 三束键名; +# _pending_curio/_curio_context 纯内部态保持宿主) var _pending_curio := {} # 三选一停点载荷暂存(战斗总结结算确认后开三选一面;续玩停点复用) var _curio_context := "level_clear" # 三选一面上下文(level_clear=关卡完成停点→推进下一关 / reward_node=奖励节点→回图选路) @@ -31,21 +18,8 @@ var _curio_context := "level_clear" # 三选一面上下文(level_clear= func _init(ctx: CurioFlowContext) -> void: # 构造参数收拢 context 对象(16 参长签名收一处命名面) db = ctx.db - _meta_get = ctx.meta_get - curio_overlay = ctx.curio_overlay - end_overlay = ctx.end_overlay - board_view = ctx.board_view - toast = ctx.toast - _save_get = ctx.save_get - _adventure_get = ctx.adventure_get - _runner_get = ctx.runner_get - _adventure_mode_get = ctx.adventure_mode_get - _open_modal = ctx.open_modal - _open_modal_after_playback = ctx.open_modal_after_playback - _close_modal = ctx.close_modal - _refresh_presentations = ctx.refresh_presentations - _fatal_screen = ctx.fatal_screen - _gp_summary = ctx.gp_summary + ctx.flow.fatal_screen = ctx.fatal_screen + ctx.flow.gp_summary = ctx.gp_summary # 停点在途判据(组合根 _on_battle_end_continue 分派面——战斗总结确认后开三选一) @@ -66,17 +40,17 @@ func resume_if_pending() -> bool: # 差异仅零面可收——curio_overlay 未开不 close。core 停点经 skip_empty_curio 消化后推进 #(continue_to_next_level 的「停点未兑付拒推进」守卫因此放行)。 func _skip_empty_stop() -> bool: - toast.show_toast("可用战利品为空——本次三选一跳过", "info") + ctx.views.toast.show_toast("可用战利品为空——本次三选一跳过", "info") var adventure = _adventure_get.call() if adventure != null: adventure.stop().skip_empty_curio() # core 停点消化(其守卫=仅空候选放行) if _curio_context == "reward_node": - var runner = _runner_get.call() + var runner = ctx.getters.runner_get.call() if runner != null: runner.continue_after_node() - elif _curio_context == "level_clear" and bool(_adventure_mode_get.call()): + elif _curio_context == "level_clear" and bool(ctx.getters.adventure_mode_get.call()): if not adventure.continue_to_next_level(): - _fatal_screen.call("关卡推进失败——见输出日志") + ctx.flow.fatal_screen.call("关卡推进失败——见输出日志") # feat_ticket = 只消化停点回店(零推进——与 on_continue 同构) _pending_curio = {} return true @@ -93,22 +67,22 @@ func on_presented(payload: Dictionary) -> void: # 空候选防御(零卡不开死面;core _redeem_feat_ticket 守卫外的末道) _skip_empty_stop() return - _open_modal.call(curio_overlay) - curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(payload.get("candidates", []) as Array)) + ctx.flow.open_modal.call(ctx.views.curio_overlay) + ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(payload.get("candidates", []) as Array)) return if int(payload.get("aux_cleared", 0)) > 0: - toast.show_toast("上关剩余 %d 件秘宝已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 清空反馈 + ctx.views.toast.show_toast("上关剩余 %d 件秘宝已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 清空反馈 _pending_curio = payload.duplicate(true) _curio_context = "level_clear" if bool(payload.get("refreshed", false)): # 刷新重发:refreshed 标直落选择面——不再重开胜利总结面 show_pick() return - end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续", - board_view.get_global_rect(), - "%s\n%s" % [String(_gp_summary.call()), Narrator.bonus_keystone_line(payload)]) - _open_modal_after_playback.call(end_overlay) - _refresh_presentations.call() + ctx.views.end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续", + ctx.views.board_view.get_global_rect(), + "%s\n%s" % [String(ctx.flow.gp_summary.call()), Narrator.bonus_keystone_line(payload)]) + ctx.flow.open_modal_after_playback.call(ctx.views.end_overlay) + ctx.flow.refresh_presentations.call() # 奖励节点三选一(与关卡完成三选一共享同池同权重同上限;CurioPickOverlay 共用面)。 @@ -121,13 +95,13 @@ func present_node_curio() -> void: if node_cands.is_empty(): # 空池降级——不开面、不挂节点停点(present_node_curio 未调即未挂), # 兑付确认直落回图选路(auto-sim「node_entered 回调内立即 continue_after_node」先例) - toast.show_toast("可用战利品为空——本次三选一跳过", "info") - var runner = _runner_get.call() + ctx.views.toast.show_toast("可用战利品为空——本次三选一跳过", "info") + var runner = ctx.getters.runner_get.call() if runner != null: runner.continue_after_node() return - _open_modal.call(curio_overlay) - curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(node_cands)) + ctx.flow.open_modal.call(ctx.views.curio_overlay) + ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(node_cands)) adventure.stop().present_node_curio(node_cands) _feed_refresh_display(adventure) # 刷新面投喂单点 @@ -135,12 +109,12 @@ func present_node_curio() -> void: # 三选一面开出(战斗总结结算确认 / 续玩停点恢复) func show_pick() -> void: var payload: Dictionary[Variant, Variant] = _pending_curio - _open_modal.call(curio_overlay) - curio_overlay.open_pick(Narrator.curio_pick_header(payload), _curio_card_texts(payload.get("candidates", []) as Array)) + ctx.flow.open_modal.call(ctx.views.curio_overlay) + ctx.views.curio_overlay.open_pick(Narrator.curio_pick_header(payload), _curio_card_texts(payload.get("candidates", []) as Array)) var adventure = _adventure_get.call() if adventure != null: _feed_refresh_display(adventure) # 刷新面投喂单点 - _refresh_presentations.call() + ctx.flow.refresh_presentations.call() # 刷新呈现投喂单点(免费余量 X/Y + GP 付费价 + 冒险钱包可付——读数与商店 set_refresh_display 同构) @@ -148,7 +122,7 @@ func _feed_refresh_display(adventure: AdventureRunner) -> void: var runner = _runner_get.call() var gp := int(runner.gp) if runner != null else 0 var cost: int = adventure.refresh_bank().gp_cost() - curio_overlay.set_refresh_display(adventure.refresh_bank().free_left(), cost, gp >= cost) + ctx.views.curio_overlay.set_refresh_display(adventure.refresh_bank().free_left(), cost, gp >= cost) func _curio_card_texts(candidates: Array) -> Array: @@ -189,15 +163,15 @@ func on_refresh() -> void: # 免费位(X/Y)已穷——GP 付费档(先扣钱包后重抽,余额不足拒绝零副作用) var cost: int = adventure.refresh_bank().gp_cost() if runner == null or not runner.spend_gp(cost): - toast.show_toast("GP不足——刷新失败", "warn") + ctx.views.toast.show_toast("GP不足——刷新失败", "warn") return refreshed = adventure.stop().refresh_curio_paid() - _refresh_presentations.call() # 钱包变化上屏(对齐商店购买后刷新面) + ctx.flow.refresh_presentations.call() # 钱包变化上屏(对齐商店购买后刷新面) if not refreshed: return if _curio_context == "reward_node": var st: Dictionary[Variant, Variant] = adventure.stop().node_curio_pending_state() - curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st.get("candidates", []) as Array)) + ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st.get("candidates", []) as Array)) _feed_refresh_display(adventure) @@ -218,31 +192,31 @@ func on_pick_confirmed(item_id: String) -> void: # 后续兑付/确认仍走节点路径回图选路) if _curio_context == "reward_node" and kind == "feat_ticket": var st2: Dictionary[Variant, Variant] = adventure.stop().node_curio_pending_state() - curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st2.get("candidates", []) as Array)) + ctx.views.curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st2.get("candidates", []) as Array)) # 兑付反馈(curio_resolved 到达——结果行上屏,唯一出口 = 继续) func on_resolved(result: Dictionary) -> void: _pending_curio = {} - curio_overlay.show_result(Narrator.bonus_result_text(result)) - _refresh_presentations.call() + ctx.views.curio_overlay.show_result(Narrator.bonus_result_text(result)) + ctx.flow.refresh_presentations.call() # 继续钮:收三选一面 → 上下文分派(关卡完成停点=推进下一关 / 奖励节点=兑付确认回图选路——零自动推进) func on_continue() -> void: - _close_modal.call(curio_overlay) - curio_overlay.close_pick() + ctx.flow.close_modal.call(ctx.views.curio_overlay) + ctx.views.curio_overlay.close_pick() var adventure = _adventure_get.call() if _curio_context == "feat_ticket": return # T5 店内专长凭证面——专长兑付完成后关面回店(无推进/无回图) if _curio_context == "reward_node": - var runner = _runner_get.call() + var runner = ctx.getters.runner_get.call() if runner != null: runner.continue_after_node() return - if bool(_adventure_mode_get.call()) and adventure != null: + if bool(ctx.getters.adventure_mode_get.call()) and adventure != null: if not adventure.continue_to_next_level(): - _fatal_screen.call("关卡推进失败——见输出日志") + ctx.flow.fatal_screen.call("关卡推进失败——见输出日志") # 秘宝 item_id → 显示名投影(keystone_settle_text/替换子命名文案数据源——拼句归 Narrator); diff --git a/game/presentation/hud_presenter.gd b/game/presentation/hud_presenter.gd index 09e8219c..e6ae0aa6 100644 --- a/game/presentation/hud_presenter.gd +++ b/game/presentation/hud_presenter.gd @@ -11,8 +11,13 @@ const Narrator := preload("res://presentation/battle_narrator.gd") # 每次结算行/轮次/节点变化/动作返回后调用(组合根全量重查装配) # 钱包 GP/钥石不在本装配面投喂(中/大循环资源迁出战斗屏——信息归属单一展示面原则); # HP/状态效果单投喂 B 区站桩(B/C 双投喂属重复展示——单一展示面原则) -static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView, - settlement, runner, level_active: bool, db, refresh_remaining: int) -> void: +# 运行态尾参收拢 ctx 束(u19——键 {settlement, runner, level_active, db, refresh_remaining}) +static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView, ctx: Dictionary) -> void: + var settlement = ctx.get("settlement") + var runner = ctx.get("runner") + var level_active: bool = ctx.get("level_active", false) + var db = ctx.get("db") + var refresh_remaining: int = ctx.get("refresh_remaining", 0) # 特性列表(runner 统一加算区投影:商店购买与关卡大奖励 # 共账本〔level_runner bonus{atk,max_hp,armor_class}〕;空加算=整节隐藏〔视图内空态纪律〕。 # 呈现只读投影零核心触碰;行文格式=白名单项) @@ -36,7 +41,7 @@ static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApB info.set_feat_holdings([]) if settlement == null: info.set_board_variants([]) # 战斗域角标出战斗即隐(空态纪律) - _refresh_out_of_battle(stage, info, ap_bar, runner, level_active, db) + _refresh_out_of_battle(stage, info, ap_bar, ctx) return var s: Dictionary[Variant, Variant] = settlement.presentation_snapshot() # 地形变异常驻角标投影(整图变异快照 → 文案行; @@ -44,7 +49,7 @@ static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApB info.set_board_variants(board_variant_lines(db, s.get("board_variants", []))) var hero: Dictionary[Variant, Variant] = s.hero if hero.is_empty() or not bool(hero.get("alive", false)): - _refresh_out_of_battle(stage, info, ap_bar, runner, level_active, db) + _refresh_out_of_battle(stage, info, ap_bar, ctx) return _refresh_hero(stage, info, s) _refresh_economy(ap_bar, s, db, refresh_remaining) @@ -92,7 +97,7 @@ static func _refresh_economy(ap_bar: ApBarView, s: Dictionary, db: ConfigDb, ref # 无 archetype 字段,聚合形态会空键兜底实显「流派 ×N · 档一」僵尸文案;流派档位非现行机制) static func feat_holdings_lines(db: ConfigDb, held: Array) -> Array[String]: var rlines: Array[String] = [] - if db == null or db.feats == null or (db.crossrun.feats.rows as Array).is_empty() or held.is_empty(): + if db == null or db.crossrun.feats == null or (db.crossrun.feats.rows as Array).is_empty() or held.is_empty(): return rlines var counts := {} for rid in held: @@ -109,7 +114,7 @@ static func feat_holdings_lines(db: ConfigDb, held: Array) -> Array[String]: # 文案用常规表述,不用俚语 static func board_variant_lines(db: ConfigDb, ids: Array) -> Array[String]: var lines: Array[String] = [] - if db == null or db.variation == null: + if db == null or db.combat.variation == null: return lines for vid in ids: var row: Dictionary[Variant, Variant] = db.combat.variation.row_by_id(String(vid)) @@ -145,7 +150,10 @@ static func sync_settlement_visuals(stage: BattleStageView, board_view: BoardVie # 战斗外(节点间隙/关卡结束)——HP 读 runner 携带值 static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelView, - ap_bar: ApBarView, runner, level_active: bool, db) -> void: + ap_bar: ApBarView, ctx: Dictionary) -> void: + var runner = ctx.get("runner") + var level_active: bool = ctx.get("level_active", false) + var db = ctx.get("db") if not level_active: return if runner != null: @@ -185,7 +193,14 @@ static func outcome_text(outcome: Dictionary) -> String: # ---- 批量模拟报告装配(文案归 presenter——视图只 set_text) ---- -static func tune_sim_report(n: int, atk_pct: int, hp_pct: int, wins: int, dmg_all: Array, hp_left_win: Array, hp_boss: Array, rounds_all: Array, death_nodes: Dictionary, base_hp: int) -> String: +## 统计束收拢(u19——stats 六键 {wins, dmg_all, hp_left_win, hp_boss, rounds_all, death_nodes}) +static func tune_sim_report(n: int, atk_pct: int, hp_pct: int, stats: Dictionary, base_hp: int) -> String: + var wins: int = stats.get("wins", 0) + var dmg_all: Array = stats.get("dmg_all", []) + var hp_left_win: Array = stats.get("hp_left_win", []) + var hp_boss: Array = stats.get("hp_boss", []) + var rounds_all: Array = stats.get("rounds_all", []) + var death_nodes: Dictionary = stats.get("death_nodes", {}) var lines: Array[String] = [] lines.append("[b]模拟 N=%d(atk+%d%% / hp+%d%%)[/b]" % [n, atk_pct, hp_pct]) lines.append("关卡完成率 [color=#%s]%.0f%%[/color](%d/%d)" % [UiKit.HEX_LOG_WIN, 100.0 * float(wins) / float(maxi(1, n)), wins, n]) diff --git a/game/presentation/main.gd b/game/presentation/main.gd index fb8b14f5..50ba97ef 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -360,7 +360,7 @@ func _wire_adventure() -> void: # meta.attune_refresh_charges(当前等级行表值)、树 rule 键集=meta.tree_rule_keys(Fork 激活侧 effects); # hero_override 沿用既有 meta 现算口径。static 形态=冒烟直呼单测本函数,免实例化整菜单场景)。 # 两根类型轴随开局下传(attack_type/armor_type——契约 2 所有权移交+契约 8 冒险开始快照; -# 取值=meta.hero_attack_type/hero_armor_type=存档选中路线类型轴,未解锁/异常选中态回退锻造配置默认值)。 +# 取值=meta.hero_type_axes()=存档选中路线类型轴(attack/armor 两键),未解锁/异常选中态回退锻造配置默认值)。 # 注:runner 侧 resume_adventure 受理面收 attunement/attune_refresh_y/tree_rules 三键 # (传参/受理两侧对齐)。 static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref: MetaProgressionCore, extra: Dictionary = {}) -> Dictionary: @@ -453,15 +453,11 @@ func _on_battle_finished(payload: Dictionary) -> void: return "level_end": _end_overlay.open_victory(Narrator.battle_end_text("victory", true), - "返回", board_view.get_global_rect(), _gp_summary_text(payload), - "钥石 · 累计 %d" % run_scope.runner.keystone, HudPresenter.bonus_summary_text(run_scope.runner), - int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp)) + "返回", 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(), _gp_summary_text(payload), - "钥石 · 累计 %d" % run_scope.runner.keystone, HudPresenter.bonus_summary_text(run_scope.runner), - int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp)) + "回到遭遇选择", 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()) @@ -482,6 +478,14 @@ func _gp_summary_text(payload: Dictionary) -> String: # ==================== core↔view 接线(战斗信号统一走这里)==================== + +## 战末账行束(open_victory opts 键装配单点——GP/钥石/加成三行与增量合计) +func _end_victory_opts(payload: Dictionary) -> Dictionary: + return {"gp_summary": _gp_summary_text(payload), + "ks_summary": "钥石 · 累计 %d" % run_scope.runner.keystone, + "bonus_summary": HudPresenter.bonus_summary_text(run_scope.runner), + "gp_gain": int(payload.get("gp_dropped", 0)), "gp_total": int(run_scope.runner.gp)} + func _wire_battle(settlement_ref: BattleOrchestrator) -> void: settlement_ref.step_row.connect(_on_step_row) settlement_ref.battle_finished.connect(_on_battle_finished) @@ -812,7 +816,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, run_scope.settlement, run_scope.runner, run_scope.level_active, db, refresh_remaining) + 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}) _treasure_ui.refresh_slots() # C 区秘宝槽重查(试炼镐积点进度随挖格刷新悬停浮窗) _sync_settlement_visuals() diff --git a/game/presentation/main_adventure_flow.gd b/game/presentation/main_adventure_flow.gd index f05b8c18..66cbe3c2 100644 --- a/game/presentation/main_adventure_flow.gd +++ b/game/presentation/main_adventure_flow.gd @@ -111,7 +111,7 @@ static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void: 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(), - "本局 GP 入账 · 合计 %d" % int(summary.get("gp_final", 0))) # GP 行(取数=summary gp_final 清账前快照);钥石明细已在 text(keystone_settle_text)——三行账 + {"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.logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷) @@ -178,7 +178,7 @@ static func _on_choices_presented(root: Variant, candidates: Array) -> void: 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(), - "战利品 GP +%d · 合计 %d" % [root.run_scope.last_gain_amount, int(root.run_scope.runner.gp)]) + {"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._refresh_presentations() return diff --git a/game/presentation/main_ui_build.gd b/game/presentation/main_ui_build.gd index badcc2bd..89cb145c 100644 --- a/game/presentation/main_ui_build.gd +++ b/game/presentation/main_ui_build.gd @@ -134,11 +134,17 @@ static func _build_curio(root: Variant) -> void: root.add_child(root.curio_overlay) # 提升三选一与替换子交互(本体在 curio_interact_controller.gd;本类只接线与回调注入, # 模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调) - root._bonus = CurioInteractController.new(CurioFlowContext.new(root.db, func() -> Variant: return root.meta, root.curio_overlay, root._end_overlay, root.board_view, - root.board_zone.toast, func() -> Variant: return root.save_manager, func() -> Variant: return root.run_scope.adventure, - func() -> Variant: return root.run_scope.runner, func() -> Variant: return root.run_scope.is_adventure, - root._open_modal, root._open_modal_after_playback, root._close_modal, root._refresh_presentations, - root._fatal_screen, func() -> String: return root._gp_summary_text({"gp_dropped": root.run_scope.last_gain_amount}))) # 构造参数收拢 context + 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}, + {"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, + "fatal_screen": root._fatal_screen, + "gp_summary": func() -> String: return root._gp_summary_text({"gp_dropped": root.run_scope.last_gain_amount}}))) # 构造参数收拢 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) @@ -154,9 +160,13 @@ static func _build_menu_wiring(root: Variant) -> void: root.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, func() -> Variant: return root.meta, func() -> Variant: return root.save_manager, root.camp_overlay, root.shop_overlay, - root.board_zone.toast, root._start_overlay, root.logger, func() -> Variant: return root.run_scope.runner, - root._open_modal, root._close_modal, root._refresh_presentations)) # 构造参数收拢 context + 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}, + {"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, + "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) diff --git a/game/presentation/playtest_logger.gd b/game/presentation/playtest_logger.gd index 1d8dce84..4182051f 100644 --- a/game/presentation/playtest_logger.gd +++ b/game/presentation/playtest_logger.gd @@ -157,8 +157,6 @@ func dump_diagnosis(context: Dictionary) -> String: if f != null: f.store_string(JSON.stringify(dump, " ")) f.close() - print("[playtest_logger] 诊断转储已写入: %s(项目日志目录见输出)" % path) - print(" [diag] %s" % JSON.stringify(context)) return path diff --git a/game/presentation/tune_sim_controller.gd b/game/presentation/tune_sim_controller.gd index bf089c07..5c998019 100644 --- a/game/presentation/tune_sim_controller.gd +++ b/game/presentation/tune_sim_controller.gd @@ -39,32 +39,30 @@ func run(n: int) -> void: # 一文件多局,run 行携带种子切分局次) var logger = PlaytestLogger.new() logger.ap_per_round = int(db.run.action_economy.ap_per_round) - var log_path: String = logger.start_session(SEED_BASE) - print("[tune_sim] 模拟日志: %s" % log_path) + logger.start_session(SEED_BASE) panel.set_sim_running(true) panel.set_sim_progress("模拟中:0/%d …" % n) - var wins := 0 - var dmg_all: Array[float] = [] - var hp_left_win: Array[float] = [] - var hp_boss: Array[float] = [] - var rounds_all: Array[int] = [] - var death_nodes := {} + var stats := {"wins": 0, "dmg_all": [], "hp_left_win": [], "hp_boss": [], "rounds_all": [], "death_nodes": {}} var autoplay = _autoplay() for i in range(n): - var st = autoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger) - dmg_all.append(st.hero_dmg) - rounds_all.append(st.rounds) - if st.result == "victory": - wins += 1 - hp_left_win.append(st.hp_left) - hp_boss.append(st.hp_at_boss) - elif st.result == "defeat": - var key := "%d.%s" % [st.defeat_node_index, st.defeat_node_type] - death_nodes[key] = int(death_nodes.get(key, 0)) + 1 + _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(), - wins, dmg_all, hp_left_win, hp_boss, rounds_all, death_nodes, _base_hp)) + panel.set_sim_report(HudPresenter.tune_sim_report(n, panel.atk_pct(), panel.hp_pct(), stats, _base_hp)) + + +## 单局模拟域(循环体提函数——结果四桶记账;u19 stats 六键同构承载) +func _run_one_level(autoplay, stats: Dictionary, i: int, panel, logger) -> void: + 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 panel.set_sim_running(false) _running = false diff --git a/game/presentation/ui_assembler.gd b/game/presentation/ui_assembler.gd index 4c815653..e0d6909d 100644 --- a/game/presentation/ui_assembler.gd +++ b/game/presentation/ui_assembler.gd @@ -101,31 +101,11 @@ static func build_start_overlay(host: Control, db: ConfigDb = null) -> Dictionar var bd := UiKit.backdrop_tile() if bd != null: overlay.add_child(bd) - overlay.add_child(UiKit.beams_rect()) + overlay.add_child(UiKit._beams_rect()) overlay.add_child(UiKit.vignette_rect()) # 双列骨架:左=操作列 56% / 右=舞台(立绘看台) - var cols := HBoxContainer.new() - cols.set_anchors_preset(Control.PRESET_FULL_RECT) - cols.add_theme_constant_override("separation", 32) - var pad := MarginContainer.new() - pad.set_anchors_preset(Control.PRESET_FULL_RECT) - for side in ["margin_left", "margin_right", "margin_top", "margin_bottom"]: - pad.add_theme_constant_override(side, 56) - cols.add_child(pad) - overlay.add_child(cols) - var box := VBoxContainer.new() - box.name = "start_box" - box.size_flags_horizontal = Control.SIZE_EXPAND_FILL - box.size_flags_stretch_ratio = 1.4 - box.add_theme_constant_override("separation", 10) - pad.add_child(box) - - var title := UiKit.label("Dungeon Sweeper",UiKit.FS_H1, true, UiKit.INK_ON_DEEP) - box.add_child(title) - box.add_child(HSeparator.new()) - - # 三入口(主菜单三入口化——营地交互重做卡②,用户裁决 1:进入存档/设置/退出游戏; - # 组帽双撤——冒险组帽组内零钮退化态不留、「营地与系统」帽指称失义,追认单留痕) + var cols := _start_columns(overlay) + var box := _start_left_column(cols) var entersavebtn := UiKit.primary_button("进入存档") # 主操作钮(唯一进度入口) entersavebtn.tooltip_text = "选择一个存档位进入(新档 / 进档 / 删档)" box.add_child(entersavebtn) @@ -135,12 +115,45 @@ static func build_start_overlay(host: Control, db: ConfigDb = null) -> Dictionar # 退出游戏与设置同层并行——只在一级菜单出现(行为零变化——契约 2) var quitbtn := UiKit.button("退出游戏") box.add_child(quitbtn) - # 调试入口收浮层角标;boot 菜单优先不开 # 浮窗,调试入口=UI 最顶层全局浮层角标(build_debug_badge——LAYER_CONTRACT 末位、 # ModalStack 登记外,任何 UI 不遮挡;本层不设屏内调试角标,全局浮层全场景承载) + _start_right_column(cols) + return {"overlay": overlay, "box": box, + "entersavebtn": entersavebtn, + "settingsbtn": settingsbtn, "quitbtn": quitbtn} - # 右列:立绘看台(三张立绘 + 标题底座;界面禁玩法文字墙,故无速览牌) + +## 双列骨架域:左=操作列 56% / 右=舞台(立绘看台)——全屏锚+56px 四边衬距 +static func _start_columns(overlay: Control) -> HBoxContainer: + var cols := HBoxContainer.new() + cols.set_anchors_preset(Control.PRESET_FULL_RECT) + cols.add_theme_constant_override("separation", 32) + var pad := MarginContainer.new() + pad.set_anchors_preset(Control.PRESET_FULL_RECT) + for side in ["margin_left", "margin_right", "margin_top", "margin_bottom"]: + pad.add_theme_constant_override(side, 56) + cols.add_child(pad) + overlay.add_child(cols) + return cols + + +## 左操作列域:标题+三入口(主菜单三入口化——营地交互重做卡②,用户裁决 1: +## 进入存档/设置/退出游戏;组帽双撤——冒险组帽组内零钮退化态不留、「营地与系统」帽指称失义,追认单留痕) +static func _start_left_column(cols: HBoxContainer) -> VBoxContainer: + var box := VBoxContainer.new() + box.name = "start_box" + box.size_flags_horizontal = Control.SIZE_EXPAND_FILL + box.size_flags_stretch_ratio = 1.4 + box.add_theme_constant_override("separation", 10) + (cols.get_child(0) as MarginContainer).add_child(box) + box.add_child(UiKit.label("Dungeon Sweeper",UiKit.FS_H1, true, UiKit.INK_ON_DEEP)) + box.add_child(HSeparator.new()) + return box + + +## 右列域:立绘看台(三张立绘 + 标题底座;界面禁玩法文字墙,故无速览牌) +static func _start_right_column(cols: HBoxContainer) -> void: var right := VBoxContainer.new() right.name = "start_right" right.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -153,17 +166,7 @@ static func build_start_overlay(host: Control, db: ConfigDb = null) -> Dictionar foe_row.alignment = BoxContainer.ALIGNMENT_CENTER foe_row.add_theme_constant_override("separation", 36) for cfg in [[UiKit.TEX_ENEMY_GOBLIN, 46, 96], [UiKit.TEX_HERO_PORTRAIT, 138, 144], [UiKit.TEX_ENEMY_OGRE_CHIEF, 80, 96]]: - var tr := TextureRect.new() - var tex := UiKit.load_tex(cfg[0]) - if tex != null: - tr.texture = tex - tr.custom_minimum_size = Vector2(cfg[1], cfg[2]) - tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - if cfg[0] != UiKit.TEX_HERO_PORTRAIT: - tr.modulate = Color(0.7, 0.7, 0.7, 0.4) # 敌影剪影退后(§6.1 立绘>背景) - foe_row.add_child(tr) + foe_row.add_child(_start_portrait(cfg)) stage.add_child(foe_row) var pedestal := Panel.new() var ped_sb := UiKit.panel_flat(UiKit.TITLE_PEDESTAL, UiKit.BG_ROOT, 17, 2) @@ -173,9 +176,20 @@ static func build_start_overlay(host: Control, db: ConfigDb = null) -> Dictionar stage.add_child(pedestal) right.add_child(stage) - return {"overlay": overlay, "box": box, - "entersavebtn": entersavebtn, - "settingsbtn": settingsbtn, "quitbtn": quitbtn} + +## 单立绘域(循环体提函数——像素过滤居中;敌影剪影退后 §6.1 立绘>背景) +static func _start_portrait(cfg: Array) -> TextureRect: + var tr := TextureRect.new() + var tex := UiKit.load_tex(cfg[0]) + if tex != null: + tr.texture = tex + tr.custom_minimum_size = Vector2(cfg[1], cfg[2]) + tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + if cfg[0] != UiKit.TEX_HERO_PORTRAIT: + tr.modulate = Color(0.7, 0.7, 0.7, 0.4) + return tr # 调试浮层入口角标(UI 最顶层浮层,任何 UI 不遮挡)——main 直接子层、 diff --git a/game/presentation/views/ap_bar_view.gd b/game/presentation/views/ap_bar_view.gd index 254d88ce..f80a4d6d 100644 --- a/game/presentation/views/ap_bar_view.gd +++ b/game/presentation/views/ap_bar_view.gd @@ -41,6 +41,16 @@ func _init() -> void: row.add_theme_constant_override("separation", 8) add_child(row) + _build_ap_face(row) + _build_rap_face(row) + _build_refresh_confirm(row) + _build_legend_refresh(row) + + +# ---- 构建私有族(AP 计量/RAP 读数/破坏性确认/图例刷新四域) ---- + +# AP 计量域:点阵+读数+停手钮+弹性撑 +func _build_ap_face(row: HBoxContainer) -> void: _dots = HBoxContainer.new() _dots.add_theme_constant_override("separation", 3) _dots.size_flags_vertical = Control.SIZE_SHRINK_CENTER @@ -48,19 +58,19 @@ func _init() -> void: _ap_label = UiKit.label("AP 0/0",UiKit.FS_BODY, true) _ap_label.size_flags_vertical = Control.SIZE_SHRINK_CENTER row.add_child(_ap_label) - _btn_stop = UiKit.button("停手") _btn_stop.custom_minimum_size = Vector2(64, 0) _btn_stop.size_flags_vertical = Control.SIZE_SHRINK_CENTER _btn_stop.pressed.connect(_emit_stop_pressed) row.add_child(_btn_stop) - var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(spacer) - # RAP 读数(自 C 区迁入):RAP 唯一消费口 = 弃图刷新——资源数字贴消费按钮 - # (就近原则);扣费文案「RAP−%d」已在刷新按钮副行,此处不重复代价提示(单一展示面原则) + +# RAP 读数域(自 C 区迁入):RAP 唯一消费口 = 弃图刷新——资源数字贴消费按钮 +# (就近原则);扣费文案「RAP−%d」已在刷新按钮副行,此处不重复代价提示(单一展示面原则) +func _build_rap_face(row: HBoxContainer) -> void: _rap_dots = HBoxContainer.new() _rap_dots.add_theme_constant_override("separation", 2) _rap_dots.size_flags_vertical = Control.SIZE_SHRINK_CENTER @@ -72,7 +82,9 @@ func _init() -> void: _rap_feedback.size_flags_vertical = Control.SIZE_SHRINK_CENTER row.add_child(_rap_feedback) - # 破坏性确认异位(pattern 7):确认 chips 出现在刷新按钮左方——与常规"按钮自身确认"不同位 + +# 破坏性确认异位(pattern 7):确认 chips 出现在刷新按钮左方——与常规"按钮自身确认"不同位 +func _build_refresh_confirm(row: HBoxContainer) -> void: _confirm_row = HBoxContainer.new() _confirm_row.add_theme_constant_override("separation", 4) _confirm_row.visible = false @@ -83,12 +95,14 @@ func _init() -> void: confirm_btn.pressed.connect(_disarm_and_fire) _confirm_row.add_child(confirm_btn) + +# 图例与弃图刷新域:图例开关钮+刷新钮组(副行=剩余次数读数) +func _build_legend_refresh(row: HBoxContainer) -> void: _btn_legend = UiKit.button("图例") _btn_legend.custom_minimum_size = Vector2(56, 0) _btn_legend.size_flags_vertical = Control.SIZE_SHRINK_CENTER _btn_legend.pressed.connect(_emit_legend_toggled) row.add_child(_btn_legend) - var refresh_box := VBoxContainer.new() refresh_box.add_theme_constant_override("separation", 0) refresh_box.size_flags_vertical = Control.SIZE_SHRINK_CENTER diff --git a/game/presentation/views/battle_end_overlay.gd b/game/presentation/views/battle_end_overlay.gd index bfb40e2e..d30b0f1f 100644 --- a/game/presentation/views/battle_end_overlay.gd +++ b/game/presentation/views/battle_end_overlay.gd @@ -10,22 +10,30 @@ const COL_GOLD := UiKit.GOLD const COL_DUSK := UiKit.DUSK # 暮蓝(全作唯一暮蓝档) const COL_WARM := UiKit.WARM -var _dim: ColorRect -var _seal: Panel -var _verdict: Label -var _rows_box: VBoxContainer # 三行账(GP/钥石/本关加成——demo ⑧两列对齐;失败侧整行隐藏) -var _gp_row: HBoxContainer -var _gp_name: Label -var _gp_val: Label -var _ks_row: HBoxContainer -var _ks_name: Label -var _ks_val: Label -var _bonus_row_l: HBoxContainer -var _bonus_name: Label -var _bonus_val: Label -var _cele: TextureRect # 庆祝立绘(idle 占位浮动) -var _btn: Button -var _board_rect := Rect2() +## 内部状态件(u6 子域拆件——契约 9 职责语句「战末呈现与演出态」):单一变化原因=终局画面呈现与 +## 演出计数。探针直读冻结成员(_btn/_gp_name/_gp_row/_gp_val)住宿主不进状态件。 +class _EndOverlayState extends RefCounted: + var _dim: ColorRect + var _seal: Panel + var _verdict: Label + var _rows_box: VBoxContainer # 三行账(GP/钥石/本关加成——demo ⑧两列对齐;失败侧整行隐藏) + var _ks_row: HBoxContainer + var _ks_name: Label + var _ks_val: Label + var _bonus_row_l: HBoxContainer + var _bonus_name: Label + var _bonus_val: Label + var _cele: TextureRect # 庆祝立绘(idle 占位浮动) + var _board_rect := Rect2() + var _crest_bricks: Array[ColorRect] = [] # 纹章碎砖驻留面(close 时回收) + var _fx_counts := {"crest": 0, "dust": 0, "seal": 0} # 演出计数(fx_probe 断言面——crest/dust/seal 三键) + + +var st := _EndOverlayState.new() +var _gp_row: HBoxContainer # 探针直读冻结(S17⑤) +var _gp_name: Label # 探针直读冻结 +var _gp_val: Label # 探针直读冻结 +var _btn: Button # 探针直读冻结 func _init() -> void: @@ -33,21 +41,45 @@ func _init() -> void: #(与营地/设置/图例同族)。半透明氛围 dim 保留(胜利暖纱/失败暮蓝——open_* 就地换色),信息与 # 按钮全部上板;金蜡封 = 面板头部静态徽记(坠落演出落点被面板遮挡)。 # 骨架经 UiKit.modal_shell;_dim 引用摘出供 open_* 换氛围色。 - var shell := UiKit.modal_shell(self, Color(COL_WARM, 0.0), Vector2(520, 0), 12, true, + var shell := UiKit.modal_shell(self, Color(COL_WARM, 0.0), Vector2(520, 0), 12, UiKit.panel_skinned(Vector4(16, 14, 16, 14))) - _dim = shell.dim + st._dim = shell.dim var box: VBoxContainer = shell.box + _build_header(box) + _build_ledger(box) + # 无明细回看钮——冒险中回看归「生涯」 + _btn = UiKit.primary_button("继续") + _btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER # 紧凑钮 + _btn.custom_minimum_size.x = 150 + _btn.pressed.connect(_emit_continue_pressed) + box.add_child(_btn) + visible = false + + +## 头部域:金蜡封徽记+判定行+庆祝立绘(demo ⑧——idle 帧浮动占位;减动效=静帧) +func _build_header(box: VBoxContainer) -> void: var header := HBoxContainer.new() header.add_theme_constant_override("separation", 12) header.alignment = BoxContainer.ALIGNMENT_CENTER box.add_child(header) - _seal = Panel.new() - _seal.mouse_filter = Control.MOUSE_FILTER_IGNORE + st._seal = Panel.new() + st._seal.mouse_filter = Control.MOUSE_FILTER_IGNORE var sb := UiKit.panel_flat(COL_GOLD, UiKit.GOLD_DEEP, 200, 4) - _seal.add_theme_stylebox_override("panel", sb) - _seal.custom_minimum_size = Vector2(52, 52) - _seal.size_flags_vertical = Control.SIZE_SHRINK_CENTER - header.add_child(_seal) + st._seal.add_theme_stylebox_override("panel", sb) + st._seal.custom_minimum_size = Vector2(52, 52) + st._seal.size_flags_vertical = Control.SIZE_SHRINK_CENTER + header.add_child(st._seal) + st._seal.add_child(_seal_img()) + st._verdict = UiKit.label("",UiKit.FS_TITLE, true, UiKit.INK_TITLE) + st._verdict.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + st._verdict.size_flags_vertical = Control.SIZE_SHRINK_CENTER + header.add_child(st._verdict) + st._cele = _cele_rect() + header.add_child(st._cele) + + +## 蜡封图域(32px 像素过滤居中) +func _seal_img() -> TextureRect: var seal_img := TextureRect.new() var seal_tex := UiKit.load_tex("res://assets/art/board/seal_mark.png") if seal_tex != null: @@ -58,76 +90,76 @@ func _init() -> void: seal_img.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST seal_img.size_flags_vertical = Control.SIZE_SHRINK_CENTER seal_img.mouse_filter = Control.MOUSE_FILTER_IGNORE - _seal.add_child(seal_img) - _verdict = UiKit.label("",UiKit.FS_TITLE, true, UiKit.INK_TITLE) - _verdict.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - _verdict.size_flags_vertical = Control.SIZE_SHRINK_CENTER - header.add_child(_verdict) - # 庆祝立绘(demo ⑧——idle 帧浮动占位;减动效=静帧) - _cele = TextureRect.new() + return seal_img + + +## 庆祝立绘域(64px idle 帧) +func _cele_rect() -> TextureRect: + var cele := TextureRect.new() var cele_tex := UiKit.load_tex(UiKit.TEX_HERO_IDLE) if cele_tex != null: - _cele.texture = cele_tex - _cele.custom_minimum_size = Vector2(64, 64) - _cele.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - _cele.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - _cele.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - _cele.size_flags_vertical = Control.SIZE_SHRINK_CENTER - _cele.visible = false - header.add_child(_cele) - _rows_box = VBoxContainer.new() - _rows_box.add_theme_constant_override("separation", 6) - _rows_box.visible = false - box.add_child(_rows_box) - # 两列对齐账行(审计差距④对齐 demo ⑧——项目名左/数值右,定宽 360) + cele.texture = cele_tex + cele.custom_minimum_size = Vector2(64, 64) + cele.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + cele.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + cele.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + cele.size_flags_vertical = Control.SIZE_SHRINK_CENTER + cele.visible = false + return cele + + +## 三行账域(两列对齐账行——审计差距④对齐 demo ⑧:项目名左/数值右,定宽 360) +func _build_ledger(box: VBoxContainer) -> void: + st._rows_box = VBoxContainer.new() + st._rows_box.add_theme_constant_override("separation", 6) + st._rows_box.visible = false + box.add_child(st._rows_box) _gp_row = _ledger_row() _gp_name = _ledger_name(UiKit.FS_SUB, UiKit.GOLD_INK, true) # GOLD_DEEP 对结算板 2.67:1 越带,换深金墨 5.21:1 _gp_val = _ledger_value(UiKit.FS_SUB, UiKit.GOLD_INK, true) # 同上 _gp_row.add_child(_gp_name) _gp_row.add_child(_gp_val) - _rows_box.add_child(_gp_row) - _ks_row = _ledger_row() - _ks_name = _ledger_name(UiKit.FS_BODY, UiKit.INK_LABEL, false) - _ks_val = _ledger_value(UiKit.FS_BODY, UiKit.INK_LABEL, true) - _ks_row.add_child(_ks_name) - _ks_row.add_child(_ks_val) - _ks_row.visible = false - _rows_box.add_child(_ks_row) - _bonus_row_l = _ledger_row() - _bonus_name = _ledger_name(UiKit.FS_HINT, UiKit.INK_LABEL, false) - _bonus_val = _ledger_value(UiKit.FS_HINT, UiKit.INK_LABEL, true) - _bonus_row_l.add_child(_bonus_name) - _bonus_row_l.add_child(_bonus_val) - _bonus_row_l.visible = false - _rows_box.add_child(_bonus_row_l) + st._rows_box.add_child(_gp_row) + st._ks_row = _ledger_row() + st._ks_name = _ledger_name(UiKit.FS_BODY, UiKit.INK_LABEL, false) + st._ks_val = _ledger_value(UiKit.FS_BODY, UiKit.INK_LABEL, true) + st._ks_row.add_child(st._ks_name) + st._ks_row.add_child(st._ks_val) + st._ks_row.visible = false + st._rows_box.add_child(st._ks_row) + st._bonus_row_l = _ledger_row() + st._bonus_name = _ledger_name(UiKit.FS_HINT, UiKit.INK_LABEL, false) + st._bonus_val = _ledger_value(UiKit.FS_HINT, UiKit.INK_LABEL, true) + st._bonus_row_l.add_child(st._bonus_name) + st._bonus_row_l.add_child(st._bonus_val) + st._bonus_row_l.visible = false + st._rows_box.add_child(st._bonus_row_l) box.add_child(HSeparator.new()) - # 无明细回看钮——冒险中回看归「生涯」 - _btn = UiKit.primary_button("继续") - _btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER # 紧凑钮 - _btn.custom_minimum_size.x = 150 - _btn.pressed.connect(_emit_continue_pressed) - box.add_child(_btn) - visible = false # 终局画面数据锚 = 战斗结算 判定行(verdict)——判定行文案由组合根装配传入。 # gp_summary(胜利总结结算展示 GP 总量+增量):组合根装配「战利品 +增量 · 合计总量」 # 传入(入账次序 = LevelRunner 入账先于组合根 handler,runner.gp 即战后总量);空串 = 不显示(失败侧恒空)。 -func open_victory(verdict_line: String, continue_label: String, board_rect: Rect2, gp_summary: String = "", - ks_summary: String = "", bonus_summary: String = "", gp_gain := -1, gp_total := -1) -> void: - _dim.color = Color(UiKit.DIM_NIGHT, 0.62) # 全屏压暗对齐 demo - _verdict.text = verdict_line - _seal.visible = true # 胜利徽记(面板头部静态) - _seal.modulate = Color.WHITE +# 可选尾参收拢 opts 束(u6——键 {gp_summary, ks_summary, bonus_summary, gp_gain, gp_total},缺省逐字同值) +func open_victory(verdict_line: String, continue_label: String, board_rect: Rect2, opts := {}) -> void: + var gp_summary: String = opts.get("gp_summary", "") + var ks_summary: String = opts.get("ks_summary", "") + var bonus_summary: String = opts.get("bonus_summary", "") + var gp_gain: int = opts.get("gp_gain", -1) + var gp_total: int = opts.get("gp_total", -1) + st._dim.color = Color(UiKit.DIM_NIGHT, 0.62) # 全屏压暗对齐 demo + st._verdict.text = verdict_line + st._seal.visible = true # 胜利徽记(面板头部静态) + st._seal.modulate = Color.WHITE _set_ledger_row(_gp_name, _gp_val, gp_summary, true) # GP 行=固定名「战利品 GP」(滚动/整串均只入值列) - _set_ledger_row(_ks_name, _ks_val, ks_summary) - _ks_row.visible = ks_summary != "" - _set_ledger_row(_bonus_name, _bonus_val, bonus_summary) - _bonus_row_l.visible = bonus_summary != "" - _rows_box.visible = gp_summary != "" or ks_summary != "" or bonus_summary != "" + _set_ledger_row(st._ks_name, st._ks_val, ks_summary) + st._ks_row.visible = ks_summary != "" + _set_ledger_row(st._bonus_name, st._bonus_val, bonus_summary) + st._bonus_row_l.visible = bonus_summary != "" + st._rows_box.visible = gp_summary != "" or ks_summary != "" or bonus_summary != "" _btn.text = continue_label - _board_rect = board_rect - _cele.visible = true + st._board_rect = board_rect + st._cele.visible = true _play_celebrate() visible = true if UiKit.victory_fx_style == "dust": @@ -141,23 +173,23 @@ func open_victory(verdict_line: String, continue_label: String, board_rect: Rect func open_defeat(verdict_line: String, continue_label: String, board_rect: Rect2) -> void: # 明度不变的暮蓝色相氛围(全屏唯一暮蓝档——§2.5 最静一拍) - _dim.color = Color(COL_DUSK, 0.6) # 同族压暗 - _verdict.text = verdict_line - _board_rect = board_rect + st._dim.color = Color(COL_DUSK, 0.6) # 同族压暗 + st._verdict.text = verdict_line + st._board_rect = board_rect visible = true - _seal.visible = true - _seal.modulate = Color(0.4, 0.4, 0.4) # 失败徽记灰化(demo ⑧) - _cele.visible = false - _rows_box.visible = false # 失败侧不呈现账行 + st._seal.visible = true + st._seal.modulate = Color(0.4, 0.4, 0.4) # 失败徽记灰化(demo ⑧) + st._cele.visible = false + st._rows_box.visible = false # 失败侧不呈现账行 _play_dust_sadle() # 坐砖掸灰(§2.5——终稿) _play_seal_recall(8) func close() -> void: visible = false - _seal.visible = false - _seal.modulate = Color.WHITE - _cele.visible = false + st._seal.visible = false + st._seal.modulate = Color.WHITE + st._cele.visible = false var tw: Tween = get_meta(&"celebrate_tw", null) # 循环句柄随节点存(跨 open/close,close 必杀) if tw != null and tw.is_valid(): tw.kill() @@ -170,7 +202,7 @@ func is_open() -> bool: func verdict_text() -> String: - return _verdict.text + return st._verdict.text # 两列账行工厂(审计差距④——名左/值右,定宽 360) @@ -217,68 +249,83 @@ func _fx_center() -> Vector2: # 封印化光点收回(失败——插过的封印逐面化光点收回占位,§2.5) func _play_seal_recall(count: int) -> void: - _fx_seal_dots = count + st._fx_counts["seal"] = count + var base := st._board_rect.get_center() if st._board_rect.size != Vector2.ZERO else get_viewport_rect().size * 0.5 for i in count: - var dot := ColorRect.new() - dot.color = COL_GOLD - dot.size = Vector2(6, 6) - var base := _board_rect.get_center() if _board_rect.size != Vector2.ZERO else get_viewport_rect().size * 0.5 - dot.position = base + Vector2((float(i % 5) - 2.0) * _board_rect.size.x * 0.14 if _board_rect.size.x > 0 else 0.0, - (float(i % 3) - 1.0) * _board_rect.size.y * 0.18) # 确定性散布(S2 禁全局随机——占位演出同守) - dot.mouse_filter = Control.MOUSE_FILTER_IGNORE - add_child(dot) - var tw := dot.create_tween() - tw.tween_interval(UiKit.motion(0.1 * i)) - tw.tween_property(dot, "position:y", dot.position.y - 120.0, UiKit.motion(0.8)) - tw.parallel().tween_property(dot, "modulate:a", 0.0, UiKit.motion(0.8)) - tw.tween_callback(dot.queue_free) + _spawn_seal_dot(i, base) + + +## 单光点域(循环体提函数——延迟升空渐隐回收;确定性散布 S2 禁全局随机) +func _spawn_seal_dot(i: int, base: Vector2) -> void: + var dot := ColorRect.new() + dot.color = COL_GOLD + dot.size = Vector2(6, 6) + dot.position = base + Vector2((float(i % 5) - 2.0) * st._board_rect.size.x * 0.14 if st._board_rect.size.x > 0 else 0.0, + (float(i % 3) - 1.0) * st._board_rect.size.y * 0.18) + dot.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(dot) + var tw := dot.create_tween() + tw.tween_interval(UiKit.motion(0.1 * i)) + tw.tween_property(dot, "position:y", dot.position.y - 120.0, UiKit.motion(0.8)) + tw.parallel().tween_property(dot, "modulate:a", 0.0, UiKit.motion(0.8)) + tw.tween_callback(dot.queue_free) # ---- 演出密度(程序化 tween,确定性散布,零新帧资产) ---- -var _crest_bricks: Array[ColorRect] = [] # 纹章碎砖驻留面(close 时回收) -var _fx_crest_shards := 0 -var _fx_dust_motes := 0 -var _fx_seal_dots := 0 +var st._crest_bricks: Array[ColorRect] = [] # 纹章碎砖驻留面(close 时回收) +var st._fx_crest_shards := 0 +var st._fx_dust_motes := 0 +var st._fx_seal_dots := 0 # 碎砖拼纹章(胜利 §2.4——蜡封落定后十枚碎砖自四周向蜡封外环收拢拼成纹章环,驻留至 close) func _play_crest_assemble() -> void: _clear_crest_bricks() - _fx_crest_shards = 10 + st._fx_counts["crest"] = 10 var c := _fx_center() for i in 10: - var brick := ColorRect.new() - brick.color = UiKit.BRICK if i % 2 == 0 else UiKit.FLOOR # 碎砖=砖面族色(掩尘/裸土——图底同源) - brick.size = Vector2(12, 7) - brick.mouse_filter = Control.MOUSE_FILTER_IGNORE - var ang := TAU * float(i) / 10.0 - var ring := c + Vector2(cos(ang), sin(ang)) * 64.0 - brick.position = c + Vector2(cos(ang), sin(ang)) * 200.0 # 自四周场外入场 - add_child(brick) - _crest_bricks.append(brick) - var btw := brick.create_tween() - btw.tween_interval(UiKit.motion(0.52)) # 待蜡封落定(seal 0.42+0.22 弹跳中段) - btw.tween_property(brick, "position", ring - brick.size * 0.5, UiKit.motion(0.4)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) - btw.parallel().tween_property(brick, "rotation", sin(ang) * 0.35, UiKit.motion(0.4)) + _spawn_crest_brick(i, c) + + +## 单碎砖域(循环体提函数——待蜡封落定后自场外收拢入环;碎砖=砖面族色〔掩尘/裸土——图底同源〕) +func _spawn_crest_brick(i: int, c: Vector2) -> void: + var brick := ColorRect.new() + brick.color = UiKit.BRICK if i % 2 == 0 else UiKit.FLOOR + brick.size = Vector2(12, 7) + brick.mouse_filter = Control.MOUSE_FILTER_IGNORE + var ang := TAU * float(i) / 10.0 + var ring := c + Vector2(cos(ang), sin(ang)) * 64.0 + brick.position = c + Vector2(cos(ang), sin(ang)) * 200.0 # 自四周场外入场 + add_child(brick) + st._crest_bricks.append(brick) + var btw := brick.create_tween() + btw.tween_interval(UiKit.motion(0.52)) # 待蜡封落定(seal 0.42+0.22 弹跳中段) + btw.tween_property(brick, "position", ring - brick.size * 0.5, UiKit.motion(0.4)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + btw.parallel().tween_property(brick, "rotation", sin(ang) * 0.35, UiKit.motion(0.4)) # 坐砖掸灰(失败 §2.5——暖灰尘絮自棋盘中带缓缓飘落,最静一拍的运动承载) func _play_dust_sadle() -> void: - _fx_dust_motes = 8 + st._fx_counts["dust"] = 8 var base := _fx_center() for i in 8: - var mote := ColorRect.new() - mote.color = Color(UiKit.WARM_FAINT, 0.85) - mote.size = Vector2(4, 4) - mote.mouse_filter = Control.MOUSE_FILTER_IGNORE - mote.position = base + Vector2((float(i % 4) - 1.5) * 30.0, -24.0 - 8.0 * float(i / 4)) - add_child(mote) - var mtw := mote.create_tween() - mtw.tween_interval(UiKit.motion(0.08 * i)) - mtw.tween_property(mote, "position:y", mote.position.y + 64.0, UiKit.motion(0.9)) - mtw.parallel().tween_property(mote, "modulate:a", 0.0, UiKit.motion(0.9)) - mtw.tween_callback(mote.queue_free) + _spawn_dust_mote(i, base) + + +## 单尘絮域(循环体提函数——中带散布缓降渐隐) +func _spawn_dust_mote(i: int, base: Vector2) -> void: + var mote := ColorRect.new() + mote.color = Color(UiKit.WARM_FAINT, 0.85) + mote.size = Vector2(4, 4) + mote.mouse_filter = Control.MOUSE_FILTER_IGNORE + mote.position = base + Vector2((float(i % 4) - 1.5) * 30.0, -24.0 - 8.0 * float(i / 4)) + add_child(mote) + var mtw := mote.create_tween() + mtw.tween_interval(UiKit.motion(0.08 * i)) + mtw.tween_property(mote, "position:y", mote.position.y + 64.0, UiKit.motion(0.9)) + mtw.parallel().tween_property(mote, "modulate:a", 0.0, UiKit.motion(0.9)) + mtw.tween_callback(mote.queue_free) # 庆祝立绘浮动(idle 占位——UiKit.motion 守减动效;motion_scale<1 静帧) @@ -288,29 +335,34 @@ func _play_celebrate() -> void: var prev: Tween = get_meta(&"celebrate_tw", null) if prev != null and prev.is_valid(): prev.kill() - var tw := _cele.create_tween() + var tw := st._cele.create_tween() set_meta(&"celebrate_tw", tw) tw.set_loops() - tw.tween_property(_cele, "position:y", _cele.position.y - 4.0, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) - tw.tween_property(_cele, "position:y", _cele.position.y, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) + tw.tween_property(st._cele, "position:y", st._cele.position.y - 4.0, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) + tw.tween_property(st._cele, "position:y", st._cele.position.y, UiKit.motion(0.6)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) # 尘金粒子(备选演出——金屑自面板上带飘落闪灭;确定性散布守 S2) func _play_victory_dust() -> void: - _fx_dust_motes += 10 + st._fx_counts["dust"] += 10 var c := _fx_center() for i in 10: - var mote := ColorRect.new() - mote.color = UiKit.CANDLE - mote.size = Vector2(5, 5) - mote.mouse_filter = Control.MOUSE_FILTER_IGNORE - mote.position = c + Vector2((float(i % 5) - 2.0) * 44.0, -40.0 - 10.0 * float(i / 5)) - add_child(mote) - var tw := mote.create_tween() - tw.tween_interval(UiKit.motion(0.12 * i)) - tw.tween_property(mote, "position:y", mote.position.y + 90.0, UiKit.motion(1.1)) - tw.parallel().tween_property(mote, "modulate:a", 0.0, UiKit.motion(1.1)) - tw.tween_callback(mote.queue_free) + _spawn_victory_mote(i, c) + + +## 单金屑域(循环体提函数——上带散布飘落闪灭) +func _spawn_victory_mote(i: int, c: Vector2) -> void: + var mote := ColorRect.new() + mote.color = UiKit.CANDLE + mote.size = Vector2(5, 5) + mote.mouse_filter = Control.MOUSE_FILTER_IGNORE + mote.position = c + Vector2((float(i % 5) - 2.0) * 44.0, -40.0 - 10.0 * float(i / 5)) + add_child(mote) + var tw := mote.create_tween() + tw.tween_interval(UiKit.motion(0.12 * i)) + tw.tween_property(mote, "position:y", mote.position.y + 90.0, UiKit.motion(1.1)) + tw.parallel().tween_property(mote, "modulate:a", 0.0, UiKit.motion(1.1)) + tw.tween_callback(mote.queue_free) # GP 行数字滚动(demo ⑧——0.8s 三次缓出;减动效直出终值) @@ -325,15 +377,15 @@ func _roll_gp(gp_gain: int, gp_total: int) -> void: func _clear_crest_bricks() -> void: - for b in _crest_bricks: + for b in st._crest_bricks: if is_instance_valid(b): b.queue_free() - _crest_bricks.clear() + st._crest_bricks.clear() # 演出探针出口(fx_probe——胜利纹章/失败掸灰/封印光收回三断言面) func fx_probe() -> Dictionary: - return {"crest_shards": _fx_crest_shards, "dust_motes": _fx_dust_motes, "seal_dots": _fx_seal_dots} + return {"crest_shards": st._fx_counts["crest"], "dust_motes": st._fx_counts["dust"], "seal_dots": st._fx_counts["seal"]} func _emit_continue_pressed() -> void: diff --git a/game/presentation/views/battle_log_view.gd b/game/presentation/views/battle_log_view.gd index fb036e42..30dd7e2c 100644 --- a/game/presentation/views/battle_log_view.gd +++ b/game/presentation/views/battle_log_view.gd @@ -145,71 +145,122 @@ func _format_row(row: Dictionary, agg_count := -1) -> String: var src := _source_label(String(row.get("source_class", ""))) var round_no: int = int(row.get("round_index", 0)) var prefix := " [color=#%s]R%d%s[/color] " % [UiKit.HEX_LOG_ROUND, round_no, ("·" + src) if src != "" else ""] - var hl := String(row.get("highlight", "")) - var v: int = int(row.get("value", 0)) - var name_: String = String(row.get("display_name", "")) - var target := String(row.get("target", "")) - var tail := " → %s" % target if target != "" else "" var suffix := "(%s)" % src if src != "" else "" - match String(row.get("type", "")): + var kind := String(row.get("type", "")) + var body := "" + match kind: "effect": - # GP 型效果行:「GP 袋 +3」兑付反馈——value = 本行 ±power, - # escrow = 战斗内缓冲现值(入钱包在战斗末一次上抛——呈现语义与增益池分账) - if String(row.get("effect_id", "")) in ["gp_gain", "gp_loss"]: - var gp_col := UiKit.HEX_LOG_GAIN if v > 0 else UiKit.HEX_LOG_LOSS - var gp_body := "%s %+d(战斗暂存 %+d)%s" % [name_, v, int(row.get("escrow", 0)), suffix] - return prefix + (_gold_frame(gp_body) if hl != "" else "[color=%s]%s[/color]" % [gp_col, gp_body]) - # 效果行 value = 单次贡献值,pool_value = 池合成值——禁相加呈现(战斗结算 呈现细则) - var pool: int = int(row.get("pool_value", 0)) - var col := UiKit.HEX_LOG_HEAL if v > 0 else UiKit.HEX_LOG_LOSS - var body := "%s 单次%+d(池 %+d)%s" % [name_, v, pool, suffix] - return prefix + (_gold_frame(body) if hl != "" else "[color=%s]%s[/color]" % [col, body]) + body = _fmt_effect(row, prefix, suffix) "damage": - # 伤害行无行上拆行文案——行上归因读数只剩掷点档位随行 - # 护甲等级掷点留档(掷点读数与档位随行可读;吃掷点分量恒带,棋盘来源行不带) - var roll_txt := "" - if row.get("roll", null) != null: - roll_txt = "〔掷 %d vs 护甲 %d" % [int(row.roll), int(row.ac)] - if bool(row.get("crit", false)): - roll_txt += " · 暴击×%d" % int(_db.combat.settlement.damage_roll.crit_multiplier) - elif int(row.tier) < 100: - roll_txt += " · ×%d%%" % int(row.tier) - roll_txt += "〕" - var kill := " ⚔击杀!" if row.get("killed", false) else "" - # 伤害数字不明显——伤害值放大加色独立于行文(朱红大一号——数字即信息主角,非同字号色温微差) - var body := "%s [color=#%s][size=17]-%d[/size][/color]%s%s%s%s" % [name_, UiKit.HEX_LOG_DMG_NUM, v, roll_txt, tail, kill, suffix] - var body_col := UiKit.HEX_LOG_DMG_KILL if row.get("killed", false) else (UiKit.HEX_LOG_ROUND if v == 0 else UiKit.HEX_LOG_DMG_BODY) - if bool(row.get("crit", false)): - return prefix + _gold_frame(body) # nat 最大面暴击 = 金框高亮行 - return prefix + "[color=%s]%s[/color]" % [body_col, body] - "heal": - return prefix + "[color=#%s]%s +%d%s%s[/color]" % [UiKit.HEX_LOG_HEAL, name_, v, tail, suffix] + body = _fmt_damage(row, prefix, suffix) + "heal", "cast", "decay": + body = _fmt_simple(kind, row, prefix, suffix) "reward_ineffective": - # 未生效类行:⚠ icon + 划线形状冗余(非色相独载)+ 聚合计数 - var n := (" ×%d" % agg_count) if agg_count > 1 else "" - return prefix + "[s][color=#%s]⚠ %s%s[/color][/s]%s" % [UiKit.HEX_LOG_VOIDED, name_, n, suffix] + body = _fmt_ineffective(row, prefix, suffix, agg_count) "seal_miss": - var miss_note := "" - if hl == "perfect_broken": - miss_note = "(本图完美封印已破——清图奖励不再加倍)" - elif hl == "": - miss_note = "(首次封印落空已豁免——不计入完美封印判定)" - var body := "✗ %s%s" % [name_, miss_note] - return prefix + (_gold_frame(body) if hl == "perfect_broken" else "[color=#%s]%s[/color]" % [UiKit.HEX_LOG_ROUND, body]) + body = _fmt_seal_miss(row, prefix) + "verdict": + body = _fmt_verdict(row) + _: + body = _fmt_default(row, prefix, suffix) + return body + + +## 简单行域(heal/cast/decay 三型——单行单墨直排) +func _fmt_simple(kind: String, row: Dictionary, prefix: String, suffix: String) -> String: + var name_: String = String(row.get("display_name", "")) + match kind: + "heal": + return prefix + "[color=#%s]%s +%d%s%s[/color]" % [UiKit.HEX_LOG_HEAL, name_, int(row.get("value", 0)), _tail_of(row), suffix] "cast": # 施放行:增益技 0 伤害施放,具名技能 return prefix + "[color=#%s]✦ %s%s[/color]" % [UiKit.HEX_LOG_CAST, name_, suffix] "decay": - return prefix + "[color=#%s]状态衰减 ×%d[/color]" % [UiKit.HEX_LOG_DECAY, v] - "verdict": - if int(row.value) == 1: - return "[color=#%s][b]◆ %s[/b][/color]" % [UiKit.HEX_LOG_WIN, name_] - return "[color=#%s][b]◆ %s[/b][/color]" % [UiKit.HEX_LOG_GAIN, name_] - "highlight": - return prefix + _gold_frame("★ %s" % name_) + return prefix + "[color=#%s]状态衰减 ×%d[/color]" % [UiKit.HEX_LOG_DECAY, int(row.get("value", 0))] + return "" + + +## 默认行域(未知型/高亮星标——未知型=名行直排) +func _fmt_default(row: Dictionary, prefix: String, suffix: String) -> String: + var name_: String = String(row.get("display_name", "")) + if String(row.get("type", "")) == "highlight": + return prefix + _gold_frame("★ %s" % name_) return prefix + name_ + suffix +## 尾注域:目标格指向(行文附加) +func _tail_of(row: Dictionary) -> String: + var target := String(row.get("target", "")) + return " → %s" % target if target != "" else "" + + +## GP 型效果行域:「GP 袋 +3」兑付反馈——value = 本行 ±power, +## escrow = 战斗内缓冲现值(入钱包在战斗末一次上抛——呈现语义与增益池分账) +func _fmt_effect(row: Dictionary, prefix: String, suffix: String) -> String: + var v: int = int(row.get("value", 0)) + var hl := String(row.get("highlight", "")) + var name_: String = String(row.get("display_name", "")) + if String(row.get("effect_id", "")) in ["gp_gain", "gp_loss"]: + var gp_col := UiKit.HEX_LOG_GAIN if v > 0 else UiKit.HEX_LOG_LOSS + var gp_body := "%s %+d(战斗暂存 %+d)%s" % [name_, v, int(row.get("escrow", 0)), suffix] + return prefix + (_gold_frame(gp_body) if hl != "" else "[color=%s]%s[/color]" % [gp_col, gp_body]) + # 效果行 value = 单次贡献值,pool_value = 池合成值——禁相加呈现(战斗结算 呈现细则) + var pool: int = int(row.get("pool_value", 0)) + var col := UiKit.HEX_LOG_HEAL if v > 0 else UiKit.HEX_LOG_LOSS + var body := "%s 单次%+d(池 %+d)%s" % [name_, v, pool, suffix] + return prefix + (_gold_frame(body) if hl != "" else "[color=%s]%s[/color]" % [col, body]) + + +## 伤害行域:行上归因读数只剩掷点档位随行(护甲等级掷点留档——掷点读数与档位随行可读; +## 吃掷点分量恒带,棋盘来源行不带)。伤害数字不明显——伤害值放大加色独立于行文(朱红大一号) +func _fmt_damage(row: Dictionary, prefix: String, suffix: String) -> String: + var v: int = int(row.get("value", 0)) + var name_: String = String(row.get("display_name", "")) + var tail := _tail_of(row) + var roll_txt := "" + if row.get("roll", null) != null: + roll_txt = "〔掷 %d vs 护甲 %d" % [int(row.roll), int(row.ac)] + if bool(row.get("crit", false)): + roll_txt += " · 暴击×%d" % int(_db.combat.settlement.damage_roll.crit_multiplier) + elif int(row.tier) < 100: + roll_txt += " · ×%d%%" % int(row.tier) + roll_txt += "〕" + var kill := " ⚔击杀!" if row.get("killed", false) else "" + var body := "%s [color=#%s][size=17]-%d[/size][/color]%s%s%s%s" % [name_, UiKit.HEX_LOG_DMG_NUM, v, roll_txt, tail, kill, suffix] + if bool(row.get("crit", false)): + return prefix + _gold_frame(body) # nat 最大面暴击 = 金框高亮行 + var body_col := UiKit.HEX_LOG_DMG_KILL if row.get("killed", false) else (UiKit.HEX_LOG_ROUND if v == 0 else UiKit.HEX_LOG_DMG_BODY) + return prefix + "[color=%s]%s[/color]" % [body_col, body] + + +## 未生效行域:⚠ icon + 划线形状冗余(非色相独载)+ 聚合计数 +func _fmt_ineffective(row: Dictionary, prefix: String, suffix: String, agg_count: int) -> String: + var name_: String = String(row.get("display_name", "")) + var n := (" ×%d" % agg_count) if agg_count > 1 else "" + return prefix + "[s][color=#%s]⚠ %s%s[/color][/s]%s" % [UiKit.HEX_LOG_VOIDED, name_, n, suffix] + + +## 封印落空行域:完美破/首失豁免两注记分档(金框=破完美/普通墨=豁免) +func _fmt_seal_miss(row: Dictionary, prefix: String) -> String: + var hl := String(row.get("highlight", "")) + var name_: String = String(row.get("display_name", "")) + var miss_note := "" + if hl == "perfect_broken": + miss_note = "(本图完美封印已破——清图奖励不再加倍)" + elif hl == "": + miss_note = "(首次封印落空已豁免——不计入完美封印判定)" + var body := "✗ %s%s" % [name_, miss_note] + return prefix + (_gold_frame(body) if hl == "perfect_broken" else "[color=#%s]%s[/color]" % [UiKit.HEX_LOG_ROUND, body]) + + +## 裁定行域:胜金/负绿双墨(粗体菱形头) +func _fmt_verdict(row: Dictionary) -> String: + var name_: String = String(row.get("display_name", "")) + if int(row.value) == 1: + return "[color=#%s][b]◆ %s[/b][/color]" % [UiKit.HEX_LOG_WIN, name_] + return "[color=#%s][b]◆ %s[/b][/color]" % [UiKit.HEX_LOG_GAIN, name_] + + func _strip_bb(bb: String) -> String: var out := "" var in_tag := false diff --git a/game/presentation/views/battle_stage_view.gd b/game/presentation/views/battle_stage_view.gd index 78ab8b5e..9ab85302 100644 --- a/game/presentation/views/battle_stage_view.gd +++ b/game/presentation/views/battle_stage_view.gd @@ -26,18 +26,28 @@ var world: Node2D # StageWorld(preload 脚本实例——Node2D 世 func _init(db_ref) -> void: custom_minimum_size = Vector2(0, 252) - # 面板底(圣经 v3.5 §6.1 舞台正面目标:B 区=地窟看台,世界底层暗背景 - # =方向稿 render_battle_screen 取值(值单点收 UiKit.STAGE_BG);描边=PANEL_BORDER 同方向稿) + _build_backdrop() + _build_world_stage() + _build_text_half(db_ref) + _build_companion_layer(db_ref) + + +## 背景域(圣经 v3.5 §6.1 舞台正面目标:B 区=地窟看台,世界底层暗背景 +## =方向稿 render_battle_screen 取值(值单点收 UiKit.STAGE_BG);描边=PANEL_BORDER 同方向稿) +func _build_backdrop() -> void: var bg := Panel.new() var sb := UiKit.panel_flat(UiKit.STAGE_BG, UiKit.PANEL_BORDER, 0, 1) bg.add_theme_stylebox_override("panel", sb) bg.set_anchors_preset(Control.PRESET_FULL_RECT) bg.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(bg) + _build_beams_layer() - # 顶光柱氛围层(§6.1 光源氛围——方向稿四柱:x 基准 110/900/1055/1210@1280,暖光(值单点收 - # UiKit.BEAM_LIGHT——T4 裸色收口)自顶向下 60 段渐宽窄渐隐〔顶 α26→0〕;绘制在暗底之上/ - # 世界站桩之下——层序=bg < 光柱 < 世界) + +## 顶光柱氛围层域(§6.1 光源氛围——方向稿四柱:x 基准 110/900/1055/1210@1280,暖光(值单点收 +## UiKit.BEAM_LIGHT——T4 裸色收口)自顶向下 60 段渐宽窄渐隐〔顶 α26→0〕;绘制在暗底之上/ +## 世界站桩之下——层序=bg < 光柱 < 世界) +func _build_beams_layer() -> void: var beams := Control.new() beams.set_anchors_preset(Control.PRESET_FULL_RECT) beams.mouse_filter = Control.MOUSE_FILTER_IGNORE @@ -45,7 +55,9 @@ func _init(db_ref) -> void: beams.resized.connect(beams.queue_redraw) add_child(beams) - # ── Node2D 舞台半区(战斗舞台迁移批 Phase B 同构;stretch 1:1 无缩放系数——C2/C5)── + +## Node2D 舞台半区域(战斗舞台迁移批 Phase B 同构;stretch 1:1 无缩放系数——C2/C5) +func _build_world_stage() -> void: _svp_container = SubViewportContainer.new() _svp_container.stretch = true # 容器尺寸 = 视口逻辑分辨率,恒 1:1 _svp_container.mouse_filter = Control.MOUSE_FILTER_IGNORE # 世界零输入(tooltip 归壳层伴随件) @@ -61,7 +73,9 @@ func _init(db_ref) -> void: world = _STAGE_WORLD.new() _viewport.add_child(world) - # ── Control 文本半区(round_label / stage_center / log_view 原样保留——C1 永久 Control)── + +## Control 文本半区域(round_label / stage_center / log_view 原样保留——C1 永久 Control) +func _build_text_half(db_ref) -> void: var vb := VBoxContainer.new() vb.add_theme_constant_override("separation", 4) vb.set_anchors_preset(Control.PRESET_FULL_RECT) @@ -70,8 +84,14 @@ func _init(db_ref) -> void: vb.offset_right = -_CONTENT_MARGINS.size.x vb.offset_bottom = -_CONTENT_MARGINS.size.y add_child(vb) + _build_narr_row(vb) + _build_center_row(vb) + log_view = BattleLogView.new(db_ref) + vb.add_child(log_view) - # M8(UI 落码批):轮次牌 plate9 素牌化(demo ⑦——墨字上亮牌 12.3:1,暗底暖字退役) + +## 轮次牌域(M8 UI 落码批:plate9 素牌化——demo ⑦,墨字上亮牌 12.3:1,暗底暖字退役) +func _build_narr_row(vb: VBoxContainer) -> void: var narr_plate := PanelContainer.new() narr_plate.add_theme_stylebox_override("panel", UiKit.panel_skinned(Vector4(12, 4, 12, 4))) narr_plate.size_flags_horizontal = Control.SIZE_SHRINK_CENTER @@ -80,12 +100,14 @@ func _init(db_ref) -> void: round_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER narr_plate.add_child(round_label) + +## 中段行域:提示语居中+敌站桩壳层伴随件容器(明细带展开/收起改行高——世界布局随动) +func _build_center_row(vb: VBoxContainer) -> void: _row = HBoxContainer.new() _row.add_theme_constant_override("separation", 10) _row.size_flags_vertical = Control.SIZE_EXPAND_FILL vb.add_child(_row) - _row.resized.connect(_on_row_resized) # 明细带展开/收起改行高——世界布局随动 - + _row.resized.connect(_on_row_resized) var center := CenterContainer.new() center.size_flags_horizontal = Control.SIZE_EXPAND_FILL center.mouse_filter = Control.MOUSE_FILTER_IGNORE # 行内只剩文本——鼠标全放行给壳层伴随件 @@ -96,21 +118,17 @@ func _init(db_ref) -> void: stage_center.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER center_col.add_child(stage_center) _row.add_child(center) - enemy_box = Control.new() # 站桩壳层伴随件容器(T3 语义改写) enemy_box.mouse_filter = Control.MOUSE_FILTER_IGNORE _row.add_child(enemy_box) - log_view = BattleLogView.new(db_ref) - vb.add_child(log_view) - # ── 壳层伴随件层(最顶——世界站桩之上的 Control 触达面)── +## 壳层伴随件层域(最顶——世界站桩之上的 Control 触达面)+主角站桩注册(世界)+ 状态行伴随件(壳层) +func _build_companion_layer(db_ref) -> void: _companion_layer = Control.new() _companion_layer.set_anchors_preset(Control.PRESET_FULL_RECT) _companion_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_companion_layer) - - # 主角站桩本体(世界)+ 状态行伴随件(壳层) hero_standee = _STANDEE_BODY.new("hero") var hero_tex := UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT) hero_standee.set_portrait_texture(hero_tex, true) # B 区立绘 48px 档·normal 态朝右(素材缺位 null 回退字形「人」) @@ -230,26 +248,24 @@ func enemy_body_at(i: int) -> Node2D: # 素材装载属组合点职责(主角侧同款先例 _init TEX_HERO_PORTRAIT),StandeeBody 保持纯收 tex 面。 # 素材缺位 load_tex→null→set_portrait_texture 字形回退(表现与素材批前逐位一致);未录键返回 null # 走默认字形「敌」(与 StandeeBody._portrait_glyph 默认档同口径)——数据表扩键先补此处映射。 +## 敌像 id→纹理键映射表(查表+回退——映射语义零变化;未知 id=null 回退字形) func _enemy_portrait_tex(enemy_id: String) -> Texture2D: - match enemy_id: - "goblin": - return UiKit.load_tex(UiKit.TEX_ENEMY_GOBLIN) - "bugbear": - return UiKit.load_tex(UiKit.TEX_ENEMY_BUGBEAR) - "ogre_chief": - return UiKit.load_tex(UiKit.TEX_ENEMY_OGRE_CHIEF) - "giant_spider": - return UiKit.load_tex(UiKit.TEX_ENEMY_GIANT_SPIDER) - "drider": - return UiKit.load_tex(UiKit.TEX_ENEMY_DRIDER) - "duergar": - return UiKit.load_tex(UiKit.TEX_ENEMY_DUERGAR) - "hook_horror": - return UiKit.load_tex(UiKit.TEX_ENEMY_HOOK_HORROR) - "duergar_king": - return UiKit.load_tex(UiKit.TEX_ENEMY_DUERGAR_KING) - _: - return null + var table := {"goblin": UiKit.TEX_ENEMY_GOBLIN, "bugbear": UiKit.TEX_ENEMY_BUGBEAR, + "ogre_chief": UiKit.TEX_ENEMY_OGRE_CHIEF, "giant_spider": UiKit.TEX_ENEMY_GIANT_SPIDER, + "drider": UiKit.TEX_ENEMY_DRIDER, "duergar": UiKit.TEX_ENEMY_DUERGAR, + "hook_horror": UiKit.TEX_ENEMY_HOOK_HORROR, "duergar_king": UiKit.TEX_ENEMY_DUERGAR_KING} + return UiKit.load_tex(String(table.get(enemy_id, ""))) if table.has(enemy_id) else null + + +## 单敌站桩构建域(循环体提函数——世界本体+壳层伴随件+敌种立绘接线〔仅重建拍装载一次; +## flip=false——八敌像素源核证朝左/正面,呈现即朝主角无误;反向朝向源换代时翻转义务归素材批〕) +func _spawn_enemy_standee(d: Dictionary) -> Node2D: + var body: Node2D = _STANDEE_BODY.new("enemy") + world.add_child(body) + body.slot_row = _make_slot_companion(enemy_box) + body.set_data(d) + body.set_portrait_texture(_enemy_portrait_tex(String(d.get("enemy_id", ""))), false) + return body # 敌方站桩投影更新:数量不变 = 就地更新(死亡灰化/受击闪烁等站桩上动画不被逐行刷新重置——禁瞬消纪律); @@ -263,14 +279,7 @@ func set_enemies(cards_data: Array) -> void: c.queue_free() _cards.clear() for d in cards_data: - var body: Node2D = _STANDEE_BODY.new("enemy") - world.add_child(body) - body.slot_row = _make_slot_companion(enemy_box) - body.set_data(d) - # 敌种立绘接线(仅重建拍装载一次;flip=false——八敌像素源核证朝左/正面, - # 呈现即朝主角无误;反向朝向源换代时翻转义务归素材批) - body.set_portrait_texture(_enemy_portrait_tex(String(d.get("enemy_id", ""))), false) - _cards.append(body) + _cards.append(_spawn_enemy_standee(d)) _sync_layout.call_deferred() return for i in cards_data.size(): diff --git a/game/presentation/views/board_cell_unit.gd b/game/presentation/views/board_cell_unit.gd index 7feda330..87a45e23 100644 --- a/game/presentation/views/board_cell_unit.gd +++ b/game/presentation/views/board_cell_unit.gd @@ -1,68 +1,13 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION ## CellUnit——BoardView SubViewport 世界内逐格渲染件(Node2D),全部程序化 _draw 占位视觉(纹理槽位已留未装载)。 ## 无 class_name(类名缓存面纪律)——使用方经 preload 引用。 +## 内部状态件(u3 子域拆件——契约 9 职责语句「单格绘制输入集」):本件为被动绘制壳, +## 状态件 _CellPaintState 承载全部绘制入参(单一变化原因=格面呈现),setter 触发宿主重绘语义 +## 经 _owner 回引保持逐字;探针直读冻结成员(glyph/fog_state/plate/glyph_color)与 _glow_layer 住宿主。 extends Node2D const _GLOW_LAYER := preload("res://presentation/views/cell_type_glow_layer.gd") -var cell := Vector2i.ZERO # 板坐标(诊断观测面) -var edge := 46.0: # 格边长(BoardView.set_cell_area 单点驱动) - set(v): - edge = v - if _glow_layer != null and is_instance_valid(_glow_layer): - _glow_layer.setup(edge, glow_group) - queue_redraw() -var bg_sb: StyleBoxFlat = null # 格底 StyleBox(BoardView 每次 paint 重建;呼吸微光就地改边框) -# ── 接触带微光绘制面(可挖域微光画在英雄格周界空间侧——art-bible §3.2 v2.8; -# 仅英雄格单元承载,BoardView.refresh 按可挖域写边/角码、_process 逐拍更新呼吸色)── -var contact_bands: Array[int] = [] # 正交接触边码 0=N/1=S/2=W/3=E(该侧整条边) -var contact_corners: Array[int] = [] # 斜向共享角码 0=NW/1=NE/2=SW/3=SE(角点+两侧各半条边) -var contact_col := Color(0, 0, 0, 0) # 当前呼吸色(alpha 0 = 不画) -var plate := false # 数字色牌开关(BoardView paint 按已开格 glyph 写入——原则二) -var outline_sides: Array[int] = [] # 墙格地面朝向描边码 0=N/1=S/2=W/3=E(程序侧结构件) -# ── 高墙感 D 投影档(研究件四变体定 D=C+地面投影,ITB 同款高度线索)── -# 南立面 = 墙格南缘内侧「口沿高光线 + 双段暗带」(邻地面格才画,墙群内聚);地面投影 = 北邻墙格 -# 在本已开格北缘投渐隐暗带。参数按研究件 64px 拼版实测比例换算(立面双段 0.16/0.25 格边、投影带 -# 0.22 格边;坐标推导留档 commit)。素材缺位程序化回退同样服役(结构归程序——art-bible §8.5)。 -var facade_south := false: # 南立面开关(BoardView paint 按「南邻已开格」写入) - set(v): - facade_south = v - queue_redraw() -var proj_band := 0.0: # 地面投影带宽(像素,0 = 不画——BoardView 按北邻墙判定写入) - set(v): - proj_band = v - queue_redraw() -var glyph := "": # 文字位——数字/雷/封印/人/?(现行视觉封印与数字互斥,同位承载) - set(v): - glyph = v - queue_redraw() -var glyph_color := UiKit.INK: # 文字色(墨族/雷朱/封印黑/小人蓝) - set(v): - glyph_color = v - queue_redraw() -var badge_glyph := "": # 奖励徽章文字(右下角金——封印之下、变异纹之上、不进数字区) - set(v): - badge_glyph = v - if _glow_layer != null and is_instance_valid(_glow_layer): - _glow_layer.set_badge_mask(v != "") - queue_redraw() -var dot_visible := false: # BFS 路径预览点迹(中心 6×6 小人蓝) - set(v): - dot_visible = v - queue_redraw() -var hl_visible := false: # 焦点高亮(悬停态边框覆盖层) - set(v): - hl_visible = v - queue_redraw() -var hl_color := UiKit.GOLD: # 高亮色(金目标/封印朱/禁入灰禁红) - set(v): - hl_color = v - queue_redraw() -var font: Font = null # 字体槽位(null = ThemeDB.fallback_font——BoardView.set_world_font 注入) -var bevel := 0.0: # 顶面倒角强度(0=关;档位 0/0.35/0.6/1.0) - set(v): - bevel = v - queue_redraw() # ── D 投影档绘制常量(研究件实测取色——立面高光/双段暗带/地面投影;黑罩 alpha 叠加实测等效)── const FACADE_HIGHLIGHT_PX := 2.0 # 口沿高光线厚(像素) const FACADE_NEAR_FRAC := 0.16 # 立面近段高(格边比——研究件 10/64) @@ -76,235 +21,355 @@ const PROJECTION_EDGE_COL := Color(0, 0, 0, 0.06) # 投影远段(渐隐收 # 直贴零暗档零阴影;墙脚阴线只画地面/墙面交界;(b)-(c) 交界 # 无边框由 BoardView 侧跳过墙格北向描边承载)── const E_FACADE_FRAC := 0.30 # 墙面承接高(格边比 30%) -# ── 素材槽位(现全 null = 程序化占位服役;art-bible §8.2 格面 32×32 暂定档)── -var tex_base: Texture2D = null # ① 砖面底 -var tex_mutation: Texture2D = null # ② 变异纹带 -var tex_glyph: Texture2D = null # ③ 数字 -var tex_badge: Texture2D = null # ③' 徽章 -var tex_seal: Texture2D = null: # ④ 封印(槽位可运行时换装〔封印〕——同族 facade/treasure_mark - set(v): # 均 setter 触发重绘(普通 var 赋值不重绘=封印插上不显示)) - if tex_seal != v: - tex_seal = v - queue_redraw() -var tex_facade: Texture2D = null # ⑥ E 格墙面材质(v3.5——南邻墙顶 center 瓦片同源,BoardView 注入) -var facade_strip := false: # E 格开关(BoardView paint 按「已开格且南邻未开墙」写入——瓦片态专用) + + +## 单格绘制输入集(被动绘制壳的全部绘制入参——BoardView 直写面;setter 触发宿主重绘语义逐字) +class _CellPaintState extends RefCounted: + var _owner: Node2D = null + var cell := Vector2i.ZERO # 板坐标(诊断观测面) + var edge := 46.0: # 格边长(BoardView.set_cell_area 单点驱动) + set(v): + edge = v + if _owner._glow_layer != null and is_instance_valid(_owner._glow_layer): + _owner._glow_layer.setup(edge, glow_group) + _owner.queue_redraw() + var bg_sb: StyleBoxFlat = null # 格底 StyleBox(BoardView 每次 paint 重建;呼吸微光就地改边框) + # ── 接触带微光绘制面(可挖域微光画在英雄格周界空间侧——art-bible §3.2 v2.8; + # 仅英雄格单元承载,BoardView.refresh 按可挖域写边/角码、_process 逐拍更新呼吸色)── + var contact_bands: Array[int] = [] # 正交接触边码 0=N/1=S/2=W/3=E(该侧整条边) + var contact_corners: Array[int] = [] # 斜向共享角码 0=NW/1=NE/2=SW/3=SE(角点+两侧各半条边) + var contact_col := Color(0, 0, 0, 0) # 当前呼吸色(alpha 0 = 不画) + var outline_sides: Array[int] = [] # 墙格地面朝向描边码 0=N/1=S/2=W/3=E(程序侧结构件) + # ── 高墙感 D 投影档(研究件四变体定 D=C+地面投影,ITB 同款高度线索)── + # 南立面 = 墙格南缘内侧「口沿高光线 + 双段暗带」(邻地面格才画,墙群内聚);地面投影 = 北邻墙格 + # 在本已开格北缘投渐隐暗带。参数按研究件 64px 拼版实测比例换算(立面双段 0.16/0.25 格边、投影带 + # 0.22 格边;坐标推导留档 commit)。素材缺位程序化回退同样服役(结构归程序——art-bible §8.5)。 + var facade_south := false: # 南立面开关(BoardView paint 按「南邻已开格」写入) + set(v): + facade_south = v + _owner.queue_redraw() + var proj_band := 0.0: # 地面投影带宽(像素,0 = 不画——BoardView 按北邻墙判定写入) + set(v): + proj_band = v + _owner.queue_redraw() + var badge_glyph := "": # 奖励徽章文字(右下角金——封印之下、变异纹之上、不进数字区) + set(v): + badge_glyph = v + if _owner._glow_layer != null and is_instance_valid(_owner._glow_layer): + _owner._glow_layer.set_badge_mask(v != "") + _owner.queue_redraw() + var dot_visible := false: # BFS 路径预览点迹(中心 6×6 小人蓝) + set(v): + dot_visible = v + _owner.queue_redraw() + var hl_visible := false: # 焦点高亮(悬停态边框覆盖层) + set(v): + hl_visible = v + _owner.queue_redraw() + var hl_color := UiKit.GOLD: # 高亮色(金目标/封印朱/禁入灰禁红) + set(v): + hl_color = v + _owner.queue_redraw() + var font: Font = null # 字体槽位(null = ThemeDB.fallback_font——BoardView.set_world_font 注入) + var bevel := 0.0: # 顶面倒角强度(0=关;档位 0/0.35/0.6/1.0) + set(v): + bevel = v + _owner.queue_redraw() + # ── 素材槽位(现全 null = 程序化占位服役;art-bible §8.2 格面 32×32 暂定档)── + var tex_base: Texture2D = null # ① 砖面底 + var tex_mutation: Texture2D = null # ② 变异纹带 + var tex_glyph: Texture2D = null # ③ 数字 + var tex_badge: Texture2D = null # ③' 徽章 + var tex_seal: Texture2D = null: # ④ 封印(槽位可运行时换装〔封印〕——同族 facade/treasure_mark + set(v): # 均 setter 触发重绘(普通 var 赋值不重绘=封印插上不显示)) + if tex_seal != v: + tex_seal = v + _owner.queue_redraw() + var tex_facade: Texture2D = null # ⑥ E 格墙面材质(v3.5——南邻墙顶 center 瓦片同源,BoardView 注入) + var facade_strip := false: # E 格开关(BoardView paint 按「已开格且南邻未开墙」写入——瓦片态专用) + set(v): + facade_strip = v + _owner.queue_redraw() + + # 变异角标(程序化,tex_mutation 纹带槽暂留): + # "flat_damage" = 未开格伤害型变异,左下角危险红三角(开格有代价的策略纹理标记) + # 叠加层标记(秘宝整图叠加层的格面呈现;派生信息层零回写): + # "mine_reveal" = 真视格(危险红内缩框——「这格是雷」的确定决策标记,封印照常计费); + # "reward_preview" 格不复用本字段——预读徽章走 badge_glyph 注入(board_view refresh 注入 REWARD_BADGE 字形) + var treasure_mark := "": + set(v): + if treasure_mark != v: + treasure_mark = v + _owner.queue_redraw() + var mutation_mark := "": + set(v): + if mutation_mark != v: + mutation_mark = v + _owner.queue_redraw() + + # ── 未开格类型微光(英雄 8 向相邻未开格的组粒度类型提示) + # 作用集 = 可挖域 ∩ 身份可查格,由 BoardView 侧派生下发;本件只承接绘制与子件生命周期。 + # 静态:不呼吸、不入动效预算;相位门控(结算/道具窗口态隐藏)在 BoardView 侧。 + var glow_group := 0: + set(v): + if glow_group != v: + glow_group = v + _owner._sync_glow_layer() + + var removed_dim := false: # 中立移除退色层(BoardView paint 按排雷玩法 is_removed 写入) + set(v): + removed_dim = v + _owner.queue_redraw() + + +var st := _CellPaintState.new() +var glyph := "": # 文字位——数字/雷/封印/人/?(现行视觉封印与数字互斥,同位承载;探针直读冻结) set(v): - facade_strip = v + glyph = v queue_redraw() - -# 变异角标(程序化,tex_mutation 纹带槽暂留): -# "flat_damage" = 未开格伤害型变异,左下角危险红三角(开格有代价的策略纹理标记) -# 叠加层标记(秘宝整图叠加层的格面呈现;派生信息层零回写): -# "mine_reveal" = 真视格(危险红内缩框——「这格是雷」的确定决策标记,封印照常计费); -# "reward_preview" 格不复用本字段——预读徽章走 badge_glyph 注入(board_view refresh 注入 REWARD_BADGE 字形) -var treasure_mark := "": +var glyph_color := UiKit.INK: # 文字色(墨族/雷朱/封印黑/小人蓝;探针直读冻结) set(v): - if treasure_mark != v: - treasure_mark = v - queue_redraw() -var mutation_mark := "": - set(v): - if mutation_mark != v: - mutation_mark = v - queue_redraw() - + glyph_color = v + queue_redraw() +var plate := false # 数字色牌开关(BoardView paint 按已开格 glyph 写入——原则二;探针直读冻结) # ── 视野迷雾呈现态(纯呈现零逻辑;规格=art-bible §6.5 v3.5,现行事实源=HUD框架「视野迷雾」节)── # 0=正常(灯光区内)/ 1=全黑(区外未开格——现行事实源「区外未开格=全黑不渲染」不透明档)/ 2=记忆态(区外已开格灰显可读) -var fog_state := 0: +var fog_state := 0: # 探针直读冻结 set(v): if fog_state != v: fog_state = v queue_redraw() -var removed_dim := false: # 中立移除退色层(BoardView paint 按排雷玩法 is_removed 写入) - set(v): - removed_dim = v - queue_redraw() -# ── 未开格类型微光(英雄 8 向相邻未开格的组粒度类型提示) -# 作用集 = 可挖域 ∩ 身份可查格,由 BoardView 侧派生下发;本件只承接绘制与子件生命周期。 -# 静态:不呼吸、不入动效预算;相位门控(结算/道具窗口态隐藏)在 BoardView 侧。 -var glow_group := 0: - set(v): - if glow_group != v: - glow_group = v - _sync_glow_layer() - var _glow_layer: Node2D = null +func _init() -> void: + st._owner = self + + func _draw() -> void: - var rect := Rect2(Vector2.ZERO, Vector2(edge, edge)) - # ① 砖面底(占位 = StyleBoxFlat:格底色 + 暗化描边 + 3px 圆角; - # 呼吸微光/主角蓝框由 BoardView 改 bg_sb 后触发重绘) - if tex_base != null: - draw_texture_rect(tex_base, rect, false) + var rect := Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)) + _draw_base_layer(rect) + _draw_mutation_marks() + _draw_glyph_layer(rect) + _draw_overlay_layers(rect) + + +## ① 砖面底层域(占位 = StyleBoxFlat:格底色 + 暗化描边 + 3px 圆角;瓦片态描边覆盖层; +## 呼吸微光/主角蓝框由 BoardView 改 bg_sb 后触发重绘。素材缺位回退走 elif 程序化原样,零观感变化) +func _draw_base_layer(rect: Rect2) -> void: + if st.tex_base != null: + draw_texture_rect(st.tex_base, rect, false) # 轮廓加强(轮廓线不明显):墙格地面朝向边补 2px 深墨线(§8.5 结构归程序—— # 素材 1px 墨线在 45/36px 非整数缩放下视觉衰减,程序线保底;墙-墙内界不画,保墙群内聚; # 坐标推导:各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合) - if not outline_sides.is_empty(): - var ow := 2.0 - var oc := UiKit.FOG_OUTLINE - for b in outline_sides: - var or_ := Rect2() - match int(b): - 0: or_ = Rect2(0, 0, edge, ow) - 1: or_ = Rect2(0, edge - ow, edge, ow) - 2: or_ = Rect2(0, 0, ow, edge) - 3: or_ = Rect2(edge - ow, 0, ow, edge) - draw_rect(or_, oc, true) + if not st.outline_sides.is_empty(): + _draw_outline_sides() _draw_south_facade() _draw_north_projection() _draw_e_facade() # 瓦片态描边覆盖层:tex_base 在位时 BoardView 传入的 bg_sb # 已降级为「透明底 + 描边」载体——呼吸微光/主角蓝框画在瓦片之上(art-bible §1.2 叠层序 # 不变:底 < 纹带 < 数字 < 封印 < 焦点高亮);素材缺位回退走 elif 程序化原样,零观感变化 - if bg_sb != null: - draw_style_box(bg_sb, rect) + if st.bg_sb != null: + draw_style_box(st.bg_sb, rect) + _draw_contact_or_fallback(rect) - # 接触带微光(英雄格周界空间侧——正交邻墙=整条边;斜向=共享角点+两侧各半条边;光不进墙格): - # 带宽 4px 贴格边内侧(坐标推导:N=[0,0,edge,4] S=[0,edge-4,edge,4] W=[0,0,4,edge] E=[edge-4,0,4,edge]; - # 半条边=沿邻边自角点起 edge/2;角点圆 r=6 贴角点圆心,近白芯 r=2.5 提亮) - if contact_col.a > 0.0: + +## 接触带微光或平色回退域(英雄格周界空间侧——正交邻墙=整条边;斜向=共享角点+两侧各半条边;光不进墙格): +## 带宽 4px 贴格边内侧(坐标推导:N=[0,0,edge,4] S=[0,edge-4,edge,4] W=[0,0,4,edge] E=[edge-4,0,4,edge]; +## 半条边=沿邻边自角点起 edge/2;角点圆 r=6 贴角点圆心,近白芯 r=2.5 提亮) +func _draw_contact_or_fallback(rect: Rect2) -> void: + if st.contact_col.a > 0.0: _draw_north_projection() # 平色回退+微光并发的英雄格补画(瓦片路径已画——同像素重复无观感差) _draw_e_facade() # E 格墙面补画(同上——英雄格兼 E 格时瓦片路径已画,重复无害) - var band := 4.0 - var half := edge * 0.5 - for b in contact_bands: - var r := Rect2() - match int(b): - 0: r = Rect2(0, 0, edge, band) - 1: r = Rect2(0, edge - band, edge, band) - 2: r = Rect2(0, 0, band, edge) - 3: r = Rect2(edge - band, 0, band, edge) - draw_rect(r, contact_col, true) - for cc in contact_corners: - var pt := Vector2.ZERO - match int(cc): - 0: pt = Vector2(0, 0) - 1: pt = Vector2(edge, 0) - 2: pt = Vector2(0, edge) - 3: pt = Vector2(edge, edge) - draw_circle(pt, 6.0, contact_col) - draw_circle(pt, 2.5, Color(1, 1, 1, minf(1.0, contact_col.a * 0.95))) - # 半条边:角点沿两条邻边各延伸 edge/2(坐标推导:横带沿 N/S 自角点向板心侧 half; - # NW/SE 半带 x=y=角点坐标,NE/SE 半带自 edge-half 收进——正交整边时重叠同色无害) - if int(cc) == 0 or int(cc) == 1: - # N 侧横带:NW 自 x=0、NE 自 x=edge-half - draw_rect(Rect2(pt.x if int(cc) == 0 else pt.x - half, 0.0, half, band), contact_col, true) - if int(cc) == 2 or int(cc) == 3: - # S 侧横带:SW 自 x=0、SE 自 x=edge-half - draw_rect(Rect2(pt.x if int(cc) == 2 else pt.x - half, edge - band, half, band), contact_col, true) - if int(cc) == 0 or int(cc) == 2: - # W 侧竖带:NW 自 y=0、SW 自 y=edge-half - draw_rect(Rect2(0.0, pt.y if int(cc) == 0 else pt.y - half, band, half), contact_col, true) - if int(cc) == 1 or int(cc) == 3: - # E 侧竖带:NE 自 y=0、SE 自 y=edge-half - draw_rect(Rect2(edge - band, pt.y if int(cc) == 1 else pt.y - half, band, half), contact_col, true) - elif bg_sb != null: - draw_style_box(bg_sb, rect) + _draw_contact_bands() + elif st.bg_sb != null: + draw_style_box(st.bg_sb, rect) _draw_south_facade() # 平色回退路径——D 投影档两件同样服役(结构归程序) _draw_north_projection() - # 顶面倒角(光源左上——上/左内沿受光 2px、下/右内沿背光 2px;内缩 2px 让 - # 呼吸微光/主角蓝边框保持最外沿可见;强度随档位) - if bevel > 0.0: - var base: Color = bg_sb.bg_color - var lit: Color = base.lightened(0.22 * bevel) - var dark: Color = base.darkened(0.22 * bevel) - var t := 2.0 - draw_rect(Rect2(t, t, edge - t * 2.0, t), lit, true) - draw_rect(Rect2(t, t, t, edge - t * 2.0), lit, true) - draw_rect(Rect2(t, edge - t * 2.0, edge - t * 2.0, t), dark, true) - draw_rect(Rect2(edge - t * 2.0, t, t, edge - t * 2.0), dark, true) + _draw_bevel() + + +## ②③④ 中层域:变异纹带与标记/数字色牌与字形/徽章/封印 +func _draw_glyph_layer(rect: Rect2) -> void: # ② 变异纹带(无程序化纹带——槽位先行,贴图落位即用) - if tex_mutation != null: - draw_texture_rect(tex_mutation, rect, false) - # ②' 变异角标:左下角危险红三角(危险红=「开此格有代价」语义位,与雷族同源; - # 尺寸随格边比例——灰色可辨辅助=红对砖面明度差,禁喧宾) - # ②" 秘宝叠加层:真视格=危险红内缩框(策略纹理——确定决策输入不喧宾) - if treasure_mark == "mine_reveal": - var inset := edge * 0.14 - draw_rect(Rect2(inset, inset, edge - inset * 2.0, edge - inset * 2.0), - Color(UiKit.RED_MINE, 0.9), false, 2.0) - if mutation_mark == "flat_damage": - var mw := edge * 0.28 - var mpts := PackedVector2Array([Vector2(0, edge - mw), Vector2(0, edge), Vector2(mw, edge)]) - draw_colored_polygon(mpts, Color(UiKit.RED_MINE, 0.85)) + if st.tex_mutation != null: + draw_texture_rect(st.tex_mutation, rect, false) # ③ 数字/徽章(数字 ≥ 格高 50% 墨族对格底 ≥4.5:1;徽章右下角金,遮蔽态 = "?") if glyph != "": - # 数字色牌后备路径(圣经 v3.5 冻结④:数字亮墨直画已开格底须垫牌; - # 牌=后备件——雷/「?」维持牌载保读性(牌上深墨族 12:1); - # 牌尺寸 0.60/0.62 格边居中;封印=未开格不垫牌) - if plate: - var pw := edge * 0.60 - var ph := edge * 0.62 - var pr := Rect2((edge - pw) * 0.5, (edge - ph) * 0.5, pw, ph) - draw_rect(pr, UiKit.DIGIT_PLATE, true) - draw_rect(pr, UiKit.DIGIT_PLATE_EDGE, false, 1.0) - if tex_glyph != null: - draw_texture_rect(tex_glyph, rect, false) + _draw_digit_plate() + if st.tex_glyph != null: + draw_texture_rect(st.tex_glyph, rect, false) else: - _draw_glyph(glyph, glyph_color, maxi(18, int(ceil(edge * 0.5)))) # 字号下限 18(§7.4 冻结公式) - if badge_glyph != "": - if tex_badge != null: - draw_texture_rect(tex_badge, rect, false) + _draw_glyph(glyph, glyph_color, maxi(18, int(ceil(st.edge * 0.5)))) # 字号下限 18(§7.4 冻结公式) + if st.badge_glyph != "": + if st.tex_badge != null: + draw_texture_rect(st.tex_badge, rect, false) else: - _draw_badge(badge_glyph) + _draw_badge(st.badge_glyph) # ④ 封印(现行视觉封印文字在 ③ 文字位承载——独立槽位留给封印图) - if tex_seal != null: - draw_texture_rect(tex_seal, rect, false) + if st.tex_seal != null: + draw_texture_rect(st.tex_seal, rect, false) + + +## 数字色牌后备域(圣经 v3.5 冻结④:数字亮墨直画已开格底须垫牌; +## 牌=后备件——雷/「?」维持牌载保读性(牌上深墨族 12:1); +## 牌尺寸 0.60/0.62 格边居中;封印=未开格不垫牌) +func _draw_digit_plate() -> void: + var pw := st.edge * 0.60 + var ph := st.edge * 0.62 + var pr := Rect2((st.edge - pw) * 0.5, (st.edge - ph) * 0.5, pw, ph) + draw_rect(pr, UiKit.DIGIT_PLATE, true) + draw_rect(pr, UiKit.DIGIT_PLATE_EDGE, false, 1.0) + + +## ⑤⑥⑦ 顶层域:路径点迹 + 焦点高亮 + 视野迷雾 + 中立移除退色 +func _draw_overlay_layers(rect: Rect2) -> void: # ⑤ 路径点迹 + 焦点高亮(悬停态覆盖层,不参与命中) - if dot_visible: + if st.dot_visible: var ds := 6.0 - draw_rect(Rect2(Vector2((edge - ds) * 0.5, (edge - ds) * 0.5), Vector2(ds, ds)), UiKit.HERO_BLUE, true) - if hl_visible: + draw_rect(Rect2(Vector2((st.edge - ds) * 0.5, (st.edge - ds) * 0.5), Vector2(ds, ds)), UiKit.HERO_BLUE, true) + if st.hl_visible: # 悬停高亮减淡(金框太明显——绘制时 55% alpha,hl_color 属性与探针断言面不动) - draw_rect(Rect2(1, 1, edge - 2.0, edge - 2.0), Color(hl_color, 0.55), false, 2.0) # 2px 内描边 - # ⑥ 视野迷雾层(最顶遮罩,§6.5 落文): - # 全黑=不透明纯黑盖全部叠层(含封印印/徽章/高亮——封印印记随格隐没,§6.5 显式推论); - # 记忆态=记忆板素色压平(纹理免疫——色牌/封印印/徽章随板隐没)+数字记忆墨重画(数字可读硬指标 ≥4.5:1) - if fog_state == 1: - draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), Color(0, 0, 0, 1.0), true) - elif fog_state == 2: - # 记忆态换装(demo 口径「地板纹理 + 灰罩 α.66」: - # 地板纹理原样直画 + FOG_MEMORY_PLATE α.66 灰罩压顶;素材零改动、格边长零改动。 - # 记忆墨 FOG_MEMORY_INK 对合成底归一后(真实渲染面)三关 5.71–5.93:1 ≥4.5:1(§7.2 硬指标; - # 测点行值与素材随动,见 ui_contrast_probe 迷雾段)) - if tex_base != null: - draw_texture_rect(tex_base, Rect2(Vector2.ZERO, Vector2(edge, edge)), false) - draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), Color(UiKit.FOG_MEMORY_PLATE, 0.66), true) - if glyph != "": - _draw_glyph(glyph, UiKit.FOG_MEMORY_INK, maxi(18, int(ceil(edge * 0.5)))) # 字号下限 18(§7.4 冻结公式同源) + draw_rect(Rect2(1, 1, st.edge - 2.0, st.edge - 2.0), Color(st.hl_color, 0.55), false, 2.0) # 2px 内描边 + _draw_fog_layer() # ⑦ 中立移除退色层(退色降档:暗幕压顶全层覆盖,暗淡记忆态同族呈现语义; # 顶层于迷雾之上——移除格不再参与迷雾语义。本实现移除集=未揭示危险格(无数字无封印印)) - if removed_dim: - draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), Color(0.12, 0.10, 0.09, 0.62), true) + if st.removed_dim: + draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(0.12, 0.10, 0.09, 0.62), true) + + +## ⑥ 视野迷雾层(最顶遮罩,§6.5 落文): +## 全黑=不透明纯黑盖全部叠层(含封印印/徽章/高亮——封印印记随格隐没,§6.5 显式推论); +## 记忆态=记忆板素色压平(纹理免疫——色牌/封印印/徽章随板隐没)+数字记忆墨重画(数字可读硬指标 ≥4.5:1) +func _draw_fog_layer() -> void: + if fog_state == 1: + draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(0, 0, 0, 1.0), true) + elif fog_state == 2: + _draw_fog_memory() + + +## 墙格轮廓域:地面朝向边补 2px 深墨线(各边内贴 2px 矩形,相邻边矩形全长相交=周界闭合) +func _draw_outline_sides() -> void: + var ow := 2.0 + var oc := UiKit.FOG_OUTLINE + for b in st.outline_sides: + var or_ := Rect2() + match int(b): + 0: or_ = Rect2(0, 0, st.edge, ow) + 1: or_ = Rect2(0, st.edge - ow, st.edge, ow) + 2: or_ = Rect2(0, 0, ow, st.edge) + 3: or_ = Rect2(st.edge - ow, 0, ow, st.edge) + draw_rect(or_, oc, true) + + +## 接触带微光域:正交整条边 + 斜向共享角点与两侧半条边 +func _draw_contact_bands() -> void: + var band := 4.0 + var half := st.edge * 0.5 + for b in st.contact_bands: + var r := Rect2() + match int(b): + 0: r = Rect2(0, 0, st.edge, band) + 1: r = Rect2(0, st.edge - band, st.edge, band) + 2: r = Rect2(0, 0, band, st.edge) + 3: r = Rect2(st.edge - band, 0, band, st.edge) + draw_rect(r, st.contact_col, true) + for cc in st.contact_corners: + _draw_contact_corner(int(cc), band, half) + + +## 斜向角点域:角点圆 r=6 贴角点圆心 + 近白芯 r=2.5 提亮 + 半条边(角点沿两条邻边各延伸 edge/2—— +## 坐标推导:横带沿 N/S 自角点向板心侧 half;NW/SE 半带 x=y=角点坐标,NE/SE 半带自 edge-half 收进—— +## 正交整边时重叠同色无害) +func _draw_contact_corner(cc: int, band: float, half: float) -> void: + var pt := Vector2.ZERO + match cc: + 0: pt = Vector2(0, 0) + 1: pt = Vector2(st.edge, 0) + 2: pt = Vector2(0, st.edge) + 3: pt = Vector2(st.edge, st.edge) + draw_circle(pt, 6.0, st.contact_col) + draw_circle(pt, 2.5, Color(1, 1, 1, minf(1.0, st.contact_col.a * 0.95))) + if cc == 0 or cc == 1: + # N 侧横带:NW 自 x=0、NE 自 x=edge-half + draw_rect(Rect2(pt.x if cc == 0 else pt.x - half, 0.0, half, band), st.contact_col, true) + if cc == 2 or cc == 3: + # S 侧横带:SW 自 x=0、SE 自 x=edge-half + draw_rect(Rect2(pt.x if cc == 2 else pt.x - half, st.edge - band, half, band), st.contact_col, true) + if cc == 0 or cc == 2: + # W 侧竖带:NW 自 y=0、SW 自 y=edge-half + draw_rect(Rect2(0.0, pt.y if cc == 0 else pt.y - half, band, half), st.contact_col, true) + if cc == 1 or cc == 3: + # E 侧竖带:NE 自 y=0、SE 自 y=edge-half + draw_rect(Rect2(st.edge - band, pt.y if cc == 1 else pt.y - half, band, half), st.contact_col, true) + + +## 顶面倒角域(光源左上——上/左内沿受光 2px、下/右内沿背光 2px;内缩 2px 让 +## 呼吸微光/主角蓝边框保持最外沿可见;强度随档位) +func _draw_bevel() -> void: + if st.bevel <= 0.0: + return + var base: Color = st.bg_sb.bg_color + var lit: Color = base.lightened(0.22 * st.bevel) + var dark: Color = base.darkened(0.22 * st.bevel) + var t := 2.0 + draw_rect(Rect2(t, t, st.edge - t * 2.0, t), lit, true) + draw_rect(Rect2(t, t, t, st.edge - t * 2.0), lit, true) + draw_rect(Rect2(t, st.edge - t * 2.0, st.edge - t * 2.0, t), dark, true) + draw_rect(Rect2(st.edge - t * 2.0, t, t, st.edge - t * 2.0), dark, true) + + +## 变异标记域:真视格=危险红内缩框/未开格伤害型变异=左下角危险红三角 +func _draw_mutation_marks() -> void: + if treasure_mark == "mine_reveal": + var inset := st.edge * 0.14 + draw_rect(Rect2(inset, inset, st.edge - inset * 2.0, st.edge - inset * 2.0), + Color(UiKit.RED_MINE, 0.9), false, 2.0) + if st.mutation_mark == "flat_damage": + var mw := st.edge * 0.28 + var mpts := PackedVector2Array([Vector2(0, st.edge - mw), Vector2(0, st.edge), Vector2(mw, st.edge)]) + draw_colored_polygon(mpts, Color(UiKit.RED_MINE, 0.85)) + + +## 记忆态换装域(demo 口径「地板纹理 + 灰罩 α.66」: +## 地板纹理原样直画 + FOG_MEMORY_PLATE α.66 灰罩压顶;素材零改动、格边长零改动。 +## 记忆墨 FOG_MEMORY_INK 对合成底归一后(真实渲染面)三关 5.71–5.93:1 ≥4.5:1(§7.2 硬指标; +## 测点行值与素材随动,见 ui_contrast_probe 迷雾段)) +func _draw_fog_memory() -> void: + if st.tex_base != null: + draw_texture_rect(st.tex_base, Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), false) + draw_rect(Rect2(Vector2.ZERO, Vector2(st.edge, st.edge)), Color(UiKit.FOG_MEMORY_PLATE, 0.66), true) + if glyph != "": + _draw_glyph(glyph, UiKit.FOG_MEMORY_INK, maxi(18, int(ceil(st.edge * 0.5)))) # 字号下限 18(§7.4 冻结公式同源) # 南立面(D 档件一——墙格南缘内侧,自底边向上:双段暗带在下、口沿高光线贴其上沿): # 坐标推导(自格底边起算):远段=[edge−far, edge],近段=[edge−far−near, edge−far], # 高光线=[edge−far−near−hl, edge−far−near]——与研究件「墙顶面→高光→暗段→更暗段→格界」下行序一致 func _draw_south_facade() -> void: - if not facade_south: + if not st.facade_south: return - var far_h := edge * FACADE_FAR_FRAC - var near_h := edge * FACADE_NEAR_FRAC - var y_far := edge - far_h + var far_h := st.edge * FACADE_FAR_FRAC + var near_h := st.edge * FACADE_NEAR_FRAC + var y_far := st.edge - far_h var y_near := y_far - near_h var y_hl := y_near - FACADE_HIGHLIGHT_PX - draw_rect(Rect2(0.0, y_hl, edge, FACADE_HIGHLIGHT_PX), UiKit.STAND_STONE_RIM, true) # 口沿高光(UiKit.STAND_STONE_RIM 并档——同值单点) - draw_rect(Rect2(0.0, y_near, edge, near_h), FACADE_NEAR_COL, true) - draw_rect(Rect2(0.0, y_far, edge, far_h), FACADE_FAR_COL, true) + draw_rect(Rect2(0.0, y_hl, st.edge, FACADE_HIGHLIGHT_PX), UiKit.STAND_STONE_RIM, true) # 口沿高光(UiKit.STAND_STONE_RIM 并档——同值单点) + draw_rect(Rect2(0.0, y_near, st.edge, near_h), FACADE_NEAR_COL, true) + draw_rect(Rect2(0.0, y_far, st.edge, far_h), FACADE_FAR_COL, true) # 地面投影(D 档件二——北邻墙格在本已开格北缘投渐隐暗带,自北缘向南三段衰减): # 坐标推导:三等分 proj_band,α 0.28/0.16/0.06(研究件实测贴墙 α≈0.28、带末渐融入地面) func _draw_north_projection() -> void: - if proj_band <= 0.0: + if st.proj_band <= 0.0: return - var seg := proj_band / 3.0 - draw_rect(Rect2(0.0, 0.0, edge, seg), PROJECTION_NEAR_COL, true) - draw_rect(Rect2(0.0, seg, edge, seg), PROJECTION_MID_COL, true) - draw_rect(Rect2(0.0, seg * 2.0, edge, seg), PROJECTION_EDGE_COL, true) + var seg := st.proj_band / 3.0 + draw_rect(Rect2(0.0, 0.0, st.edge, seg), PROJECTION_NEAR_COL, true) + draw_rect(Rect2(0.0, seg, st.edge, seg), PROJECTION_MID_COL, true) + draw_rect(Rect2(0.0, seg * 2.0, st.edge, seg), PROJECTION_EDGE_COL, true) # E 格高墙面(圣经 v3.5 冻结①——方向稿 draw_facade 移植):本格已开且南邻为墙 → @@ -312,31 +377,31 @@ func _draw_north_projection() -> void: # 墙脚阴线只画地面/墙面交界(strip 顶缘 1px);墙面底缘(格界处)不画任何线——直接接南侧墙顶 # (白边导致两截)。仅瓦片态服役(tex_facade 缺位 = 平色回退原语义不动)。 func _draw_e_facade() -> void: - if not facade_strip or tex_facade == null: + if not st.facade_strip or st.tex_facade == null: return - var strip_h := edge * E_FACADE_FRAC - var y0 := edge - strip_h - var ts := tex_facade.get_size() - draw_texture_rect_region(tex_facade, Rect2(0.0, y0, edge, strip_h), + var strip_h := st.edge * E_FACADE_FRAC + var y0 := st.edge - strip_h + var ts := st.tex_facade.get_size() + draw_texture_rect_region(st.tex_facade, Rect2(0.0, y0, st.edge, strip_h), Rect2(0.0, 0.0, ts.x, ts.y * E_FACADE_FRAC)) - draw_rect(Rect2(0.0, y0, edge, 1.0), UiKit.E_FACADE_FOOT_COL, true) + draw_rect(Rect2(0.0, y0, st.edge, 1.0), UiKit.E_FACADE_FOOT_COL, true) # 文字位绘制:水平居中 + 垂直居中(基线 = (格高 − 字高)/2 + 上伸部) func _draw_glyph(g: String, col: Color, fs: int) -> void: - var f := font if font != null else ThemeDB.fallback_font + var f := st.font if st.font != null else ThemeDB.fallback_font if f == null: return var asc := f.get_ascent(fs) var desc := f.get_descent(fs) - draw_string(f, Vector2(0.0, (edge - (asc + desc)) * 0.5 + asc), g, - HORIZONTAL_ALIGNMENT_CENTER, edge, fs, col) + draw_string(f, Vector2(0.0, (st.edge - (asc + desc)) * 0.5 + asc), g, + HORIZONTAL_ALIGNMENT_CENTER, st.edge, fs, col) # 微光子件生命周期:有组才建、组变即换、无组即撤——作用集 ≤8 格常驻, # 其余格零节点零绘制成本。与父件 _draw() 分离的三条理由见本件头注(命令序列/重绘预算/材质挂点)。 func _sync_glow_layer() -> void: - if glow_group == 0: + if st.glow_group == 0: if _glow_layer != null and is_instance_valid(_glow_layer): _glow_layer.queue_free() _glow_layer = null @@ -344,15 +409,15 @@ func _sync_glow_layer() -> void: if _glow_layer == null or not is_instance_valid(_glow_layer): _glow_layer = _GLOW_LAYER.new() add_child(_glow_layer) - _glow_layer.setup(edge, glow_group) - _glow_layer.set_badge_mask(badge_glyph != "") + _glow_layer.setup(st.edge, st.glow_group) + _glow_layer.set_badge_mask(st.badge_glyph != "") # 徽章绘制:右下角金(文字贴角内缩 2–3px) func _draw_badge(g: String) -> void: - var f := font if font != null else ThemeDB.fallback_font + var f := st.font if st.font != null else ThemeDB.fallback_font if f == null: return - var fs := maxi(10, int(edge * 0.28)) + var fs := maxi(10, int(st.edge * 0.28)) var sz := f.get_string_size(g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs) - draw_string(f, Vector2(edge - sz.x - 2.0, edge - 3.0), g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiKit.GOLD) + draw_string(f, Vector2(st.edge - sz.x - 2.0, st.edge - 3.0), g, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, UiKit.GOLD) diff --git a/game/presentation/views/board_fx_view.gd b/game/presentation/views/board_fx_view.gd index 5fc47c7a..ddd764ae 100644 --- a/game/presentation/views/board_fx_view.gd +++ b/game/presentation/views/board_fx_view.gd @@ -89,19 +89,24 @@ func play_boss_shadow() -> void: # 完美封印:金光带加倍扫射 + 横幅占位(§2.2 最高档) func play_perfect_sweep() -> void: - for i in 2: - var band := ColorRect.new() - band.color = Color(COL_GOLD, 0.35) - band.size = Vector2(size.x, 14.0 + 10.0 * i) - band.position = Vector2(-size.x, size.y * 0.3 + 40.0 * i) - band.mouse_filter = Control.MOUSE_FILTER_IGNORE - add_child(band) - var tw := band.create_tween() - tw.tween_property(band, "position:x", size.x + 8.0, UiKit.motion(0.5 + 0.15 * i)) - tw.tween_callback(band.queue_free) + for i in 3: + _spawn_sweep_band(i, size) _show_banner("完美封印 ×2") +## 单光带域(循环体提函数——自左掠过渐出) +func _spawn_sweep_band(i: int, size: Vector2) -> void: + var band := ColorRect.new() + band.color = Color(COL_GOLD, 0.35) + band.size = Vector2(size.x, 14.0 + 10.0 * i) + band.position = Vector2(-size.x, size.y * 0.3 + 40.0 * i) + band.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(band) + var tw := band.create_tween() + tw.tween_property(band, "position:x", size.x + 8.0, UiKit.motion(0.5 + 0.15 * i)) + tw.tween_callback(band.queue_free) + + # 封印命中:封印化暴击弹道加重占位(自棋盘上缘飞向 B 区敌人卡方向——越界即收) func play_seal_hit_projectile(heavy: bool) -> void: var shot := ColorRect.new() diff --git a/game/presentation/views/board_view.gd b/game/presentation/views/board_view.gd index c805c20d..c15e5c67 100644 --- a/game/presentation/views/board_view.gd +++ b/game/presentation/views/board_view.gd @@ -182,7 +182,7 @@ func set_treasure_overlays(reveal_cells: Array, preview_cells: Array) -> void: func set_world_font(f: Font) -> void: _world.board_font = f for c in _cells: - _cells[c].font = f + _cells[c].st.font = f _cells[c].queue_redraw() @@ -201,7 +201,7 @@ func set_cell_area(avail: Vector2) -> void: var stride := _cell + float(SEP) for c in _cells: var u: Node2D = _cells[c] - u.edge = _cell + u.st.edge = _cell u.position = Vector2(c) * stride if _world.hero != null and _hero_sprite.last.x >= 0: _place_hero(_hero_sprite.last, _hero_sprite.last, true) # 格边长变更(窗口缩放)——英雄即时随格重置(杀位移 tween 防飞陈旧落点) @@ -247,17 +247,17 @@ func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void: for x in _board.width: var c := Vector2i(x, y) var u: Node2D = _CELL_UNIT.new() - u.cell = c - u.edge = _cell + u.st.cell = c + u.st.edge = _cell u.position = Vector2(c) * stride - u.bevel = DEPTH_BEVEL # 新建格单元继承 + 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.font = _world.board_font + u.st.font = _world.board_font _world.add_child(u) _cells[c] = u set_cell_area(size) @@ -509,21 +509,21 @@ func _apply_hover_visuals(c: Vector2i) -> void: var seal: bool = _minesweeper.has_seal(c) var adjacent: bool = _diggable.has(c) if revealed: - u.hl_visible = true - u.hl_color = COL_HL_TARGET # 移动目标格边框(金) + u.st.hl_visible = true + u.st.hl_color = COL_HL_TARGET # 移动目标格边框(金) _set_cursor_shape(c, CURSOR_MOVE_SEM) _show_path_preview(c) elif adjacent: - u.hl_visible = true - u.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻封印格同此——左键直接挖) + u.st.hl_visible = true + u.st.hl_color = COL_HL_TARGET # 挖掘目标格边框(相邻封印格同此——左键直接挖) _set_cursor_shape(c, CURSOR_DIG) elif seal: - u.hl_visible = true - u.hl_color = COL_SEAL # 已承诺封印标记(封印黑;无交互可供) + u.st.hl_visible = true + u.st.hl_color = COL_SEAL # 已承诺封印标记(封印黑;无交互可供) _set_cursor_shape(c, Input.CURSOR_FORBIDDEN) else: - u.hl_visible = true - u.hl_color = UiKit.RED_FORBID # 不可及域灰禁框(禁止光标+首遇 toast 双通道) + u.st.hl_visible = true + u.st.hl_color = UiKit.RED_FORBID # 不可及域灰禁框(禁止光标+首遇 toast 双通道) _set_cursor_shape(c, Input.CURSOR_FORBIDDEN) hover_forbidden.emit(c) @@ -531,7 +531,7 @@ func _apply_hover_visuals(c: Vector2i) -> void: func _clear_hover_visuals(c: Vector2i) -> void: if not _cells.has(c): return - _cells[c].hl_visible = false + _cells[c].st.hl_visible = false _hide_path_preview() @@ -546,12 +546,12 @@ func _show_path_preview(c: Vector2i) -> void: var path: Array[Variant] = _minesweeper.compute_move_path(c) for pc in path: if _cells.has(pc): - _cells[pc].dot_visible = true + _cells[pc].st.dot_visible = true func _hide_path_preview() -> void: for c in _cells: - _cells[c].dot_visible = false + _cells[c].st.dot_visible = false # ---- 奖励徽章(已开格身份 icon——封印之下、纹带之上、不进数字区) ---- @@ -611,11 +611,11 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: var digit_col := COL_DIGIT # 封印槽统一维护:封印格=封印印记纹理;其余格清 null(封印格挖开后残留在 tex_seal # 会让 _draw 无条件把封印印画在已开格上——tex_seal 绘制无 revealed 条件)。缺盘走字形回退。 - u.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null + u.st.tex_seal = _palette.seal_tex if (seal and not revealed and _palette.seal_tex != null) else null if removed: revealed = false # 移除格固定未开呈现基座(退色暗幕层承载;危险格本无数字,周边数字冻结) seal = false - u.tex_seal = null + u.st.tex_seal = null if revealed: var rec: Dictionary[Variant, Variant] = _minesweeper.reveal_record(c) # 已开记录经查询面 if rec.get("kind", "") == "mine": @@ -662,8 +662,8 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: # 贴图缺位回退字形「人」(项目纹理槽惯例——贴图在位=格内字形退役,单位本体由独立精灵承载) glyph = "人" digit_col = COL_HERO - u.tex_base = tile - u.bg_sb = sb + u.st.tex_base = tile + u.st.bg_sb = sb u.glyph = glyph u.glyph_color = digit_col # 色牌后备(数字直画退牌;「危」/「?」维持牌载保读性)+ v3.4:双峰纹理板 @@ -673,11 +673,11 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: digit_col = COL_DIGIT # 牌上深墨族(素牌 12:1——双峰纹理免疫) u.glyph_color = digit_col u.plate = revealed and (glyph == "危" or glyph == "?" or plate_digit) - u.outline_sides.assign([]) # typed Array[int](assign 保持引用语义) - u.facade_south = false - u.proj_band = 0.0 - u.tex_facade = null - u.facade_strip = false + u.st.outline_sides.assign([]) # typed Array[int](assign 保持引用语义) + u.st.facade_south = false + u.st.proj_band = 0.0 + u.st.tex_facade = null + u.st.facade_strip = false # 封印槽不在此重置:尾部重置会与此上方条件赋值构成双写踩踏(把封印纹理恒清 null=素材在位时封印不可见); # 上方 else 分支已是全路径清残留,尾部重置系冗余且有害 var south_n: Vector2i = c + Vector2i(0, 1) @@ -687,44 +687,44 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: if not revealed: # 高墙感 D 投影档:南邻地面格 → 墙格南缘立「口沿高光+双段暗带」立面 #(邻地面才画——墙-墙内界保墙群内聚);板外=墙不画 - u.facade_south = south_in and _minesweeper.is_revealed(south_n) + u.st.facade_south = south_in and _minesweeper.is_revealed(south_n) # 轮廓加强:墙格四邻中已开(地面)格朝向记描边码(板外=墙不描——保墙群外轮廓只在墙/地交界) for d in [Vector2i(0, -1), Vector2i(0, 1), Vector2i(-1, 0), Vector2i(1, 0)]: var n: Vector2i = c + d if n.x >= 0 and n.y >= 0 and n.x < _board.width and n.y < _board.height and _minesweeper.is_revealed(n): var code := 0 if d.y < 0 else (1 if d.y > 0 else (2 if d.x < 0 else 3)) - if code == 1 and u.facade_south: + if code == 1 and u.st.facade_south: continue # 南缘由立面件承载(高光+双段暗带)——2px 描边让位防三重叠黑 if code == 0 and _palette.tiles_ok: continue # v3.5 冻结① (b)-(c) 交界无边框:墙面直贴接墙顶(E 格墙面件在邻格承载)——北向描边不画(平色回退态保留描边) - u.outline_sides.append(code) + u.st.outline_sides.append(code) else: # 地面投影:北邻墙格在本已开格北缘投渐隐暗带(ITB 同款高度线索——带宽按格边比例常量) if north_in and not _minesweeper.is_revealed(north_n): - u.proj_band = u.edge * WALL_PROJECTION_FRAC + u.st.proj_band = u.st.edge * WALL_PROJECTION_FRAC # E 格高墙面(圣经 v3.5 冻结①——方向稿 is_facade_cell 口径):已开格南邻未开墙 → 南部 30% # 承接墙面(材质=同板墙顶 center 瓦片,原亮度直贴);仅瓦片态启用——平色回退保持原语义 if south_in and not _minesweeper.is_revealed(south_n) and _palette.tiles_ok: - u.facade_strip = true - u.tex_facade = _palette.tiles.get("wall_center") + u.st.facade_strip = true + u.st.tex_facade = _palette.tiles.get("wall_center") # ②( 秘宝叠加层:真视格=红内缩框;预读格=奖励身份徽章(identity_resolver 既有 # 只读面取字形——零数据复制,badge 位复用);已开格标记自然退位(refresh 逐拍重算) if not revealed: - u.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else "" + u.st.treasure_mark = "mine_reveal" if _treasure_reveal.has(c) else "" if _treasure_preview.has(c) and glyph == "": var pid = _identity_cb.call(c) if _identity_cb.is_valid() else null if pid != null and REWARD_BADGE.has(String(pid.get("cell_type", ""))): glyph = String(REWARD_BADGE[String(pid.get("cell_type", ""))]) digit_col = COL_GOLD - u.badge_glyph = glyph + u.st.badge_glyph = glyph # ②' 变异角标:未开格带伤害型变异 → 危险红角标(已开=威胁已兑现标记退位); # 回调缺位 = 无标记(程序化角标,tex_mutation 纹带槽暂留) if not revealed and _vmark_cb.is_valid(): - u.mutation_mark = String(_vmark_cb.call(c)) + u.st.mutation_mark = String(_vmark_cb.call(c)) else: - u.mutation_mark = "" - u.badge_glyph = _update_badge(c) - u.glow_group = int(_glow.get(c, UiKit.GLOW_NONE)) + u.st.mutation_mark = "" + u.st.badge_glyph = _update_badge(c) + u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE)) # 视野迷雾呈态:曼哈顿距离 > 半径 → 区外——未开格全黑(现行事实源照抄)/ # 已开格记忆态(灰显可读);以英雄格为心逐格重算,refresh()/notify_hero_move() 既有重绘驱动=移动实时跟随。 # 纯呈现零逻辑——格态/数字真值/清图判定零触碰(🔴 契约 1);结算期去饱和叠加互斥由 CellUnit 记忆态终态档承载。 @@ -734,7 +734,7 @@ 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.removed_dim = removed + u.st.removed_dim = removed u.queue_redraw() @@ -765,7 +765,7 @@ func _sync_glow_to_units() -> void: for c in touched: var u: Node2D = _cells.get(c) if u != null: - u.glow_group = int(_glow.get(c, UiKit.GLOW_NONE)) + u.st.glow_group = int(_glow.get(c, UiKit.GLOW_NONE)) func _process(delta: float) -> void: @@ -777,14 +777,14 @@ func _process(delta: float) -> void: if UiKit.motion_scale < 1.0: return # 减动效:呼吸停格(refresh/enable 时已施静态色——静态可辨、无持续运动) var k := 0.5 + 0.5 * sin(_breath_t * 3.0) # 3 rad/s≈0.48Hz——无 >3 次/秒频闪(AC 基线) - _contact_unit.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k) + _contact_unit.st.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k) _contact_unit.queue_redraw() # 带几何写入(refresh / set_dig_glow_enabled(true) 挂点):清旧承载件→按英雄格可挖域写边/角码→施色 func _apply_contact_bands() -> void: if _contact_unit != null and is_instance_valid(_contact_unit) and (_cells.get(_minesweeper.hero_pos()) != _contact_unit if _minesweeper != null else true): - _contact_unit.contact_col = Color(0, 0, 0, 0) + _contact_unit.st.contact_col = Color(0, 0, 0, 0) _contact_unit.queue_redraw() _contact_unit = null if _board == null or _minesweeper == null or not _dig_glow: @@ -801,19 +801,19 @@ func _apply_contact_bands() -> void: for d in [Vector2i(-1, -1), Vector2i(1, -1), Vector2i(-1, 1), Vector2i(1, 1)]: if _diggable.has(_minesweeper.hero_pos() + d): corners.append(0 if (d.x < 0 and d.y < 0) else (1 if d.x > 0 and d.y < 0 else (2 if d.x < 0 else 3))) - hu.contact_bands = bands - hu.contact_corners = corners + hu.st.contact_bands = bands + hu.st.contact_corners = corners # 减动效:呼吸停格——refresh/enable 重放时施**静态**色(本件头注「已施静态色」的落実); # 0.5 = 呼吸周期中值(正常态 k 的均值)。否则减动效下每次 refresh 都会按当前相位重施色, # 带色在 refresh 间跳变=持续运动假象,且令 on/off 双帧取证无法隔离单一变量。 var k := 0.5 if UiKit.motion_scale < 1.0 else 0.5 + 0.5 * sin(_breath_t * 3.0) - hu.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k) + hu.st.contact_col = COL_CONTACT.lerp(UiKit.CONTACT_GLOW_DEEP, 0.55 - 0.35 * k) hu.queue_redraw() func _reset_breath() -> void: if _contact_unit != null and is_instance_valid(_contact_unit): - _contact_unit.contact_col = Color(0, 0, 0, 0) + _contact_unit.st.contact_col = Color(0, 0, 0, 0) _contact_unit.queue_redraw() @@ -888,5 +888,5 @@ func cell_rect_probe(c: Vector2i) -> Rect2: if host.size.x <= 0.0 or host.size.y <= 0.0 or vp_size.x <= 0.0 or vp_size.y <= 0.0: return Rect2() var k: Vector2 = host.size / vp_size - var local := Rect2(u.global_position, Vector2(u.edge, u.edge)) + var local := Rect2(u.global_position, Vector2(u.st.edge, u.st.edge)) return Rect2(host.position + local.position * k, local.size * k) diff --git a/game/presentation/views/board_zone_view.gd b/game/presentation/views/board_zone_view.gd index 68573246..8577227c 100644 --- a/game/presentation/views/board_zone_view.gd +++ b/game/presentation/views/board_zone_view.gd @@ -23,6 +23,12 @@ func _init() -> void: var vb := VBoxContainer.new() vb.add_theme_constant_override("separation", 4) add_child(vb) + _build_views(vb) + _build_status_corner() + + +## 视图三件域:AP 条+棋盘区(含动效与 toast 叠层) +func _build_views(vb: VBoxContainer) -> void: ap_bar = ApBarView.new() vb.add_child(ap_bar) var board_area := Control.new() @@ -31,6 +37,11 @@ func _init() -> void: vb.add_child(board_area) _board_area = board_area board_area.resized.connect(_on_board_area_resized) + _build_board_children(board_area) + + +## 棋盘子件域:视图+动效+toast 三叠层挂接 +func _build_board_children(board_area: Control) -> void: board_view = BoardView.new() board_view.set_anchors_preset(Control.PRESET_FULL_RECT) board_area.add_child(board_view) diff --git a/game/presentation/views/camp_overlay_view.gd b/game/presentation/views/camp_overlay_view.gd index f9e6903d..77458ab6 100644 --- a/game/presentation/views/camp_overlay_view.gd +++ b/game/presentation/views/camp_overlay_view.gd @@ -2,6 +2,10 @@ ## 营地覆盖层:树淬炼/锻造工坊/秘宝匣/奖励格图鉴四柜台只读投影,购买与切换意图经信号外发。 ## 柜台锁定态(钥匙未点亮)开/关零副作用——零写档零报错、锁定行纯文案无可交互购买路径。 ## 开合与开始层互斥归组合根承接(打开前隐藏开始层)。 +## 内部状态件(u1 子域拆件——契约 9 职责轴):_TreeState=树淬炼柜台内容容器(单一变化原因=树行板结构); +## _ForgeState=锻造工坊与秘宝匣两柜台行板与锁定门(钥匙门同构);_AttuneState=同调装配段节点与钮册; +## _SideState=Y 刷新升级行与最近冒险账面。探针直读冻结成员(_balance_label/_tree_buy_btns/ +## _tree_fork_btns/_workshop_holder/_workshop_rows)住宿主不进状态件。 class_name CampOverlay extends Control @@ -12,37 +16,54 @@ signal tree_buy_requested(node_id: String) ## 天赋树购买意图(写口 signal tree_fork_toggled(group: String, side: String) ## 双态切换意图(一节点双态——零币切态) signal forge_buy_requested(route_id: String) ## 锻造路线购买意图(写口唯一归 MetaProgression.forge_buy_level) signal forge_unlock_requested(route_id: String) ## 锻造路线解锁意图(写口唯一归 MetaProgression.forge_buy_unlock) -signal forge_select_requested(route_id: String) ## 锻造路线选中意图(写口唯一归 MetaProgression.forge_select_weapon/armor) +signal forge_select_requested(route_id: String) ## 锻造路线选中意图(写口唯一归 MetaProgression.forge_select) # 方向稿取色(柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit const HEADER_BAND_COL := UiKit.CAMP_HEADER_BAND const ROW_PLATE_COL := UiKit.CAMP_ROW_PLATE -var _balance_label: Label -var _tree_holder: VBoxContainer # 树淬炼柜台内容(主界面——脊柱两行+钥匙/socket 行板+Fork 双态钮) -var _tree_buy_btns: Array[Dictionary] = [] # [{node_id, btn}]——树购买/解锁钮登记面(open/refresh 重建时刷新;探针消费) -var _tree_fork_btns: Array[Dictionary] = [] # [{group, side, btn}]——Fork 双态切换钮登记面(探针消费) -var _forge_lock_l: Label # 锻造工坊柜台锁定态行(钥匙未点亮) -var _forge_workbench: VBoxContainer # 锻造工坊工作间(钥匙点亮后展开——两区六路线行板) -var _forge_zone_weapon: VBoxContainer # 武器锻造区行板容器(三路线) -var _forge_zone_armor: VBoxContainer # 护甲锻造区行板容器(三路线) -var _forge_rows: Array[Dictionary] = [] # [{route_id, name_l, lvl_l, status_l, buy_btn, unlock_btn, select_btn, next_price, unlock_price}](open/refresh 重建;探针消费) -var _ws_scroll: ScrollContainer # 秘宝匣柜台内容容器(钥匙点亮后可见) -var _ws_lock_l: Label # 秘宝匣柜台锁定态行 -var _workshop_holder: VBoxContainer # 秘宝匣柜台行板(六行+同调装配段+Y 升级行) -var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}] -var _attune_header_l: Label # 同调装配段(Slots 点亮呈现) -var _attune_btns: Array[Dictionary] = [] # [{item_id, btn}]——装配/卸下切换钮(满位禁选随刷) -var _attune_nodes: Array[Control] = [] # 段内节点册(open_camp 重建时定向清理,不扰工坊六行) -var _refresh_l: Label # Y 刷新升级行(双入口之一——树上 ks_refresh 同账) -var _refresh_btn: Button -var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建) + +class _TreeState extends RefCounted: + var _tree_holder: VBoxContainer # 树淬炼柜台内容(主界面——脊柱两行+钥匙/socket 行板+Fork 双态钮) + + +class _ForgeState extends RefCounted: + var _forge_lock_l: Label # 锻造工坊柜台锁定态行(钥匙未点亮) + var _forge_workbench: VBoxContainer # 锻造工坊工作间(钥匙点亮后展开——两区六路线行板) + var _forge_zone_weapon: VBoxContainer # 武器锻造区行板容器(三路线) + var _forge_zone_armor: VBoxContainer # 护甲锻造区行板容器(三路线) + var _forge_rows: Array[Dictionary] = [] # [{route_id, name_l, lvl_l, status_l, buy_btn, unlock_btn, select_btn, next_price, unlock_price}](open/refresh 重建;探针消费) + var _ws_scroll: ScrollContainer # 秘宝匣柜台内容容器(钥匙点亮后可见) + var _ws_lock_l: Label # 秘宝匣柜台锁定态行 + + +class _AttuneState extends RefCounted: + var _attune_header_l: Label # 同调装配段(Slots 点亮呈现) + var _attune_btns: Array[Dictionary] = [] # [{item_id, btn}]——装配/卸下切换钮(满位禁选随刷) + var _attune_nodes: Array[Control] = [] # 段内节点册(open_camp 重建时定向清理,不扰工坊六行) + + +class _SideState extends RefCounted: + var _refresh_l: Label # Y 刷新升级行(双入口之一——树上 ks_refresh 同账) + var _refresh_btn: Button + var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建) + + +var _balance_label: Label # 探针直读冻结(S17①——住宿主) +var _tree_buy_btns: Array[Dictionary] = [] # [{node_id, btn}]——树购买/解锁钮登记面(open/refresh 重建时刷新;探针消费;住宿主) +var _tree_fork_btns: Array[Dictionary] = [] # [{group, side, btn}]——Fork 双态切换钮登记面(探针消费;住宿主) +var _workshop_holder: VBoxContainer # 秘宝匣柜台行板(六行+同调装配段+Y 升级行;探针直读冻结——住宿主) +var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}](探针消费;住宿主) +var _tree := _TreeState.new() +var _forge := _ForgeState.new() +var _attune := _AttuneState.new() +var _side := _SideState.new() func _init() -> void: # 底=不透明暖亮档(半透 dim 会透出底下棋盘/战斗提示条,故须实底;demo #v-camp=bg-bright 实底, # 遮蔽语义由组合根输入锁兜底) - var dim := UiKit.modal_dim(self, UiKit.BG_BRIGHT) + var dim := UiKit._modal_dim(self, UiKit.BG_BRIGHT) # 石纹弱质感+暗角(demo .tilebg 0.14/.vignette;缺位不挂即回退平色) var bd := UiKit.backdrop_tile() if bd != null: @@ -61,6 +82,29 @@ func _init() -> void: root.add_child(col) # ── 顶栏:左铭牌(标题+副题)/ 右钥石徽 ── + _build_top_bar(col) + + # ── 四柜台并排(程序占位重排)── + # 宽度不足横向滚动(四柜 300 min 宽 > 窄窗可用宽时横滚兜底;纵向撑满余高) + var camp_scroll := ScrollContainer.new() + camp_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + camp_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO + camp_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + col.add_child(camp_scroll) + var mid := HBoxContainer.new() + mid.add_theme_constant_override("separation", 14) + mid.size_flags_vertical = Control.SIZE_EXPAND_FILL + camp_scroll.add_child(mid) + _build_tree_counter(mid) + _build_forge_counter(mid) + _build_workshop_counter(mid) + _build_atlas_counter(mid) + _build_bottom_bar(col) + visible = false + + +## 顶栏域:铭牌(标题+副题)+钥石徽章 +func _build_top_bar(col: VBoxContainer) -> void: var top := HBoxContainer.new() top.add_theme_constant_override("separation", 10) col.add_child(top) @@ -82,82 +126,78 @@ func _init() -> void: top.add_child(badge) _balance_label = badge.value_label - # ── 四柜台并排(程序占位重排)── - # 宽度不足横向滚动(四柜 300 min 宽 > 窄窗可用宽时横滚兜底;纵向撑满余高) - var camp_scroll := ScrollContainer.new() - camp_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL - camp_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO - camp_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED - col.add_child(camp_scroll) - var mid := HBoxContainer.new() - mid.add_theme_constant_override("separation", 14) - mid.size_flags_vertical = Control.SIZE_EXPAND_FILL - camp_scroll.add_child(mid) - # 柜一·树淬炼(树脊柱界面形态=主界面:一线一节点行板内升级+Fork 双态切换钮) - _tree_holder = VBoxContainer.new() - _tree_holder.add_theme_constant_override("separation", 6) +## 柜一·树淬炼(树脊柱界面形态=主界面:一线一节点行板内升级+Fork 双态切换钮) +func _build_tree_counter(mid: HBoxContainer) -> void: + _tree._tree_holder = VBoxContainer.new() + _tree._tree_holder.add_theme_constant_override("separation", 6) var tree_scroll := ScrollContainer.new() tree_scroll.custom_minimum_size = Vector2(300, 336) tree_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL tree_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED - tree_scroll.add_child(_tree_holder) + tree_scroll.add_child(_tree._tree_holder) var tree_panel := _counter_panel("天赋树 · 淬炼", "", tree_scroll, 300) tree_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL tree_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸 mid.add_child(tree_panel) - # 柜二·锻造工坊(钥匙门——forge_workshop 点亮前锁定态;点亮=工作间两区六路线行板) - _forge_lock_l = UiKit.label("🔒 锻造工坊未开放\n\n点亮树上「锻造工坊」钥匙节点后展开工作间。\n(钥匙免费、无前置——树上直接点亮)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) - _forge_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 - _forge_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - _forge_lock_l.custom_minimum_size = Vector2(280, 0) - _forge_workbench = VBoxContainer.new() - _forge_workbench.add_theme_constant_override("separation", 6) - _forge_workbench.visible = false # 默认锁定态(钥匙门未判前零行板——门开才建) - _forge_zone_weapon = VBoxContainer.new() - _forge_zone_weapon.add_theme_constant_override("separation", 4) - _forge_zone_armor = VBoxContainer.new() - _forge_zone_armor.add_theme_constant_override("separation", 4) + +## 柜二·锻造工坊(钥匙门——forge_workshop 点亮前锁定态;点亮=工作间两区六路线行板) +func _build_forge_counter(mid: HBoxContainer) -> void: + _forge._forge_lock_l = UiKit.label("🔒 锻造工坊未开放\n\n点亮树上「锻造工坊」钥匙节点后展开工作间。\n(钥匙免费、无前置——树上直接点亮)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) + _forge._forge_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 + _forge._forge_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _forge._forge_lock_l.custom_minimum_size = Vector2(280, 0) + _forge._forge_workbench = VBoxContainer.new() + _forge._forge_workbench.add_theme_constant_override("separation", 6) + _forge._forge_workbench.visible = false # 默认锁定态(钥匙门未判前零行板——门开才建) + _forge._forge_zone_weapon = VBoxContainer.new() + _forge._forge_zone_weapon.add_theme_constant_override("separation", 4) + _forge._forge_zone_armor = VBoxContainer.new() + _forge._forge_zone_armor.add_theme_constant_override("separation", 4) var wb_wh := UiKit.label("── 武器锻造区 ──",UiKit.FS_HINT, true, UiKit.GOLD_INK) # 同款深金墨(对柜台板 5.21:1) - _forge_workbench.add_child(wb_wh) - _forge_workbench.add_child(_forge_zone_weapon) + _forge._forge_workbench.add_child(wb_wh) + _forge._forge_workbench.add_child(_forge._forge_zone_weapon) var wb_ah := UiKit.label("── 护甲锻造区 ──",UiKit.FS_HINT, true, UiKit.GOLD_INK) - _forge_workbench.add_child(wb_ah) - _forge_workbench.add_child(_forge_zone_armor) + _forge._forge_workbench.add_child(wb_ah) + _forge._forge_workbench.add_child(_forge._forge_zone_armor) var forge_holder := VBoxContainer.new() forge_holder.add_theme_constant_override("separation", 6) forge_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(与树/秘宝匣 scroll 336 min 对齐——锁定态仅锁文也撑住栏高) - forge_holder.add_child(_forge_lock_l) - forge_holder.add_child(_forge_workbench) + forge_holder.add_child(_forge._forge_lock_l) + forge_holder.add_child(_forge._forge_workbench) var forge_panel := _counter_panel("锻造工坊", UiKit.TEX_ICON_FORGE, forge_holder, 300) forge_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL forge_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸 mid.add_child(forge_panel) - # 柜三·秘宝匣(钥匙门——ks_workshop 点亮前锁定态;点亮=现工坊内容整体迁入:六行+同调装配段+Y 升级行) - _ws_lock_l = UiKit.label("🔒 秘宝匣未开放\n\n点亮树上「秘宝匣」钥匙节点后展开。\n(前置=脊柱生命第 1 级)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) - _ws_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 - _ws_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - _ws_lock_l.custom_minimum_size = Vector2(280, 0) - _ws_scroll = ScrollContainer.new() - _ws_scroll.custom_minimum_size = Vector2(300, 336) - _ws_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL - _ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + +## 柜三·秘宝匣(钥匙门——ks_workshop 点亮前锁定态;点亮=现工坊内容整体迁入:六行+同调装配段+Y 升级行) +func _build_workshop_counter(mid: HBoxContainer) -> void: + _forge._ws_lock_l = UiKit.label("🔒 秘宝匣未开放\n\n点亮树上「秘宝匣」钥匙节点后展开。\n(前置=脊柱生命第 1 级)",UiKit.FS_HINT, false, UiKit.STONE_DEEP) + _forge._ws_lock_l.size_flags_vertical = Control.SIZE_SHRINK_CENTER # 锁文在等高栏内垂直居中 + _forge._ws_lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _forge._ws_lock_l.custom_minimum_size = Vector2(280, 0) + _forge._ws_scroll = ScrollContainer.new() + _forge._ws_scroll.custom_minimum_size = Vector2(300, 336) + _forge._ws_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + _forge._ws_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED _workshop_holder = VBoxContainer.new() _workshop_holder.add_theme_constant_override("separation", 6) - _ws_scroll.add_child(_workshop_holder) + _forge._ws_scroll.add_child(_workshop_holder) var ws_holder := VBoxContainer.new() ws_holder.add_theme_constant_override("separation", 6) ws_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(锁定态 scroll 隐、仅锁文也撑住栏高) - ws_holder.add_child(_ws_lock_l) - ws_holder.add_child(_ws_scroll) + ws_holder.add_child(_forge._ws_lock_l) + ws_holder.add_child(_forge._ws_scroll) var ws_panel := _counter_panel("秘宝匣", UiKit.TEX_ICON_WORKSHOP, ws_holder, 300) ws_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL ws_panel.size_flags_vertical = Control.SIZE_FILL # 四栏等高:任一栏长高时四栏同步拉伸 mid.add_child(ws_panel) - # 柜四·奖励格图鉴(常开独立位——结构位预留,内容排 Alpha) + +## 柜四·奖励格图鉴(常开独立位——结构位预留,内容排 Alpha) +func _build_atlas_counter(mid: HBoxContainer) -> void: var atlas_holder := VBoxContainer.new() atlas_holder.custom_minimum_size = Vector2(0, 336) # 四栏等高(图鉴仅锁文也撑住栏高) var atlas_panel := _counter_panel("奖励格图鉴", UiKit.TEX_ICON_ATLAS, atlas_holder, 300) @@ -170,7 +210,9 @@ func _init() -> void: lock_l.custom_minimum_size = Vector2(280, 0) atlas_holder.add_child(lock_l) - # ── 底栏:关闭钮(左)/ 操作注记(右)+ 最近冒险条带 ── + +## 底栏域:关闭钮(左)/ 操作注记(右)+ 最近冒险条带 +func _build_bottom_bar(col: VBoxContainer) -> void: var close_btn := UiKit.button("关闭营地(回开始界面)") close_btn.pressed.connect(_emit_closed) var bottom := HBoxContainer.new() @@ -182,11 +224,10 @@ func _init() -> void: var caption := UiKit.label("树上点亮 · 工作间随钥匙展开 · 图鉴常开",UiKit.FS_HINT, false, UiKit.WARM_SOFT) bottom.add_child(caption) col.add_child(bottom) - _recent_list = UiKit.label("",UiKit.FS_HINT, false, UiKit.WARM_SOFT) - _recent_list.clip_text = true - _recent_list.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - col.add_child(_recent_list) - visible = false + _side._recent_list = UiKit.label("",UiKit.FS_HINT, false, UiKit.WARM_SOFT) + _side._recent_list.clip_text = true + _side._recent_list.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + col.add_child(_side._recent_list) # 柜台列面板(方向稿同构):PanelContainer 米板身 + 头带深棕(图标+标题暖白) @@ -220,7 +261,7 @@ func _counter_panel(title_text: String, icon_path: String, holder: Control, min_ # 树数据 = 组合根 _camp_data().tree 投影 [{node_id, display_name, vein, kind, level, price, can_buy, # currency}...] + fork_groups [{group, node_id, active_side, owned, can_buy, price, sides:[{side, display_name, effects}]}]) func _rebuild_tree(data: Dictionary) -> void: - for c in _tree_holder.get_children(): + for c in _tree._tree_holder.get_children(): c.queue_free() _tree_buy_btns.clear() _tree_fork_btns.clear() @@ -233,52 +274,62 @@ func _rebuild_tree(data: Dictionary) -> void: if vein != last_vein: last_vein = vein var vh := UiKit.label(String(vein_names.get(vein, vein)),UiKit.FS_HINT, true, UiKit.GOLD_INK) # GOLD_DEEP 对柜台板 2.67:1 越带,换深金墨 5.21:1 - _tree_holder.add_child(vh) - var hb := HBoxContainer.new() - hb.add_theme_constant_override("separation", 6) - var lvl := int(row.get("level", 0)) - var cap := int(row.get("cap", 1)) - var name_l := UiKit.label("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, cap], 13, false, UiKit.INK_LABEL) - name_l.custom_minimum_size = Vector2(150, 0) - name_l.clip_text = true - var gate := String(row.get("gate_text", "")) - if gate != "": - name_l.text += " 🔒" # 等级阈值前置未达标标记(R1 机制位——悬停看门槛;达标后随 refresh 消失) - name_l.tooltip_text = "等级门槛:%s" % gate - hb.add_child(name_l) + _tree._tree_holder.add_child(vh) if String(row.get("kind", "")) == "fork": continue # Fork 行去行板化——由 fork_groups 段承载双态切换钮 - var btn := UiKit.button("购买 %d 钥石" % int(row.get("price", 0)), 0.0) # 柜台行板紧凑钮豁免宽度基准 - btn.pressed.connect(func(): tree_buy_requested.emit(String(row.get("node_id", "")))) - btn.disabled = not bool(row.get("can_buy", false)) - hb.add_child(btn) - _tree_buy_btns.append({"node_id": String(row.get("node_id", "")), "btn": btn}) - _tree_holder.add_child(hb) + _tree._tree_holder.add_child(_tree_row_line(row)) for fg in forks: - var group := String(fg.get("group", "")) - var active := String(fg.get("active_side", "")) - var owned := bool(fg.get("owned", false)) - var fb := HBoxContainer.new() - fb.add_theme_constant_override("separation", 6) - var state_txt := "未解锁" - if owned: - state_txt = "激活:%s" % active - var gl := UiKit.label("%s[%s]" % [String(fg.get("display_name", group)), state_txt], 13, false, UiKit.INK_LABEL) - fb.add_child(gl) - if not owned: - var bb := UiKit.button("解锁 %d 钥石" % int(fg.get("price", 0)), 0.0) # 行板紧凑钮豁免 - bb.pressed.connect(func(): tree_buy_requested.emit(String(fg.get("node_id", "")))) - bb.disabled = not bool(fg.get("can_buy", false)) - fb.add_child(bb) - _tree_buy_btns.append({"node_id": String(fg.get("node_id", "")), "btn": bb}) - else: - for side_row in (fg.get("sides", []) as Array): - var sd := String(side_row.get("side", "")) - var sb := UiKit.button(String(side_row.get("display_name", sd)), 0.0) # 行板紧凑钮豁免 - sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd)) - fb.add_child(sb) - _tree_fork_btns.append({"group": group, "side": sd, "btn": sb}) - _tree_holder.add_child(fb) + _tree._tree_holder.add_child(_tree_fork_line(fg)) + + +## 树行板构建域:节点名(含门槛标记)+购买钮(追加序与锚句随段整体搬迁) +func _tree_row_line(row: Dictionary) -> HBoxContainer: + var hb := HBoxContainer.new() + hb.add_theme_constant_override("separation", 6) + var lvl := int(row.get("level", 0)) + var cap := int(row.get("cap", 1)) + var name_l := UiKit.label("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, cap], 13, false, UiKit.INK_LABEL) + name_l.custom_minimum_size = Vector2(150, 0) + name_l.clip_text = true + var gate := String(row.get("gate_text", "")) + if gate != "": + name_l.text += " 🔒" # 等级阈值前置未达标标记(R1 机制位——悬停看门槛;达标后随 refresh 消失) + name_l.tooltip_text = "等级门槛:%s" % gate + hb.add_child(name_l) + var btn := UiKit.button("购买 %d 钥石" % int(row.get("price", 0)), 0.0) # 柜台行板紧凑钮豁免宽度基准 + btn.pressed.connect(func(): tree_buy_requested.emit(String(row.get("node_id", "")))) + btn.disabled = not bool(row.get("can_buy", false)) + hb.add_child(btn) + _tree_buy_btns.append({"node_id": String(row.get("node_id", "")), "btn": btn}) + return hb + + +## Fork 双态行板构建域:组状态文案+解锁钮或双态切换钮 +func _tree_fork_line(fg: Dictionary) -> HBoxContainer: + var group := String(fg.get("group", "")) + var active := String(fg.get("active_side", "")) + var owned := bool(fg.get("owned", false)) + var fb := HBoxContainer.new() + fb.add_theme_constant_override("separation", 6) + var state_txt := "未解锁" + if owned: + state_txt = "激活:%s" % active + var gl := UiKit.label("%s[%s]" % [String(fg.get("display_name", group)), state_txt], 13, false, UiKit.INK_LABEL) + fb.add_child(gl) + if not owned: + var bb := UiKit.button("解锁 %d 钥石" % int(fg.get("price", 0)), 0.0) # 行板紧凑钮豁免 + bb.pressed.connect(func(): tree_buy_requested.emit(String(fg.get("node_id", "")))) + bb.disabled = not bool(fg.get("can_buy", false)) + fb.add_child(bb) + _tree_buy_btns.append({"node_id": String(fg.get("node_id", "")), "btn": bb}) + else: + for side_row in (fg.get("sides", []) as Array): + var sd := String(side_row.get("side", "")) + var sb := UiKit.button(String(side_row.get("display_name", sd)), 0.0) # 行板紧凑钮豁免 + sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd)) + fb.add_child(sb) + _tree_fork_btns.append({"group": group, "side": sd, "btn": sb}) + return fb # 工作间钥匙门(锻造工坊/秘宝匣柜台锁定态切换:节点点亮=展开、未点亮=锁定行; @@ -289,86 +340,106 @@ func _refresh_key_gates(data: Dictionary) -> void: lvl_by_id[String(row.get("node_id", ""))] = int(row.get("level", 0)) var forge_open := int(lvl_by_id.get("forge_workshop", 0)) >= 1 var ws_open := int(lvl_by_id.get("ks_workshop", 0)) >= 1 - _forge_lock_l.visible = not forge_open - _forge_workbench.visible = forge_open - _ws_lock_l.visible = not ws_open - _ws_scroll.visible = ws_open + _forge._forge_lock_l.visible = not forge_open + _forge._forge_workbench.visible = forge_open + _forge._ws_lock_l.visible = not ws_open + _forge._ws_scroll.visible = ws_open # 锻造工坊行板重建(两区六路线;open_camp/refresh_camp 调用。行结构仿秘宝匣行板: # 件名+等级+解锁态+购买/解锁/选中钮。UI 只发意图信号〔契约 9——零核心写调用、零 MetaProgression # 持有〕;钮禁用=常态防线(余额/未解锁/满级/已选中),兜底拒绝可见归 presenter pattern 1) func _rebuild_forge(data: Dictionary) -> void: - for zone in [_forge_zone_weapon, _forge_zone_armor]: + for zone in [_forge._forge_zone_weapon, _forge._forge_zone_armor]: for c in zone.get_children(): c.queue_free() - _forge_rows.clear() - if not _forge_workbench.visible: + _forge._forge_rows.clear() + if not _forge._forge_workbench.visible: return # 锁定态(钥匙未点亮):零行板零购买路径——占位纪律延续 var bal := int(data.get("balance", 0)) for row in data.get("forge", []): - var rid := String(row.get("route_id", "")) var kind := String(row.get("kind", "")) - var zone: VBoxContainer = _forge_zone_weapon if kind == "weapon" else _forge_zone_armor - var unlocked := bool(row.get("unlocked", false)) - var initial := bool(row.get("initial_owned", false)) - var lvl := int(row.get("level", 0)) - var cap_v := int(row.get("cap", 0)) - var next_price := int(row.get("next_price", 0)) - var unlock_price := int(row.get("unlock_price", 0)) - var card := PanelContainer.new() - card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) - var line := HBoxContainer.new() - line.add_theme_constant_override("separation", 6) - card.add_child(line) - var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK) - name_l.custom_minimum_size = Vector2(104, 0) - name_l.clip_text = true - name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(name_l) - var lvl_l := UiKit.label("Lv%d/%d" % [lvl, cap_v], 13, false, UiKit.INK_LABEL) - lvl_l.custom_minimum_size = Vector2(44, 0) - line.add_child(lvl_l) - # 解锁态/解锁通道呈现(并联双通道:练满前一档 or 一次性解锁价直购) - var st_text := "已解锁" - if not unlocked: - if initial: - st_text = "未解锁" - else: - st_text = "未解锁 · 练满%s 或 %d 钥石直购" % [String(row.get("prereq_name", "")), unlock_price] - elif lvl >= cap_v: - st_text = "已满级" - var status_l := UiKit.label(st_text, UiKit.FS_HINT, false, UiKit.STONE_DEEP) - status_l.clip_text = true - status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL - line.add_child(status_l) - # 购买钮(已解锁未满级——几何价显示;余额不足禁用) - var buy_btn: Button = null - if unlocked and lvl < cap_v: - buy_btn = UiKit.button("锻造 %d" % next_price, 0.0) # 行板紧凑钮豁免 - buy_btn.pressed.connect(forge_buy_requested.emit.bind(rid)) - buy_btn.disabled = bal < next_price - line.add_child(buy_btn) - # 解锁钮(待解锁非初始——一次性解锁价) - var unlock_btn: Button = null - if not unlocked and not initial: - unlock_btn = UiKit.button("解锁 %d" % unlock_price, 0.0) # 行板紧凑钮豁免 - unlock_btn.pressed.connect(forge_unlock_requested.emit.bind(rid)) - unlock_btn.disabled = bal < unlock_price - line.add_child(unlock_btn) - # 选中钮(已解锁——免费切换;当前=禁用态标记) - var select_btn: Button = null - if unlocked: - var sel := bool(row.get("selected", false)) - select_btn = UiKit.button("当前" if sel else "选中", 0.0) # 行板紧凑钮豁免 - select_btn.pressed.connect(forge_select_requested.emit.bind(rid)) - select_btn.disabled = sel - line.add_child(select_btn) - zone.add_child(card) - _forge_rows.append({"route_id": rid, "name_l": name_l, "lvl_l": lvl_l, "status_l": status_l, - "buy_btn": buy_btn, "unlock_btn": unlock_btn, "select_btn": select_btn, - "next_price": next_price, "unlock_price": unlock_price}) + var zone: VBoxContainer = _forge._forge_zone_weapon if kind == "weapon" else _forge._forge_zone_armor + var card := _forge_route_card(row, bal) + zone.add_child(card["card"]) + _forge._forge_rows.append(card) + + +## 锻造行板构建域:单路线卡(件名+等级+状态+购买/解锁/选中钮——并联双通道呈现面) +func _forge_route_card(row: Dictionary, bal: int) -> Dictionary: + var d := _forge_route_scalars(row) + var card := PanelContainer.new() + card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER, UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) + var line := HBoxContainer.new() + line.add_theme_constant_override("separation", 6) + card.add_child(line) + var name_l := UiKit.label(String(row.get("display_name", "")), UiKit.FS_HINT, true, UiKit.INK) + name_l.custom_minimum_size = Vector2(104, 0) + name_l.clip_text = true + name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + line.add_child(name_l) + var lvl_l := UiKit.label("Lv%d/%d" % [d["lvl"], d["cap_v"]], 13, false, UiKit.INK_LABEL) + lvl_l.custom_minimum_size = Vector2(44, 0) + line.add_child(lvl_l) + var status_l := UiKit.label(_forge_status_text(d, row), UiKit.FS_HINT, false, UiKit.STONE_DEEP) + status_l.clip_text = true + status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL + line.add_child(status_l) + var btns := _forge_route_buttons(d, row, bal, line) + return {"card": card, "route_id": d["rid"], "name_l": name_l, "lvl_l": lvl_l, "status_l": status_l, + "buy_btn": btns["buy_btn"], "unlock_btn": btns["unlock_btn"], "select_btn": btns["select_btn"], + "next_price": d["next_price"], "unlock_price": d["unlock_price"]} + + +## 锻造行标量束:路线 id/解锁态/初始持有/等级/cap/几何价/解锁价一次取数 +func _forge_route_scalars(row: Dictionary) -> Dictionary: + return { + "rid": String(row.get("route_id", "")), + "unlocked": bool(row.get("unlocked", false)), + "initial": bool(row.get("initial_owned", false)), + "lvl": int(row.get("level", 0)), + "cap_v": int(row.get("cap", 0)), + "next_price": int(row.get("next_price", 0)), + "unlock_price": int(row.get("unlock_price", 0)), + } + + +## 状态文案域(并联双通道:练满前一档 or 一次性解锁价直购;已满级态) +func _forge_status_text(d: Dictionary, row: Dictionary) -> String: + var st_text := "已解锁" + if not d["unlocked"]: + if d["initial"]: + st_text = "未解锁" + else: + st_text = "未解锁 · 练满%s 或 %d 钥石直购" % [String(row.get("prereq_name", "")), d["unlock_price"]] + elif d["lvl"] >= d["cap_v"]: + st_text = "已满级" + return st_text + + +## 三钮域:购买(已解锁未满级——余额不足禁用)/解锁(待解锁非初始——余额不足禁用)/选中(已解锁——当前=禁用态标记) +func _forge_route_buttons(d: Dictionary, row: Dictionary, bal: int, line: HBoxContainer) -> Dictionary: + var buy_btn: Button = null + if d["unlocked"] and d["lvl"] < d["cap_v"]: + buy_btn = UiKit.button("锻造 %d" % d["next_price"], 0.0) # 行板紧凑钮豁免 + buy_btn.pressed.connect(forge_buy_requested.emit.bind(d["rid"])) + buy_btn.disabled = bal < d["next_price"] + line.add_child(buy_btn) + var unlock_btn: Button = null + if not d["unlocked"] and not d["initial"]: + unlock_btn = UiKit.button("解锁 %d" % d["unlock_price"], 0.0) # 行板紧凑钮豁免 + unlock_btn.pressed.connect(forge_unlock_requested.emit.bind(d["rid"])) + unlock_btn.disabled = bal < d["unlock_price"] + line.add_child(unlock_btn) + var select_btn: Button = null + if d["unlocked"]: + var sel := bool(row.get("selected", false)) + select_btn = UiKit.button("当前" if sel else "选中", 0.0) # 行板紧凑钮豁免 + select_btn.pressed.connect(forge_select_requested.emit.bind(d["rid"])) + select_btn.disabled = sel + line.add_child(select_btn) + return {"buy_btn": buy_btn, "unlock_btn": unlock_btn, "select_btn": select_btn} # 打开营地(全量重建四柜台行 + 同调装配段 + 最近冒险;树段与钥匙门随 refresh_camp 重建—— @@ -389,6 +460,12 @@ func refresh_camp(data: Dictionary) -> void: _rebuild_tree(data) _refresh_key_gates(data) _rebuild_forge(data) # 行板随刷重建(购买/解锁/选中后等级/价/禁用态同步) + _refresh_workshop_rows(data, balance) + _refresh_attune_segment(data, balance) + + +## 秘宝匣行板刷新域:进度列(解锁混合制三形态)与状态色(亮板双色墨制)与购买钮余额联动 +func _refresh_workshop_rows(data: Dictionary, balance: int) -> void: var ws_by_id := {} for row in data.get("workshop", []): ws_by_id[String(row.get("item_id", ""))] = row @@ -397,21 +474,7 @@ func refresh_camp(data: Dictionary) -> void: if row.is_empty(): continue var unlocked := bool(row.get("unlocked", false)) - # 秘宝匣进度列(解锁混合制三形态:钥石购买价/条件进度拼接/已入手) - var prog_text := "" - var mode := String(row.get("mode", "")) - if unlocked: - prog_text = "已入手" - elif mode == "keystone": - prog_text = "钥石 %d" % int(row.get("price", 0)) - elif mode == "condition": - var parts: Array[String] = [] - for req_v in (row.get("reqs", []) as Array): - var req: Dictionary[Variant, Variant] = req_v as Dictionary - var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件")) - parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))]) - prog_text = "、".join(parts) - card.progress_l.text = prog_text + card.progress_l.text = _workshop_progress_text(row, unlocked) # 状态色用亮板双色墨制(深墨族替代暖白族——亮板上暖白不可读) var st_text := "未解锁" var st_col: Color = UiKit.STONE_DEEP @@ -424,111 +487,148 @@ func refresh_camp(data: Dictionary) -> void: # 购买钮余额联动(禁用态=余额不足,常态防线;pattern 1 兜底拒绝可见归 presenter) if card.get("buy_btn") != null: card.buy_btn.disabled = balance < int(card.get("price", 0)) - # 同调装配段状态就地刷(Slots 点亮/满位禁选/Y 升级行余额联动) + + +## 进度列文案域(解锁混合制三形态:钥石购买价/条件进度拼接/已入手) +func _workshop_progress_text(row: Dictionary, unlocked: bool) -> String: + if unlocked: + return "已入手" + var mode := String(row.get("mode", "")) + if mode == "keystone": + return "钥石 %d" % int(row.get("price", 0)) + if mode == "condition": + var parts: Array[String] = [] + for req_v in (row.get("reqs", []) as Array): + var req: Dictionary[Variant, Variant] = req_v as Dictionary + var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件")) + parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))]) + return "、".join(parts) + return "" + + +## 同调装配段刷新域(Slots 点亮/满位禁选/Y 升级行余额联动——就地刷) +func _refresh_attune_segment(data: Dictionary, balance: int) -> void: var att: Dictionary[Variant, Variant] = data.get("attune", {}) - if _attune_header_l != null: + if _attune._attune_header_l != null: var slots_n := int(att.get("slots", 0)) var eq: Array[Variant] = att.get("equipped", []) - _attune_header_l.text = "── 同调装配 Slots %d/%d ──" % [eq.size(), slots_n] + _attune._attune_header_l.text = "── 同调装配 Slots %d/%d ──" % [eq.size(), slots_n] var eq_set := {} for v in eq: eq_set[String(v)] = true - for card2 in _attune_btns: + for card2 in _attune._attune_btns: var on: bool = eq_set.has(String(card2.item_id)) card2.btn.text = "卸下" if on else "装配" # 满位禁选+未解锁禁装(已装件恒可卸下;unlocked 快照随 rebuild 刷新) card2.btn.disabled = (not on and eq.size() >= slots_n) or (not on and not bool(card2.get("unlocked", false))) - if _refresh_l != null: + if _side._refresh_l != null: var rf: Dictionary[Variant, Variant] = att.get("refresh", {}) - _refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))] - _refresh_btn.text = "已满级" if bool(rf.get("maxed", false)) else "升级 %d 钥石" % int(rf.get("price", 0)) - _refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0)) + _side._refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))] + _side._refresh_btn.text = "已满级" if bool(rf.get("maxed", false)) else "升级 %d 钥石" % int(rf.get("price", 0)) + _side._refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0)) func _rebuild_workshop(data: Dictionary) -> void: _workshop_rows.clear() for child in _workshop_holder.get_children(): child.queue_free() - _attune_nodes.clear() - _attune_btns.clear() - _attune_header_l = null - _refresh_l = null - _refresh_btn = null + _attune._attune_nodes.clear() + _attune._attune_btns.clear() + _attune._attune_header_l = null + _side._refresh_l = null + _side._refresh_btn = null for row in data.get("workshop", []): - var card := PanelContainer.new() - card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) - var line := HBoxContainer.new() - line.add_theme_constant_override("separation", 12) - card.add_child(line) - var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "")) - var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""),UiKit.FS_HINT, true, UiKit.INK) - name_l.custom_minimum_size = Vector2(132, 0) - name_l.clip_text = true - name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀) - line.add_child(name_l) - var progress_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 同行价格墨同案 - progress_l.custom_minimum_size = Vector2(150, 0) - progress_l.clip_text = true # 营地溢出修——文字全宽不撑柜 - progress_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(progress_l) - # 状态色用亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式 - var status_l := UiKit.label("",UiKit.FS_HINT, true, UiKit.STONE_DEEP) - status_l.clip_text = true # 同上——「已解锁 · 能力封存中」不撑柜(余宽吸收者) - status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL - line.add_child(status_l) - # 钥石购买钮(购买件且未解锁时在列;写口唯一=buy_treasure_unlock, - # 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足) - var buy_btn: Button = null - if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone": - buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免 - buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", "")))) - line.add_child(buy_btn) - _workshop_holder.add_child(card) - _workshop_rows.append({"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false))}) + var card := _workshop_card(row) + _workshop_holder.add_child(card["card"]) + _workshop_rows.append(card) _rebuild_attune(data) +## 秘宝匣行板构建域:件名(分档后缀+效果摘要悬停)+进度列+状态列+钥石购买钮(购买件未解锁在列) +func _workshop_card(row: Dictionary) -> Dictionary: + var card := PanelContainer.new() + card.add_theme_stylebox_override("panel", UiKit.panel_flat(ROW_PLATE_COL, UiKit.PANEL_BORDER,UiKit.RADIUS_OBJECT, 1, Vector4(10, 4, 10, 4))) + var line := HBoxContainer.new() + line.add_theme_constant_override("separation", 12) + card.add_child(line) + var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "")) + var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""),UiKit.FS_HINT, true, UiKit.INK) + name_l.custom_minimum_size = Vector2(132, 0) + name_l.clip_text = true + name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀) + line.add_child(name_l) + var progress_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 同行价格墨同案 + progress_l.custom_minimum_size = Vector2(150, 0) + progress_l.clip_text = true # 营地溢出修——文字全宽不撑柜 + progress_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + line.add_child(progress_l) + # 状态色用亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式 + var status_l := UiKit.label("",UiKit.FS_HINT, true, UiKit.STONE_DEEP) + status_l.clip_text = true # 同上——「已解锁 · 能力封存中」不撑柜(余宽吸收者) + status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL + line.add_child(status_l) + # 钥石购买钮(购买件且未解锁时在列;写口唯一=buy_treasure_unlock, + # 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足) + var buy_btn: Button = null + if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone": + buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免 + buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", "")))) + line.add_child(buy_btn) + return {"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false)), "card": card} + + # 同调装配段重建(秘宝匣柜台尾部段;Slots 点亮+ # 九件行板+Y 升级行,程序占位面) func _rebuild_attune(data: Dictionary) -> void: var att: Dictionary[Variant, Variant] = data.get("attune", {}) - _attune_header_l = UiKit.label("",UiKit.FS_HINT, true, UiKit.INK) - _workshop_holder.add_child(_attune_header_l) - _attune_nodes.append(_attune_header_l) + _attune._attune_header_l = UiKit.label("",UiKit.FS_HINT, true, UiKit.INK) + _workshop_holder.add_child(_attune._attune_header_l) + _attune._attune_nodes.append(_attune._attune_header_l) for row in att.get("items", []): - var iid := String(row.get("item_id", "")) - var line := HBoxContainer.new() - line.add_theme_constant_override("separation", 6) - var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK) - name_l.custom_minimum_size = Vector2(96, 0) - name_l.clip_text = true - name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(name_l) - var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) - form_l.custom_minimum_size = Vector2(58, 0) - line.add_child(form_l) - # 档位标签(槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边) - var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")),UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) - tier_l.custom_minimum_size = Vector2(36, 0) - line.add_child(tier_l) - var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免 - btn.pressed.connect(attune_toggled.emit.bind(iid)) - btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫) - line.add_child(btn) + var line := _attune_item_line(row) _workshop_holder.add_child(line) - _attune_nodes.append(line) - _attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))}) + _attune._attune_nodes.append(line) + _refresh_attune_tail(att) + + +## 同调件行板构建域:件名+形态+档位+装配切换钮(未解锁禁装——toggle 写口同守卫) +func _attune_item_line(row: Dictionary) -> HBoxContainer: + var iid := String(row.get("item_id", "")) + var line := HBoxContainer.new() + line.add_theme_constant_override("separation", 6) + var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK) + name_l.custom_minimum_size = Vector2(96, 0) + name_l.clip_text = true + name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + line.add_child(name_l) + var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) + form_l.custom_minimum_size = Vector2(58, 0) + line.add_child(form_l) + # 档位标签(槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边) + var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")),UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) + tier_l.custom_minimum_size = Vector2(36, 0) + line.add_child(tier_l) + var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免 + btn.pressed.connect(attune_toggled.emit.bind(iid)) + btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫) + line.add_child(btn) + _attune._attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))}) + return line + + +## Y 升级尾行构建域(双入口之一——树上 ks_refresh 同账) +func _refresh_attune_tail(att: Dictionary) -> void: var refresh_line := HBoxContainer.new() refresh_line.add_theme_constant_override("separation", 6) - _refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) - refresh_line.add_child(_refresh_l) - _refresh_btn = UiKit.button("升级", 0.0) # 行板紧凑钮豁免 - _refresh_btn.pressed.connect(_emit_buy_requested.bind("attune_refresh")) - refresh_line.add_child(_refresh_btn) + _side._refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP) + refresh_line.add_child(_side._refresh_l) + _side._refresh_btn = UiKit.button("升级", 0.0) # 行板紧凑钮豁免 + _side._refresh_btn.pressed.connect(_emit_buy_requested.bind("attune_refresh")) + refresh_line.add_child(_side._refresh_btn) _workshop_holder.add_child(refresh_line) - _attune_nodes.append(refresh_line) + _attune._attune_nodes.append(refresh_line) func _rebuild_recent(data: Dictionary) -> void: @@ -544,7 +644,7 @@ func _rebuild_recent(data: Dictionary) -> void: verdict = "放弃" lines.append("最近冒险:%s · 完成 %d 关 · 钥石 +%d · 种子 %d" % [verdict, int(s.get("levels_cleared", 0)), int(s.get("keystone_total", 0)), int(s.get("master_seed", 0))]) - _recent_list.text = " | ".join(lines) + _side._recent_list.text = " | ".join(lines) func _emit_closed() -> void: diff --git a/game/presentation/views/confirm_dialog.gd b/game/presentation/views/confirm_dialog.gd index b7816983..5025b5b0 100644 --- a/game/presentation/views/confirm_dialog.gd +++ b/game/presentation/views/confirm_dialog.gd @@ -14,7 +14,7 @@ var _cancel_btn: Button func _init() -> void: var box: VBoxContainer = UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(460, 0), 12, - true, UiKit.panel_skinned(Vector4(14, 12, 14, 12))).box + UiKit.panel_skinned(Vector4(14, 12, 14, 12))).box _text = UiKit.label("",UiKit.FS_BODY, false, UiKit.INK_TITLE) _text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _text.custom_minimum_size = Vector2(430, 0) diff --git a/game/presentation/views/curio_pick_overlay.gd b/game/presentation/views/curio_pick_overlay.gd index 46e2eb67..3ca6c255 100644 --- a/game/presentation/views/curio_pick_overlay.gd +++ b/game/presentation/views/curio_pick_overlay.gd @@ -30,7 +30,14 @@ func _init() -> void: # bcard 三竖卡(D1 232×352)+ 底部确认主钮与免费选择注记。差异白名单=免费选择/✓章/无价签。 # 状态机不变:pick→result。M7 同构裁决遗产保留。 var page: Dictionary[Variant, Variant] = UiKit.page_shell(self, false) - var top: HBoxContainer = page.top_row + _build_top_row(page.top_row) + var box: VBoxContainer = page.box + _build_body(box) + visible = false + + +## 顶条域:标题+刷新图标钮(44×44 豁免宽度基准+角标叠加)+刷新余量行 +func _build_top_row(top: HBoxContainer) -> void: top.add_child(UiKit.label("战利品三选一",UiKit.FS_TITLE, true, UiKit.WARM)) var top_sp := Control.new() top_sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -51,7 +58,32 @@ func _init() -> void: _refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.WARM_SOFT) _refresh_l.visible = false top.add_child(_refresh_l) - var box: VBoxContainer = page.box + + +## 主体域:入账信息行+三竖卡列+底部确认/继续钮与免费选择注记 +func _build_body(box: VBoxContainer) -> void: + _label = UiKit.label("",UiKit.FS_BODY, false, UiKit.WARM_SOFT) + _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + box.add_child(_label) + _choice_box = HBoxContainer.new() + _choice_box.add_theme_constant_override("separation", 26) # demo .cards gap 26(T7 对齐) + _choice_box.alignment = BoxContainer.ALIGNMENT_CENTER + _choice_box.size_flags_vertical = Control.SIZE_EXPAND_FILL + box.add_child(_choice_box) + var foot := HBoxContainer.new() + foot.alignment = BoxContainer.ALIGNMENT_CENTER + foot.add_theme_constant_override("separation", 16) + box.add_child(foot) + _confirm_btn = UiKit.primary_button("确认选择") + _confirm_btn.pressed.connect(_on_confirm_pressed) + _confirm_btn.visible = false + foot.add_child(_confirm_btn) + _continue_btn = UiKit.button("继续") + _continue_btn.pressed.connect(_emit_continue_pressed) + _continue_btn.visible = false + foot.add_child(_continue_btn) + _foot_hint = UiKit.label("免费选择 · 刷新不消耗 AP",UiKit.FS_HINT, false, UiKit.WARM_FAINT) + foot.add_child(_foot_hint) _label = UiKit.label("",UiKit.FS_BODY, false, UiKit.WARM_SOFT) _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(_label) @@ -96,7 +128,7 @@ func open_pick(header: String, cards: Array) -> void: var idl := cid.to_lower() var band := UiKit.tg_band(idl) # 类型色带单点(TG_* 五族同源判定;aoe/blast 归 TG_AOE) # 类型脚注(T7 判官 checkpoint:demo .bcard .src 底部类型行——实机缺=卡下半空底;词=demo 同款) - var card := UiKit.bcard(title, desc, band, _icon_for_id(idl), "", _type_label(idl)) + var card := UiKit.bcard(title, desc, band, _icon_for_id(idl), {"footnote": _type_label(idl)}) card.gui_input.connect(func(ev: InputEvent) -> void: if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT: _select_card(cid)) @@ -189,19 +221,15 @@ func _emit_continue_pressed() -> void: # M7:id → 手绘图标键(子串映射容错 id 词表演进;缺位=BCard 空图标槽不炸) +## 图标映射表(id 子串→图标键——判定序 heal/blast/gp/attack/armor/ap 缺省钱袋逐字) func _icon_for_id(idl: String) -> String: - if idl.contains("heal"): - return UiKit.TEX_ICON_MEDKIT - if idl.contains("blast") or idl.contains("magic"): - return UiKit.TEX_ICON_BLAST - if idl.contains("gp") or idl.contains("coin"): - return UiKit.TEX_ICON_COINBAG - if idl.contains("attack"): - return UiKit.TEX_ICON_SWORD - if idl.contains("armor"): - return UiKit.TEX_ICON_SHIELD - if idl.contains("ap") or idl.contains("action"): - return UiKit.TEX_ICON_POUCH + var rules := [[["heal"], UiKit.TEX_ICON_MEDKIT], [["blast", "magic"], UiKit.TEX_ICON_BLAST], + [["gp", "coin"], UiKit.TEX_ICON_COINBAG], [["attack"], UiKit.TEX_ICON_SWORD], + [["armor"], UiKit.TEX_ICON_SHIELD], [["ap", "action"], UiKit.TEX_ICON_POUCH]] + for rule in rules: + for kw in (rule[0] as Array): + if idl.contains(String(kw)): + return String(rule[1]) return UiKit.TEX_ICON_COINBAG diff --git a/game/presentation/views/debug_overlay.gd b/game/presentation/views/debug_overlay.gd index 19128e92..80e80373 100644 --- a/game/presentation/views/debug_overlay.gd +++ b/game/presentation/views/debug_overlay.gd @@ -10,30 +10,36 @@ signal debug_levels_requested # 两层调试对局入口 signal diagnose_requested # 诊断转储入口(卡死取证编排归组合根) signal closed -var seed_edit: LineEdit # 组合根 _seed_value 唯一读取面 +## 内部状态件(u7 子域拆件——契约 9 职责语句「调试浮窗运行态」):单一变化原因=调试面板状态与 +## 当局数据投影。探针直读冻结成员(seed_edit/_sim_btn/_dbg_btn)住宿主; +## st._run_data/st._run_labels 经 run_data_probe()/update_run_data() 方法出口(字段直访面零外部依赖)。 +class _DebugState extends RefCounted: + var _db + var _base_atk := 0 + var _base_hp := 0 + var _atk: HSlider + var _hp: HSlider + var _atk_val: Label + var _hp_val: Label + var _eff: Label + var _sim_count: SpinBox + var _sim_out: RichTextLabel + var _sim_running := false # 批量模拟进行中(is_running 判据——与对局中禁用态分离防互扰) + var _ctx_locked := false # 对局中上下文锁定(组合根 _sync_debug_overlay 喂入——批量模拟/两层调试入口禁用) + var _run_labels := {} # 当局数据六项标签(只读投影,组合根 update_run_data 喂入) + var _run_data := {} # 最近一次投喂快照(探针值对面 run_data_probe) -var _db -var _base_atk := 0 -var _base_hp := 0 -var _atk: HSlider -var _hp: HSlider -var _atk_val: Label -var _hp_val: Label -var _eff: Label -var _sim_count: SpinBox -var _sim_btn: Button -var _dbg_btn: Button # 两层调试入口钮(对局中禁用承载面) -var _sim_out: RichTextLabel -var _sim_running := false # 批量模拟进行中(is_running 判据——与对局中禁用态分离防互扰) -var _ctx_locked := false # 对局中上下文锁定(组合根 _sync_debug_overlay 喂入——批量模拟/两层调试入口禁用) -var _run_labels := {} # 当局数据六项标签(只读投影,组合根 update_run_data 喂入) -var _run_data := {} # 最近一次投喂快照(探针值对面 run_data_probe) + +var st := _DebugState.new() +var seed_edit: LineEdit # 组合根 _seed_value 唯一读取面(探针直读冻结) +var _sim_btn: Button # 两层调试批量模拟入口钮(探针直读冻结) +var _dbg_btn: Button # 两层调试入口钮(对局中禁用承载面;探针直读冻结) func _init(db_ref, base_atk: int, base_hp: int) -> void: - _db = db_ref - _base_atk = base_atk - _base_hp = base_hp + st._db = db_ref + st._base_atk = base_atk + st._base_hp = base_hp _build() _sync_labels() visible = false # 显式初始化——不靠树序巧合(缺陷⑤家族纪律) @@ -42,78 +48,78 @@ func _init(db_ref, base_atk: int, base_hp: int) -> void: # ---- 对外语义面(组合根/探针)---- func atk_pct() -> int: - return int(_atk.value) + return int(st._atk.value) func hp_pct() -> int: - return int(_hp.value) + return int(st._hp.value) func set_sliders(atk: int, hp: int) -> void: - _atk.value = atk - _hp.value = hp + st._atk.value = atk + st._hp.value = hp func reset_sliders() -> void: - _atk.value = 0 - _hp.value = 0 + st._atk.value = 0 + st._hp.value = 0 _sync_labels() func effect_text() -> String: - return _eff.text + return st._eff.text func set_sim_count(n: int) -> void: - _sim_count.value = n + st._sim_count.value = n func is_running() -> bool: - return _sim_running # 批量模拟进行中(与对局中禁用态分离,置灰 ≠ 在跑) + return st._sim_running # 批量模拟进行中(与对局中禁用态分离,置灰 ≠ 在跑) func set_sim_running(running: bool) -> void: - _sim_running = running + st._sim_running = running _sync_sim_button() # 对局中上下文锁定(组合根 _sync_debug_overlay 喂入):批量模拟与两层调试入口 # 在活跃对局并存态不开放(长程异步模拟/并发对局编排与活跃结算并存态破坏状态机) func set_context_locked(locked: bool) -> void: - _ctx_locked = locked + st._ctx_locked = locked _sync_sim_button() # 当局数据只读投影喂入(六项——组合根 _debug_run_data 构造;纯标签重查零动效) func update_run_data(data: Dictionary) -> void: - _run_data = data.duplicate() - for key in _run_labels: - (_run_labels[key] as Label).text = str(data.get(key, "—")) + st._run_data = data.duplicate() + for key in st._run_labels: + (st._run_labels[key] as Label).text = str(data.get(key, "—")) # 当局数据快照读面(探针值对面——ui_tune_probe 断言六项与只读访问器逐项一致) func run_data_probe() -> Dictionary: - return _run_data.duplicate() + return st._run_data.duplicate() # 两钮同判据(模拟运行 / 对局上下文锁定对「模拟」「两层调试」两入口等价)—— # 判据单点:两 setter 都经此同步,禁在调用点各写一份 func _sync_sim_button() -> void: - _sim_btn.disabled = _sim_running or _ctx_locked - _sim_btn.text = "模拟中…" if _sim_running else "运行模拟" - _dbg_btn.disabled = _sim_running or _ctx_locked + _sim_btn.disabled = st._sim_running or st._ctx_locked + _sim_btn.text = "模拟中…" if st._sim_running else "运行模拟" + _dbg_btn.disabled = st._sim_running or st._ctx_locked func set_sim_progress(text: String) -> void: - _sim_out.text = text + st._sim_out.text = text func set_sim_report(text: String) -> void: - _sim_out.text = text + st._sim_out.text = text func output_text() -> String: - return _sim_out.get_parsed_text() + return st._sim_out.get_parsed_text() # ---- 探针接缝(真实翻译出口——按钮 pressed 信号路径,非直调组合根)---- @@ -141,11 +147,11 @@ func _reset_btn_pressed() -> void: func effective_atk() -> int: - return UiKit.scaled(_base_atk, atk_pct()) + return UiKit.scaled(st._base_atk, atk_pct()) func effective_max_hp() -> int: - return UiKit.scaled(_base_hp, hp_pct()) + return UiKit.scaled(st._base_hp, hp_pct()) # ---- 内部构建(标题/关闭固定于浮窗骨架,可扩展列表区居中滚动)---- @@ -153,15 +159,7 @@ func effective_max_hp() -> int: func _build() -> void: var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, UiKit.BG_OVERLAY, Vector2(640, 0), 8) var box: VBoxContainer = shell.box - - var title := UiKit.label("调试",UiKit.FS_TITLE, true, UiKit.WARM) - box.add_child(title) - var desc := UiKit.label( - "开发调试专用(UI 最顶层「调试」浮层角标随时可开——任何界面不遮挡)。当局数据=本局只读投影,种子即本局种子唯一查看位。\n百分比叠加基础值:攻击 %d / 生命 %d。影响所有模式(手动与模拟),不落盘。" % [_base_atk, _base_hp], - 13, false, UiKit.WARM_SOFT) - box.add_child(desc) - box.add_child(HSeparator.new()) - + _build_intro(box) # 可扩展列表页(后续调试项按区块往下追加——超高滚动,滚轮下探) var scroll := ScrollContainer.new() scroll.custom_minimum_size = Vector2(0, 430) @@ -171,10 +169,32 @@ func _build() -> void: list.size_flags_horizontal = Control.SIZE_EXPAND_FILL list.add_theme_constant_override("separation", 8) scroll.add_child(list) + _build_run_data(list) + _build_fx_row(list) + _build_seed_row(list) + _build_slider_block(list) + _build_sim_block(list) + _build_debug_diag(list) + var close_btn := UiKit.button("关闭") + close_btn.pressed.connect(_emit_closed) + box.add_child(close_btn) - # 区块·当局数据(六项只读投影:种子=本局种子唯一查看位〔adventure - # .master_seed 现值,非 _seed_value() 重算〕/关卡/节点/HP/GP/钥石;数据由组合根开窗与模态开合 - # 时 update_run_data 喂入;纯标签零信号连接零动效) + +## 头部域:标题+说明(百分比叠加基础值——影响所有模式,不落盘) +func _build_intro(box: VBoxContainer) -> void: + var title := UiKit.label("调试",UiKit.FS_TITLE, true, UiKit.WARM) + box.add_child(title) + var desc := UiKit.label( + "开发调试专用(UI 最顶层「调试」浮层角标随时可开——任何界面不遮挡)。当局数据=本局只读投影,种子即本局种子唯一查看位。\n百分比叠加基础值:攻击 %d / 生命 %d。影响所有模式(手动与模拟),不落盘。" % [st._base_atk, st._base_hp], + 13, false, UiKit.WARM_SOFT) + box.add_child(desc) + box.add_child(HSeparator.new()) + + +## 区块·当局数据(六项只读投影:种子=本局种子唯一查看位〔adventure +## .master_seed 现值,非 _seed_value() 重算〕/关卡/节点/HP/GP/钥石;数据由组合根开窗与模态开合 +## 时 update_run_data 喂入;纯标签零信号连接零动效) +func _build_run_data(list: VBoxContainer) -> void: var run_head := UiKit.label("当局数据(只读)",UiKit.FS_HINT, true, UiKit.WARM) list.add_child(run_head) var run_grid := GridContainer.new() @@ -187,11 +207,13 @@ func _build() -> void: cap.custom_minimum_size = Vector2(52, 0) run_grid.add_child(cap) var val := UiKit.label("—",UiKit.FS_HINT, false, UiKit.WARM) - _run_labels[item[0]] = val + st._run_labels[item[0]] = val run_grid.add_child(val) list.add_child(HSeparator.new()) - # 区块⓪胜利演出风格(两套并存——纹章拼合[默认]/尘金粒子;会话级不落盘) + +## 区块⓪胜利演出风格(两套并存——纹章拼合[默认]/尘金粒子;会话级不落盘) +func _build_fx_row(list: VBoxContainer) -> void: var fx_row := HBoxContainer.new() fx_row.add_theme_constant_override("separation", 10) list.add_child(fx_row) @@ -201,8 +223,10 @@ func _build() -> void: fx_btn.pressed.connect(_on_fx_toggle.bind(fx_btn)) # lambda 转命名方法(只读巡检白名单方法名精确可判) fx_row.add_child(fx_btn) - # 区块①随机种子(种子三分判定归组合根 _seed_value 单点): - # 默认空框(空=随全局规则:首局恒 42/非首局随机;显式填入全场景优先) + +## 区块①随机种子(种子三分判定归组合根 _seed_value 单点): +## 默认空框(空=随全局规则:首局恒 42/非首局随机;显式填入全场景优先) +func _build_seed_row(list: VBoxContainer) -> void: var seed_row := HBoxContainer.new() list.add_child(seed_row) seed_row.add_child(UiKit.label("随机种子",UiKit.FS_BODY, false)) @@ -213,16 +237,17 @@ func _build() -> void: seed_row.add_child(seed_edit) list.add_child(HSeparator.new()) - # 区块②角色数值调整板 - var atk_row := _slider_row(list, "攻击加成") - _atk = atk_row.slider - _atk_val = atk_row.value_label - var hp_row := _slider_row(list, "生命加成") - _hp = hp_row.slider - _hp_val = hp_row.value_label - _eff = UiKit.label("",UiKit.FS_HINT, false) - list.add_child(_eff) +## 区块②角色数值调整板(攻击/生命两滑轨+生效值行+应用/重置钮) +func _build_slider_block(list: VBoxContainer) -> void: + var atk_row := _slider_row(list, "攻击加成") + st._atk = atk_row.slider + st._atk_val = atk_row.value_label + var hp_row := _slider_row(list, "生命加成") + st._hp = hp_row.slider + st._hp_val = hp_row.value_label + st._eff = UiKit.label("",UiKit.FS_HINT, false) + list.add_child(st._eff) var btn_row := HBoxContainer.new() list.add_child(btn_row) var apply_btn := UiKit.button("应用并关闭") @@ -233,34 +258,36 @@ func _build() -> void: btn_row.add_child(reset_btn) list.add_child(HSeparator.new()) - # 区块③批量模拟(与 headless 扫描同口径——编排归 tune_sim_controller) + +## 区块③批量模拟(与 headless 扫描同口径——编排归 tune_sim_controller) +func _build_sim_block(list: VBoxContainer) -> void: var sim_row := HBoxContainer.new() list.add_child(sim_row) sim_row.add_child(UiKit.label("批量模拟第一关",UiKit.FS_BODY, false)) - _sim_count = SpinBox.new() - _sim_count.min_value = 10 - _sim_count.max_value = 200 - _sim_count.step = 10 - _sim_count.value = 30 - _sim_count.custom_minimum_size = Vector2(90, 0) - sim_row.add_child(_sim_count) + st._sim_count = SpinBox.new() + st._sim_count.min_value = 10 + st._sim_count.max_value = 200 + st._sim_count.step = 10 + st._sim_count.value = 30 + st._sim_count.custom_minimum_size = Vector2(90, 0) + sim_row.add_child(st._sim_count) sim_row.add_child(UiKit.label("次(理性玩家策略,含面板当前加成)",UiKit.FS_HINT, false)) _sim_btn = UiKit.button("运行模拟") _sim_btn.pressed.connect(_emit_simulate_requested) sim_row.add_child(_sim_btn) - _sim_out = RichTextLabel.new() - _sim_out.bbcode_enabled = true - _sim_out.custom_minimum_size = Vector2(0, 300) - list.add_child(_sim_out) + st._sim_out = RichTextLabel.new() + st._sim_out.bbcode_enabled = true + st._sim_out.custom_minimum_size = Vector2(0, 300) + list.add_child(st._sim_out) list.add_child(HSeparator.new()) - # 区块④两层调试对局入口(HP 关内延续;对局中禁用) + +## 区块④⑤两层调试入口与诊断转储(对局中禁用;转储编排经信号外发归组合根——纯视图边界; +## dump_diagnosis 卡死取证功能保留,不设门控隐藏——本浮窗本就可全局可开,双轨无意义) +func _build_debug_diag(list: VBoxContainer) -> void: _dbg_btn = UiKit.button("调试 · 连续关卡两层(HP 关内延续)") _dbg_btn.pressed.connect(_emit_debug_levels) list.add_child(_dbg_btn) - - # 区块⑤诊断转储:dump_diagnosis 卡死取证功能保留;不设门控隐藏——本浮窗本就可全局可开,双轨无意义。 - # 纯视图边界:转储编排经信号外发归组合根。 var diag_row := HBoxContainer.new() diag_row.add_theme_constant_override("separation", 10) list.add_child(diag_row) @@ -270,10 +297,6 @@ func _build() -> void: diag_btn.pressed.connect(_emit_diagnose_requested) diag_row.add_child(diag_btn) - var close_btn := UiKit.button("关闭") - close_btn.pressed.connect(_emit_closed) - box.add_child(close_btn) - func _slider_row(box: VBoxContainer, title: String) -> Dictionary: var row := HBoxContainer.new() @@ -293,14 +316,14 @@ func _slider_row(box: VBoxContainer, title: String) -> Dictionary: func _sync_labels() -> void: - _atk_val.text = "+%d%%" % atk_pct() - _hp_val.text = "+%d%%" % hp_pct() - _eff.text = "生效值:攻击 %d → %d · 生命 %d → %d" % [ - _base_atk, effective_atk(), _base_hp, effective_max_hp()] + st._atk_val.text = "+%d%%" % atk_pct() + st._hp_val.text = "+%d%%" % hp_pct() + st._eff.text = "生效值:攻击 %d → %d · 生命 %d → %d" % [ + st._base_atk, effective_atk(), st._base_hp, effective_max_hp()] func _emit_simulate_requested() -> void: - simulate_requested.emit(int(_sim_count.value)) + simulate_requested.emit(int(st._sim_count.value)) func _emit_diagnose_requested() -> void: diff --git a/game/presentation/views/hero_info_panel_view.gd b/game/presentation/views/hero_info_panel_view.gd index 7b1c5ca5..7dd765d7 100644 --- a/game/presentation/views/hero_info_panel_view.gd +++ b/game/presentation/views/hero_info_panel_view.gd @@ -30,9 +30,14 @@ func _init() -> void: var col := VBoxContainer.new() col.add_theme_constant_override("separation", 14) # 布局充实(区段间距放大) add_child(col) + _build_data_row(col) + _build_slots(col) + _build_side_sections(col) + +## 数据行域(直角细线数据框——battle.md Component Inventory C 区行规格落形;同时消化纵向空白) +func _build_data_row(col: VBoxContainer) -> void: col.add_child(UiKit.label("探险者",UiKit.FS_SUB, true)) - # 数据行装框(直角细线数据框——battle.md Component Inventory C 区行规格落形;同时消化纵向空白) var data_box := PanelContainer.new() data_box.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(6, 4, 6, 4))) @@ -45,55 +50,60 @@ func _init() -> void: _armor_label = _data_line() data_col.add_child(_armor_label) - # 秘宝槽(预留):随身战斗用道具的唯一展示面——空槽占位承诺格子在此, - # 不预设交互(挖掘期指向交互归 E 区既有裁决) + +## 秘宝槽域(预留):随身战斗用道具的唯一展示面——空槽占位承诺格子在此, +## 不预设交互(挖掘期指向交互归 E 区既有裁决) +func _build_slots(col: VBoxContainer) -> void: col.add_child(UiKit.label("秘宝",UiKit.FS_BODY, true)) item_slots = HBoxContainer.new() item_slots.add_theme_constant_override("separation", 10) col.add_child(item_slots) for i in 4: - var slot := PanelContainer.new() - slot.custom_minimum_size = Vector2(48, 48) - slot.add_theme_stylebox_override("panel", - UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(4, 4, 4, 4))) - var idx := i - slot.gui_input.connect(func(ev: InputEvent): - if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT: - # 空槽/越界槽点击=取消选中(handle 0);有效槽发其 handle(组合根判 AP 即用/进选中态) - treasure_slot_clicked.emit(_slot_handles[idx] if idx < _slot_handles.size() else 0)) - item_slots.add_child(slot) - _slot_panels.append(slot) + _slot_panels.append(_build_slot(i)) - # 特性持有段(三选一特性兑付后常驻可读:逐件行「件名 ×N」;空态整节隐藏。 - # 上移承载——构筑核心信息优先级高于临时强化,且防 C 区下部裁切不可见) - _feat_section = VBoxContainer.new() - _feat_section.add_theme_constant_override("separation", 4) - _feat_section.visible = false - col.add_child(_feat_section) - _feat_section.add_child(UiKit.label("专长",UiKit.FS_BODY, true)) - _feat_list = VBoxContainer.new() - _feat_list.add_theme_constant_override("separation", 2) - _feat_section.add_child(_feat_list) - # 地形变异常驻角标(Boss 图整图型战斗全程可见防中局遗忘; - # 空态整节隐藏=特性段先例;摆位/文案 provisional) - _board_variant_section = VBoxContainer.new() - _board_variant_section.add_theme_constant_override("separation", 4) - _board_variant_section.visible = false - col.add_child(_board_variant_section) - _board_variant_section.add_child(UiKit.label("地形变异",UiKit.FS_BODY, true)) - _board_variant_list = VBoxContainer.new() - _board_variant_list.add_theme_constant_override("separation", 2) - _board_variant_section.add_child(_board_variant_list) - # 特性(预留):锻造/大奖励/关卡奖励节点的属性提升——中循环数据因改变战斗数学准入; - # 与战斗内状态效果(B 区站桩,随战斗消散)分区,防「战斗结束被清」误读;空态整节隐藏 - _curio_section = VBoxContainer.new() - _curio_section.add_theme_constant_override("separation", 4) - _curio_section.visible = false - col.add_child(_curio_section) - _curio_section.add_child(UiKit.label("属性值提升",UiKit.FS_BODY, true)) - _curio_list = VBoxContainer.new() - _curio_list.add_theme_constant_override("separation", 2) - _curio_section.add_child(_curio_list) + +## 单槽构建域:48px 槽牌+点击路由(空槽/越界槽点击=取消选中 handle 0;有效槽发其 handle) +func _build_slot(i: int) -> PanelContainer: + var slot := PanelContainer.new() + slot.custom_minimum_size = Vector2(48, 48) + slot.add_theme_stylebox_override("panel", + UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(4, 4, 4, 4))) + var idx := i + slot.gui_input.connect(func(ev: InputEvent): + if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT: + # 空槽/越界槽点击=取消选中(handle 0);有效槽发其 handle(组合根判 AP 即用/进选中态) + treasure_slot_clicked.emit(_slot_handles[idx] if idx < _slot_handles.size() else 0)) + item_slots.add_child(slot) + return slot + + +## 三区段域(特性持有段上移承载——构筑核心信息优先级高于临时强化,且防 C 区下部裁切不可见/ +## 地形变异常驻角标 Boss 图整图型战斗全程可见防中局遗忘/特性预留段防「战斗结束被清」误读;空态整节隐藏) +func _build_side_sections(col: VBoxContainer) -> void: + _feat_section = _labeled_section(col, "专长") + _feat_list = _list_of(_feat_section) + _board_variant_section = _labeled_section(col, "地形变异") + _board_variant_list = _list_of(_board_variant_section) + _curio_section = _labeled_section(col, "属性值提升") + _curio_list = _list_of(_curio_section) + + +## 区段骨架(标题+隐藏容器) +func _labeled_section(col: VBoxContainer, title: String) -> VBoxContainer: + var section := VBoxContainer.new() + section.add_theme_constant_override("separation", 4) + section.visible = false + col.add_child(section) + section.add_child(UiKit.label(title,UiKit.FS_BODY, true)) + return section + + +## 行板容器(separation 2) +func _list_of(section: VBoxContainer) -> VBoxContainer: + var list := VBoxContainer.new() + list.add_theme_constant_override("separation", 2) + section.add_child(list) + return list func set_attack(atk: int, mod_text: String) -> void: @@ -174,46 +184,69 @@ func set_treasure_inventory(items: Array, selected_handle: int) -> void: continue var rec: Dictionary[Variant, Variant] = items[i] _slot_handles.append(int(rec.get("handle", 0))) - var used := bool(rec.get("used", false)) - var usable := bool(rec.get("usable", true)) - var selected := selected_handle != 0 and int(rec.get("handle", 0)) == selected_handle - # 槽内容=手绘像素图标(demo ⑦;glyph 字符→图标键映射,未映射/used/不可用回退字形+条件墨色) - var glyph_ch := String(rec.get("glyph", "?")) - var icon_map := {"爆": UiKit.TEX_ICON_BLAST, "医": UiKit.TEX_ICON_MEDKIT, - "铲": UiKit.TEX_ICON_SHOVEL, "袋": UiKit.TEX_ICON_COINBAG, "钥": UiKit.TEX_ICON_KEYSHARD, - "剑": UiKit.TEX_ICON_SWORD, "盾": UiKit.TEX_ICON_SHIELD, "心": UiKit.TEX_ICON_HEART, - "行": UiKit.TEX_ICON_POUCH} - var icon_path: String = icon_map.get(glyph_ch, "") - var itex := UiKit.load_tex(icon_path) if icon_path != "" else null - if itex != null and not used and usable: - var tr := TextureRect.new() - tr.texture = itex - tr.custom_minimum_size = Vector2(40, 40) - tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - tr.set_anchors_preset(Control.PRESET_FULL_RECT) - tr.mouse_filter = Control.MOUSE_FILTER_IGNORE - slot.add_child(tr) - else: - var glyph := UiKit.label(glyph_ch,UiKit.FS_SUB, true) - glyph.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - glyph.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - glyph.set_anchors_preset(Control.PRESET_FULL_RECT) - glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE - if used: - # 「已用」占位墨=灰石深化(STONE_FAINT 对 PANEL_DATA 槽牌 2.7:1; - # STONE_DEEP = 5.6:1;语义分档保持双墨) - glyph.add_theme_color_override("font_color", UiKit.STONE_DEEP) - elif not usable: - # 「不可用」置灰墨=标签深墨(STONE 对槽牌 3.4:1;INK_LABEL = 10.4:1,虚位/失效更强置灰语义) - glyph.add_theme_color_override("font_color", UiKit.INK_LABEL) - slot.add_child(glyph) - var border_col: Color = UiKit.GOLD if selected else UiKit.PANEL_BORDER - slot.add_theme_stylebox_override("panel", - UiKit.panel_flat(UiKit.PANEL_DATA, border_col, 0, 1 if not selected else 2, Vector4(4, 4, 4, 4))) - # note=积点进度行(自动兑付件悬停浮窗补「积点 N/M」,进度随挖格经组合根重查刷新) - var note := String(rec.get("note", "")) - slot.tooltip_text = "%s%s%s" % [String(rec.get("name", "")), - "(%s)" % note if note != "" else "", - "(已用)" if used else ("(不可用——当前无有效目标)" if not usable else "")] + _fill_slot(slot, rec, selected_handle) + + +## 单槽内容填充域:图标优先(像素图标未映射/used/不可用回退字形+条件墨色)+选中金框+悬停浮窗 +func _fill_slot(slot: PanelContainer, rec: Dictionary, selected_handle: int) -> void: + var used := bool(rec.get("used", false)) + var usable := bool(rec.get("usable", true)) + var selected := selected_handle != 0 and int(rec.get("handle", 0)) == selected_handle + # 槽内容=手绘像素图标(demo ⑦;glyph 字符→图标键映射,未映射/used/不可用回退字形+条件墨色) + var itex := _slot_icon_tex(String(rec.get("glyph", "?"))) + if itex != null and not used and usable: + slot.add_child(_slot_icon_rect(itex)) + else: + slot.add_child(_slot_glyph_label(String(rec.get("glyph", "?")), used, usable)) + var border_col: Color = UiKit.GOLD if selected else UiKit.PANEL_BORDER + slot.add_theme_stylebox_override("panel", + UiKit.panel_flat(UiKit.PANEL_DATA, border_col, 0, 1 if not selected else 2, Vector4(4, 4, 4, 4))) + slot.tooltip_text = _slot_tooltip(rec, used, usable) + + +## 字形→图标纹理映射(demo ⑦ 键表单点) +func _slot_icon_tex(glyph_ch: String) -> Texture2D: + var icon_map := {"爆": UiKit.TEX_ICON_BLAST, "医": UiKit.TEX_ICON_MEDKIT, + "铲": UiKit.TEX_ICON_SHOVEL, "袋": UiKit.TEX_ICON_COINBAG, "钥": UiKit.TEX_ICON_KEYSHARD, + "剑": UiKit.TEX_ICON_SWORD, "盾": UiKit.TEX_ICON_SHIELD, "心": UiKit.TEX_ICON_HEART, + "行": UiKit.TEX_ICON_POUCH} + var icon_path: String = icon_map.get(glyph_ch, "") + return UiKit.load_tex(icon_path) if icon_path != "" else null + + +## 图标矩形域(40px 素描框,像素过滤) +func _slot_icon_rect(itex: Texture2D) -> TextureRect: + var tr := TextureRect.new() + tr.texture = itex + tr.custom_minimum_size = Vector2(40, 40) + tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + tr.set_anchors_preset(Control.PRESET_FULL_RECT) + tr.mouse_filter = Control.MOUSE_FILTER_IGNORE + return tr + + +## 字形回退标签域(used=灰石深化墨 5.6:1/不可用=标签深墨 10.4:1 虚位失效更强置灰——语义分档双墨) +func _slot_glyph_label(glyph_ch: String, used: bool, usable: bool) -> Label: + var glyph := UiKit.label(glyph_ch,UiKit.FS_SUB, true) + glyph.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + glyph.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + glyph.set_anchors_preset(Control.PRESET_FULL_RECT) + glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE + if used: + # 「已用」占位墨=灰石深化(STONE_FAINT 对 PANEL_DATA 槽牌 2.7:1; + # STONE_DEEP = 5.6:1;语义分档保持双墨) + glyph.add_theme_color_override("font_color", UiKit.STONE_DEEP) + elif not usable: + # 「不可用」置灰墨=标签深墨(STONE 对槽牌 3.4:1;INK_LABEL = 10.4:1,虚位/失效更强置灰语义) + glyph.add_theme_color_override("font_color", UiKit.INK_LABEL) + return glyph + + +## 悬停浮窗域(note=积点进度行——自动兑付件悬停浮窗补「积点 N/M」,进度随挖格经组合根重查刷新) +func _slot_tooltip(rec: Dictionary, used: bool, usable: bool) -> String: + var note := String(rec.get("note", "")) + return "%s%s%s" % [String(rec.get("name", "")), + "(%s)" % note if note != "" else "", + "(已用)" if used else ("(不可用——当前无有效目标)" if not usable else "")] diff --git a/game/presentation/views/legend_overlay.gd b/game/presentation/views/legend_overlay.gd index 08454647..238d4c0c 100644 --- a/game/presentation/views/legend_overlay.gd +++ b/game/presentation/views/legend_overlay.gd @@ -23,7 +23,7 @@ var _rap_cost := -1 # <0 = 不渲染 RAP 行(构造参数注表值,禁字 func _init(rap_cost: int = -1) -> void: _rap_cost = rap_cost - var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(520, 0), 6, true, + var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(520, 0), 6, UiKit.panel_skinned(Vector4(12, 10, 12, 10))) # 石板皮(T2;缺位回退 flat) _panel = shell.panel (shell.dim as ColorRect).gui_input.connect(_on_dim_gui_input) @@ -32,17 +32,7 @@ func _init(rap_cost: int = -1) -> void: box.add_child(title) box.add_child(HSeparator.new()) for e in ENTRIES: - var row := HBoxContainer.new() - row.add_theme_constant_override("separation", 8) - var chip := ColorRect.new() - chip.custom_minimum_size = Vector2(18, 18) - chip.color = Color(e[0]) - row.add_child(chip) - var lab := UiKit.label("%s —— %s" % [e[1], e[2]], 13, false, UiKit.INK_LABEL) # 亮底墨字(§7.2 数据素面) - lab.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - lab.custom_minimum_size = Vector2(440, 0) - row.add_child(lab) - box.add_child(row) + box.add_child(_legend_row(e)) box.add_child(HSeparator.new()) # RAP 行(表值驱动,禁字面量:刷新代价读行动经济表) if _rap_cost >= 0: @@ -64,6 +54,24 @@ func _init(rap_cost: int = -1) -> void: visible = false + +## 单行域(循环体提函数——色块 chip+词条 18px 网格对齐) +func _legend_row(e: Array) -> HBoxContainer: + var row := HBoxContainer.new() + row.add_theme_constant_override("separation", 8) + var chip := ColorRect.new() + chip.custom_minimum_size = Vector2(18, 18) + chip.color = Color(e[0]) + row.add_child(chip) + var lab := UiKit.label("%s —— %s" % [e[1], e[2]], 13, false, UiKit.INK_LABEL) # 亮底墨字(§7.2 数据素面) + lab.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + lab.custom_minimum_size = Vector2(440, 0) + row.add_child(lab) + return row + + + + func is_open() -> bool: return visible diff --git a/game/presentation/views/level_map_screen.gd b/game/presentation/views/level_map_screen.gd index 65a53761..9f53deaa 100644 --- a/game/presentation/views/level_map_screen.gd +++ b/game/presentation/views/level_map_screen.gd @@ -31,43 +31,47 @@ const HIT_PAD := 6.0 # 图上命中外扩(指 const CONFIRM_WINDOW_S := 3.0 # 放弃确认态窗口(秒——超时回落) const COL_HP_BAR_BACK := UiKit.HP_BAR_BACK # 迷你血条深底(对暗面板可辨;值单点收 UiKit) -var _runner = null -var _title: Label -var _canvas: Control -var _level_label: Label -var _gp_value: Label -var _hp_value: Label -var _hp_bar: Control -var _attr_value: Label -var _bonus_row: HBoxContainer -var _bonus_value: Label -var _feat_value: Label # 特性持有行(组合根投喂逐件文案) -var _cand_ids: Array[int] = [] # 本停点候选 id 集(图上命中合法集) -var _content_size := Vector2(620, 240) # 图内容包络(open_choices 按图拟合——画布 min 尺寸随它) -var _hover_id := -1 # 悬停预承诺候选(-1 = 无) -var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开) -var status_badge: Button # 状态面板入口角标(顶栏 GP 徽右侧;连接归组合根) -var _abandon_timer := ArmedTimer.new() # 放弃确认窗(_process 帧驱动倒计时机制收口;窗口=CONFIRM_WINDOW_S) -var _drawn_disp: Dictionary[int, Dictionary] = {} # 本停点实际绘制记录(_draw_graph 逐帧在实际绘制点重建——display_probe 消费面) -var _node_icons := {} # 节点图标贴图(load_tex 守卫——缺位 null → 字形回退,冒烟/无素材检出安全) -var _noise_rect: TextureRect # 屏底石纹(换层色板渐变承载面——null = 纯色回退无挂点) -var _tint_level := 0 # 当前已应用 tint 的层号(0 = 未应用) -var _layout_cache := {"nodes": [], "canvas_size": Vector2.ZERO, "pos": {}} # 布局缓存(_layout 纯函数于 (nodes, canvas)——键=nodes 内容等价+画布尺寸,图态换代或画布尺寸变即自然失效) +## 内部状态件(u2 子域拆件——契约 9 职责轴):_MapState=选路屏运行态与图绘制态(单一变化原因= +## 选路呈现)。_layer_decor 域字典承载换层装饰三件(图标贴图/石纹面/tint 层号——装饰域归并形态)。 +## 探针直读冻结成员(_cand_ids/_hover_id/_content_size/_layout_cache/abandon_btn/status_badge)住宿主。 +class _MapState extends RefCounted: + var _runner = null + var _title: Label + var _canvas: Control + var _level_label: Label + var _gp_value: Label + var _hp_value: Label + var _hp_bar: Control + var _attr_value: Label + var _bonus_row: HBoxContainer + var _bonus_value: Label + var _feat_value: Label # 特性持有行(组合根投喂逐件文案) + var _abandon_timer := ArmedTimer.new() # 放弃确认窗(_process 帧驱动倒计时机制收口;窗口=CONFIRM_WINDOW_S) + var _drawn_disp: Dictionary[int, Dictionary] = {} # 本停点实际绘制记录(_draw_graph 逐帧在实际绘制点重建——display_probe 消费面) + var _layer_decor := {"node_icons": {}, "noise_rect": null, "tint_level": 0} # 换层装饰域(节点图标贴图 load_tex 守卫缺位 null→字形回退/屏底石纹面/当前已应用 tint 层号 0=未应用) + +var _cand_ids: Array[int] = [] # 本停点候选 id 集(图上命中合法集;探针直读冻结——住宿主) +var _content_size := Vector2(620, 240) # 图内容包络(open_choices 按图拟合——画布 min 尺寸随它;探针直读冻结) +var _hover_id := -1 # 悬停预承诺候选(-1 = 无;探针直读冻结) +var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开;探针直读冻结) +var status_badge: Button # 状态面板入口角标(顶栏 GP 徽右侧;连接归组合根;探针直读冻结) +var _layout_cache := {"nodes": [], "canvas_size": Vector2.ZERO, "pos": {}} # 布局缓存(_layout 纯函数于 (nodes, canvas)——键=nodes 内容等价+画布尺寸,图态换代或画布尺寸变即自然失效;探针直读冻结) +var _map := _MapState.new() func _init() -> void: - _abandon_timer.expired.connect(_reset_abandon_confirm) + _map._abandon_timer.expired.connect(_reset_abandon_confirm) # 节点图标贴图装载(load_tex 守卫,缺位回退现行字形) for t in TEX_NODE_ICONS: - _node_icons[t] = UiKit.load_tex(TEX_NODE_ICONS[t]) + _map._layer_decor.node_icons[t] = UiKit.load_tex(TEX_NODE_ICONS[t]) # 独立屏底(spec Layout——demo #v-map 暖棕竖向 # 渐变(#4A3B2A 0%→#3A2E22 40%)+石纹弱质感 0.14+暗角聚焦;底纹缺位回退纯色) - var base := UiKit.gradient_base(UiKit.BG_MAP_TOP, UiKit.BG_ROOT) + var base := UiKit._gradient_base(UiKit.BG_MAP_TOP, UiKit.BG_ROOT) add_child(base) var tex := UiKit.backdrop_tile() if tex != null: base.add_child(tex) - _noise_rect = tex # 世界侧 tint 承载面(换层色板渐变挂点——§2.7 切片档) + _map._layer_decor.noise_rect = tex # 世界侧 tint 承载面(换层色板渐变挂点——§2.7 切片档) base.add_child(UiKit.vignette_rect()) var center := CenterContainer.new() center.set_anchors_preset(Control.PRESET_FULL_RECT) @@ -75,9 +79,16 @@ func _init() -> void: var box := VBoxContainer.new() box.add_theme_constant_override("separation", 10) center.add_child(box) - # 顶栏语境(spec Layout:左关卡名 / 右 GP 常驻徽章——直角细线数据框; - # 对齐 demo ④:关卡名入框板+GP 徽章=烛光胶囊(同商店GP框)+ - # 左右两极排布) + _build_top_bar(box) + _build_canvas(box) + _build_status_bar(box) + visible = false + + +## 顶栏域(spec Layout:左关卡名 / 右 GP 常驻徽章——直角细线数据框; +## 对齐 demo ④:关卡名入框板+GP 徽章=烛光胶囊(同商店GP框)+ 左右两极排布; +## 状态角标置灰守卫归组合根 _sync_status_entry,信号连接归组合根——视图零连接) +func _build_top_bar(box: VBoxContainer) -> void: var top := HBoxContainer.new() top.add_theme_constant_override("separation", 12) top.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -86,8 +97,8 @@ func _init() -> void: lvl_plate.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.DARK_UPCOMING, UiKit.PANEL_BORDER, 0, 1, Vector4(12, 3, 12, 3))) top.add_child(lvl_plate) - _level_label = UiKit.label("",UiKit.FS_BODY, true, UiKit.WARM) - lvl_plate.add_child(_level_label) + _map._level_label = UiKit.label("",UiKit.FS_BODY, true, UiKit.WARM) + lvl_plate.add_child(_map._level_label) var top_sp := Control.new() top_sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL top.add_child(top_sp) @@ -98,31 +109,34 @@ func _init() -> void: var gp_row := HBoxContainer.new() gp_row.add_theme_constant_override("separation", 6) gp_box.add_child(gp_row) - _gp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_ON_GOLD) - gp_row.add_child(_gp_value) - # 状态面板入口角标(顶栏常驻;本屏只留状态——调试角标归全局浮层。 - # 置灰守卫归组合根 _sync_status_entry(其他模态开着=不可点;本屏自身的登记开态不计——has_open_excluding); - # 信号连接归组合根(视图零连接)) + _map._gp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_ON_GOLD) + gp_row.add_child(_map._gp_value) status_badge = UiKit.button("状态", 0.0) status_badge.add_theme_font_size_override("font_size", UiKit.FS_HINT) status_badge.tooltip_text = "查看本局冒险进度(只读)" status_badge.size_flags_vertical = Control.SIZE_SHRINK_CENTER top.add_child(status_badge) - _title = UiKit.label(TITLE_FORK,UiKit.FS_SUB, true, UiKit.WARM) - _title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - box.add_child(_title) - # 节点图画布(图上点击 = 唯一提交通道——布局决策 2;mouse_filter STOP 接悬停/点击) - _canvas = Control.new() - _canvas.custom_minimum_size = Vector2(620, 240) - _canvas.mouse_filter = Control.MOUSE_FILTER_STOP - _canvas.draw.connect(_draw_graph.bind(_canvas)) - _canvas.gui_input.connect(_on_canvas_input) - box.add_child(_canvas) - # 底部状态栏(合并单条:亮米面板一站读齐=头像+生命+属性+本关加成/流派件——demo ④; - # 道具槽不摆=道具读数无关卡画面成文出口(读快照私域越权); - # _hp_value/_attr_value/_bonus_value/_feat_value 投喂键全部保留) - # 底栏定高纪律(对齐 demo——单条 ~85px 紧凑亮条;任何子件不再能把栏撑爆: - # 特性持有行限宽截断+tooltip 全文) + _map._title = UiKit.label(TITLE_FORK,UiKit.FS_SUB, true, UiKit.WARM) + _map._title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + box.add_child(_map._title) + + +## 节点图画布域(图上点击 = 唯一提交通道——布局决策 2;mouse_filter STOP 接悬停/点击) +func _build_canvas(box: VBoxContainer) -> void: + _map._canvas = Control.new() + _map._canvas.custom_minimum_size = Vector2(620, 240) + _map._canvas.mouse_filter = Control.MOUSE_FILTER_STOP + _map._canvas.draw.connect(_draw_graph.bind(_map._canvas)) + _map._canvas.gui_input.connect(_on_canvas_input) + box.add_child(_map._canvas) + + +## 底部状态栏域(合并单条:亮米面板一站读齐=头像+生命+属性+本关加成/流派件——demo ④; +## 道具槽不摆=道具读数无关卡画面成文出口(读快照私域越权); +## _hp_value/_attr_value/_bonus_value/_feat_value 投喂键全部保留。 +## 底栏定高纪律(对齐 demo——单条 ~85px 紧凑亮条;任何子件不再能把栏撑爆: +## 特性持有行限宽截断+tooltip 全文)) +func _build_status_bar(box: VBoxContainer) -> void: var bottom := PanelContainer.new() bottom.custom_minimum_size = Vector2(0, 85) bottom.size_flags_vertical = Control.SIZE_SHRINK_END @@ -132,9 +146,37 @@ func _init() -> void: var strip := HBoxContainer.new() strip.add_theme_constant_override("separation", 18) bottom.add_child(strip) + strip.add_child(_build_face_rect()) + var life_box := VBoxContainer.new() + strip.add_child(life_box) + life_box.add_child(UiKit.label("生命",UiKit.FS_CAPTION, false, UiKit.INK_HINT)) + _map._hp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) + life_box.add_child(_map._hp_value) + _map._hp_bar = Control.new() + _map._hp_bar.custom_minimum_size = Vector2(130, 7) + _map._hp_bar.draw.connect(_draw_hp_bar.bind(_map._hp_bar)) + life_box.add_child(_map._hp_bar) + strip.add_child(_vdiv()) + var attr_box := VBoxContainer.new() + strip.add_child(attr_box) + attr_box.add_child(UiKit.label("属性",UiKit.FS_CAPTION, false, UiKit.INK_HINT)) + _map._attr_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) + attr_box.add_child(_map._attr_value) + strip.add_child(_vdiv()) + _build_bonus_group(strip) + var spacer := Control.new() + spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL + strip.add_child(spacer) + abandon_btn = UiKit.danger_button("放弃", "", 90.0) # 小号危险钮 ~90×38 + abandon_btn.pressed.connect(_on_abandon_pressed) + abandon_btn.visible = false + strip.add_child(abandon_btn) + + +## 头像域:定高 48px=1×、宽随源图比自适应(源 46×48 → 46×48;纵横异倍消除) +func _build_face_rect() -> TextureRect: var face := TextureRect.new() var face_tex := UiKit.load_tex(UiKit.TEX_HERO_PORTRAIT) - # 定高 48px=1×、宽随源图比自适应(源 46×48 → 46×48;纵横异倍消除) var face_h := 48.0 var face_w := face_h if face_tex != null: @@ -145,50 +187,26 @@ func _init() -> void: face.expand_mode = TextureRect.EXPAND_IGNORE_SIZE face.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED face.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - strip.add_child(face) - var life_box := VBoxContainer.new() - strip.add_child(life_box) - life_box.add_child(UiKit.label("生命",UiKit.FS_CAPTION, false, UiKit.INK_HINT)) - _hp_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) - life_box.add_child(_hp_value) - _hp_bar = Control.new() - _hp_bar.custom_minimum_size = Vector2(130, 7) - _hp_bar.draw.connect(_draw_hp_bar.bind(_hp_bar)) - life_box.add_child(_hp_bar) - strip.add_child(_vdiv()) - var attr_box := VBoxContainer.new() - strip.add_child(attr_box) - attr_box.add_child(UiKit.label("属性",UiKit.FS_CAPTION, false, UiKit.INK_HINT)) - _attr_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) - attr_box.add_child(_attr_value) - strip.add_child(_vdiv()) - # 本关加成框(零加成隐藏——spec 布局决策 4)+ 流派件持有行(组合根投喂) - _bonus_row = HBoxContainer.new() - strip.add_child(_bonus_row) + return face + + +## 本关加成框域(零加成隐藏——spec 布局决策 4)+ 流派件持有行(组合根投喂;限宽单行截断+tooltip 全文) +func _build_bonus_group(strip: HBoxContainer) -> void: + _map._bonus_row = HBoxContainer.new() + strip.add_child(_map._bonus_row) var bonus_box := VBoxContainer.new() - _bonus_row.add_child(bonus_box) + _map._bonus_row.add_child(bonus_box) var bonus_inner := VBoxContainer.new() bonus_box.add_child(bonus_inner) var bonus_cap := UiKit.label("属性值提升",UiKit.FS_CAPTION, false, UiKit.INK_HINT) # 深墨(亮板可读) bonus_inner.add_child(bonus_cap) - _bonus_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) # 淡墨→深墨(同上) - bonus_inner.add_child(_bonus_value) - # 特性持有行(组合根投喂) - # 限宽单行截断+tooltip 全文; - # 墨色 INK_HINT(暖白系对亮板 ≈1:1 不可读——§7.2) - _feat_value = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_HINT) - _feat_value.custom_minimum_size = Vector2(240, 0) - _feat_value.clip_text = true - _feat_value.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - bonus_box.add_child(_feat_value) - var spacer := Control.new() - spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL - strip.add_child(spacer) - abandon_btn = UiKit.danger_button("放弃", "", 90.0) # 小号危险钮 ~90×38 - abandon_btn.pressed.connect(_on_abandon_pressed) - abandon_btn.visible = false - strip.add_child(abandon_btn) - visible = false + _map._bonus_value = UiKit.label("",UiKit.FS_BODY, true, UiKit.INK_TITLE) # 淡墨→深墨(同上) + bonus_inner.add_child(_map._bonus_value) + _map._feat_value = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_HINT) + _map._feat_value.custom_minimum_size = Vector2(240, 0) + _map._feat_value.clip_text = true + _map._feat_value.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + bonus_box.add_child(_map._feat_value) # 竖分隔线(底栏分组线——亮面板上低对比细线) @@ -202,10 +220,10 @@ static func _vdiv() -> ColorRect: func _draw_hp_bar(bar: Control) -> void: # 迷你血条(深底 UiKit.HP_BAR_BACK #28221C + 小人蓝填充——与战斗站桩血条同语言, # 「蓝只说己方」§4.2)。比例 = hero_hp / effective_max_hp - if _runner == null: + if _map._runner == null: return - var max_hp := maxi(1, _runner.effective_max_hp()) - var ratio := clampf(float(_runner.hero_hp) / float(max_hp), 0.0, 1.0) + var max_hp := maxi(1, _map._runner.effective_max_hp()) + var ratio := clampf(float(_map._runner.hero_hp) / float(max_hp), 0.0, 1.0) var full := Rect2(Vector2.ZERO, bar.size) bar.draw_rect(full, COL_HP_BAR_BACK) if ratio > 0.0: @@ -215,8 +233,8 @@ func _draw_hp_bar(bar: Control) -> void: # 放弃入口可见性(组合根在冒险启动时开、默认关——语义对象 = 整局冒险) # 特性持有投影(组合根投喂逐件文案;空文案=行隐藏) func set_feat_holdings(text: String) -> void: - _feat_value.text = text - _feat_value.tooltip_text = text # 截断全量兜底(限宽单行省略——悬停看全文) + _map._feat_value.text = text + _map._feat_value.tooltip_text = text # 截断全量兜底(限宽单行省略——悬停看全文) func set_abandon_available(v: bool) -> void: @@ -226,8 +244,8 @@ func set_abandon_available(v: bool) -> void: # 放弃二次确认(布局决策 3):首击转确认态(文案+危险红脉冲约 3 秒超时回落),二击才提交。 # 单击不提交——ui_probe 战斗中 pressed.emit() 单击路径保持「未受理」语义(战斗中拒绝归组合根)。 func _on_abandon_pressed() -> void: - if not _abandon_timer.is_armed(): - _abandon_timer.arm(CONFIRM_WINDOW_S) + if not _map._abandon_timer.is_armed(): + _map._abandon_timer.arm(CONFIRM_WINDOW_S) abandon_btn.text = "再点一次确认放弃——不可逆" if ArmedTimer.motion_still(): abandon_btn.modulate = Color(1, 1, 1, 0.75) # 减动效静态基色(脉冲层不再驱动) @@ -237,45 +255,45 @@ func _on_abandon_pressed() -> void: func _reset_abandon_confirm() -> void: - _abandon_timer.disarm() + _map._abandon_timer.disarm() abandon_btn.modulate = Color(1, 1, 1, 1) abandon_btn.text = "放弃" # 文案收敛 level-map 4 字上限(确认态转义面见 _on_abandon_pressed) func _process(delta: float) -> void: - if not _abandon_timer.is_armed(): + if not _map._abandon_timer.is_armed(): return - _abandon_timer.tick(delta) - if not _abandon_timer.is_armed(): + _map._abandon_timer.tick(delta) + if not _map._abandon_timer.is_armed(): return # 窗口到点回落(expired 信号已驱动 _reset_abandon_confirm) # 危险红边框脉冲(单段呼吸约 1Hz——闪烁基线 ≤3 次/秒内;减动效 = 静态半透不闪——档判据单点 motion_still) if ArmedTimer.motion_still(): abandon_btn.modulate = Color(1, 1, 1, 0.75) else: - var k := 0.5 + 0.5 * sin(_abandon_timer.elapsed * TAU) + var k := 0.5 + 0.5 * sin(_map._abandon_timer.elapsed * TAU) abandon_btn.modulate = Color(1, 1, 1, 0.65 + 0.35 * k) # 打开选路口(cands = 候选节点引用数组;runner 只读用于整图渲染与状态栏刷新) func open_choices(cands: Array, runner: LevelRunner) -> void: var was_visible := visible # 进入转场判据(就地刷新/模态栈恢复不重放淡入) - _runner = runner + _map._runner = runner _hover_id = -1 _reset_abandon_confirm() var visited: Array[int] = [] if runner != null: visited = runner.visited_ids() if cands.size() > 1: - _title.text = TITLE_FORK + _map._title.text = TITLE_FORK elif visited.is_empty(): - _title.text = TITLE_ENTRY # 入口停点(尚未走出第一步) + _map._title.text = TITLE_ENTRY # 入口停点(尚未走出第一步) else: - _title.text = TITLE_SINGLE + _map._title.text = TITLE_SINGLE _cand_ids = [] for cand in cands: _cand_ids.append(_id_of(cand)) - _fit_canvas(_runner.nodes()) - _apply_level_tint(int(_runner.level_row.get("level_index", 0))) + _fit_canvas(_map._runner.nodes()) + _apply_level_tint(int(_map._runner.level_row.get("level_index", 0))) visible = true if not was_visible: # 独立屏淡入(spec Transitions:约 200ms;alpha 淡入非明度变化——art-bible §2 明度恒定) @@ -302,7 +320,7 @@ func _fit_canvas(nodes: Array) -> void: _content_size = Vector2(LAYER_BASE_X + maxl * LAYER_STEP_X + NODE_HALF.x + 8.0, SLOT_BASE_Y + maxslot * SLOT_STEP_Y + NODE_HALF.y + 24.0) # +24 = 类型文字标签行 _content_size = _content_size.max(Vector2(620, 256)) # 下限(抬高容纳标签行) - _canvas.custom_minimum_size = _content_size + _map._canvas.custom_minimum_size = _content_size # 换层色板渐变挂点(art-bible §2.7 下探过渡切片档——关卡结算确认→下一关→独立屏淡入链路上的 @@ -313,39 +331,39 @@ const BOARD_TINTS := {1: Color(1, 1, 1), 2: Color(0.84, 0.99, 1.28), 3: Color(0. func _apply_level_tint(level_index: int) -> void: - if _noise_rect == null or level_index == _tint_level: + if _map._layer_decor.noise_rect == null or level_index == _map._layer_decor.tint_level: return - _tint_level = level_index + _map._layer_decor.tint_level = level_index var target: Color = BOARD_TINTS.get(level_index, Color(1, 1, 1)) var tw := create_tween() - tw.tween_property(_noise_rect, "modulate", + tw.tween_property(_map._layer_decor.noise_rect, "modulate", Color(target.r, target.g, target.b, UiKit.BACKDROP_TILE_ALPHA), UiKit.motion(0.45)) # 0.14 弱质感档随 tint 保持 # 状态栏投喂(命令式纪律——组合根经 level_map_status_payload 取成品后喂入;零推送信号订阅、零核心态拼装) func apply_status(payload: Dictionary) -> void: - _level_label.text = String(payload["level_label"]) - _gp_value.text = String(payload["gp_text"]) - _hp_value.text = String(payload["hp_text"]) + _map._level_label.text = String(payload["level_label"]) + _map._gp_value.text = String(payload["gp_text"]) + _map._hp_value.text = String(payload["hp_text"]) if bool(payload["hp_bar_redraw"]): - _hp_bar.queue_redraw() - _attr_value.text = String(payload["attr_text"]) - _bonus_value.text = String(payload["bonus_text"]) + _map._hp_bar.queue_redraw() + _map._attr_value.text = String(payload["attr_text"]) + _map._bonus_value.text = String(payload["bonus_text"]) # 特性持有行可见性(与加成行并框——任一非空整框显示;文案归组合根 set_feat_holdings) - _bonus_row.visible = bool(payload["bonus_row_visible"]) or _feat_value.text != "" + _map._bonus_row.visible = bool(payload["bonus_row_visible"]) or _map._feat_value.text != "" # ---- 图上命中(布局决策 2——唯一提交通道;探针 press_candidate 同路径) ---- func _on_canvas_input(ev: InputEvent) -> void: - if _runner == null or not visible: + if _map._runner == null or not visible: return if ev is InputEventMouseMotion: var nid := _candidate_at(ev.position) # gui_input 事件坐标 = 接收控件局部坐标(push_input 同成立) if nid != _hover_id: _hover_id = nid - _canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW - _canvas.queue_redraw() + _map._canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW + _map._canvas.queue_redraw() elif ev is InputEventMouseButton and ev.button_index == MOUSE_BUTTON_LEFT and ev.pressed: var nid2 := _candidate_at(ev.position) if nid2 >= 0: @@ -354,10 +372,10 @@ func _on_canvas_input(ev: InputEvent) -> void: # 候选命中:位置 = 布局公式(与 _draw_graph 同源),矩形 = 节点方块 + 外扩容差;非候选格不响应 func _candidate_at(local_pos: Vector2) -> int: - if _runner == null: + if _map._runner == null: return -1 - var nodes: Array[Variant] = _runner.nodes() - var pos := _layout(nodes, _canvas) + var nodes: Array[Variant] = _map._runner.nodes() + var pos := _layout(nodes, _map._canvas) for nid in _cand_ids: var node := _node_by_id(nid, nodes) if node.is_empty(): @@ -372,7 +390,7 @@ func _candidate_at(local_pos: Vector2) -> int: func _submit_choice(nid: int) -> void: # 独立屏分支:战斗/Boss 节点 = 出屏切回战斗屏;奖励/恢复/商店节点 = # 本屏保持激活——兑付覆盖层由节点事件在本屏域内弹出(模态栈遮挡恢复语义复用),兑付确认后回图刷新 - var node := _node_by_id(nid, _runner.nodes()) + var node := _node_by_id(nid, _map._runner.nodes()) var t := String(node.get("type", "")) if t != "reward" and t != "rest" and t != "shop": visible = false @@ -381,13 +399,13 @@ func _submit_choice(nid: int) -> void: # 探针复用:按候选序号走图上命中同路径(候选节点中心坐标 → 命中检测 → 提交——布局决策 2 探针面改造) func press_candidate(index: int) -> void: - if index < 0 or index >= _cand_ids.size() or _runner == null: + if index < 0 or index >= _cand_ids.size() or _map._runner == null: return - var nodes: Array[Variant] = _runner.nodes() + var nodes: Array[Variant] = _map._runner.nodes() var node := _node_by_id(int(_cand_ids[index]), nodes) if node.is_empty(): return - var pos: Vector2 = _layout(nodes, _canvas)[node] + var pos: Vector2 = _layout(nodes, _map._canvas)[node] var nid := _candidate_at(pos) # 节点中心必命中自身(命中面含节点方块) if nid >= 0: _submit_choice(nid) @@ -395,7 +413,7 @@ func press_candidate(index: int) -> void: func _id_of(node: Dictionary) -> int: # 编号显示经行进器读面(编号空间解析单一出口——单链 = 下标 / 图 = node_depth) - return _runner.node_id_of(node) + return _map._runner.node_id_of(node) # ---- 节点呈现决策面(信息迷雾 v2.1:类型仅在可见集内出示,Boss 标记不入雾)---- @@ -421,20 +439,48 @@ static func node_display(node: Dictionary, nid: int, type_vis: Array, # ---- 整图自绘(程序化占位——分层布局:层 = 入口 BFS 深度,同层垂直排布)---- func _draw_graph(canvas: Control) -> void: - if _runner == null or not visible: + if _map._runner == null or not visible: return - _drawn_disp.clear() # 绘制记录随本帧重建——只反映实际发生的绘制(接线坏的判据面) - var nodes: Array[Variant] = _runner.nodes() - var pos := _layout(nodes, canvas) - var font := get_theme_default_font() # 绘制路径字体单次取用(三处 draw_string 共享——零语义差) - var current_id: int = _runner.current_node_id() - # 当前节点 id(单链=下标 / 图=编号)——visited_ids() 已按编号空间解析 - var visited: Array[Variant] = _runner.visited_ids() - var cand_ids: Array[Variant] = _runner.next_candidate_ids() - var type_vis: Array[int] = _runner.type_visible_ids() # 本停点类型可见集(每帧一取——纯函数零状态) - # 连线(通道)先行画(方块覆盖其上);悬停预承诺 = 所属通道再增亮(加粗) - # 选路停点(候选非空)非候选通道降透明——金岔路从结构网分离(干线共线重叠缺陷的 - # 视觉主因:不同源通道共用同一垂直干线,金边与石色边焊接成「整列可走」假象) + _map._drawn_disp.clear() # 绘制记录随本帧重建——只反映实际发生的绘制(接线坏的判据面) + var ctx := _draw_context(canvas) + _draw_passage_layer(canvas, ctx) + for node in ctx["nodes"]: + _draw_room(canvas, node, ctx) + + +## 绘制上下文束(单次取数——布局/字体/编号空间/可见集,绘制域共享只读面) +func _draw_context(canvas: Control) -> Dictionary: + var nodes: Array[Variant] = _map._runner.nodes() + return { + "nodes": nodes, + "pos": _layout(nodes, canvas), + "font": get_theme_default_font(), # 绘制路径字体单次取用(三处 draw_string 共享——零语义差) + "current_id": _map._runner.current_node_id(), + # 当前节点 id(单链=下标 / 图=编号)——visited_ids() 已按编号空间解析 + "visited": _map._runner.visited_ids(), + "cand_ids": _map._runner.next_candidate_ids(), + "type_vis": _map._runner.type_visible_ids(), # 本停点类型可见集(每帧一取——纯函数零状态) + } + + +## 房间底色域(行进态三色:已到暗金/当前奖励金/未达暗——绘制域与图标域共享) +func _room_color(node: Dictionary, ctx: Dictionary) -> Color: + var nid := _id_of(node) + var col := COL_ROOM_FUTURE + if (ctx["visited"] as Array).has(nid): + col = COL_ROOM_DONE + if nid == ctx["current_id"]: + col = COL_ROOM_CURRENT + return col + + +## 通道层绘制域:连线先行画(方块覆盖其上);悬停预承诺 = 所属通道再增亮(加粗)。 +## 选路停点(候选非空)非候选通道降透明——金岔路从结构网分离(干线共线重叠缺陷的 +## 视觉主因:不同源通道共用同一垂直干线,金边与石色边焊接成「整列可走」假象) +func _draw_passage_layer(canvas: Control, ctx: Dictionary) -> void: + var nodes: Array = ctx["nodes"] + var pos: Dictionary = ctx["pos"] + var cand_ids: Array = ctx["cand_ids"] var dim_passages: bool = not cand_ids.is_empty() for node in nodes: var from: Vector2 = pos[node] @@ -442,61 +488,74 @@ func _draw_graph(canvas: Control) -> void: var target := _node_by_id(tid, nodes) if target.is_empty(): continue - var to: Vector2 = pos[target] - var lit: bool = _id_of(node) == current_id and cand_ids.has(tid) - var hovered: bool = lit and _hover_id == tid + var lit: bool = _id_of(node) == ctx["current_id"] and cand_ids.has(tid) var col := COL_PASSAGE_LIT if lit else (Color(COL_PASSAGE, 0.35) if dim_passages else COL_PASSAGE) - var width := 4.0 if hovered else (3.0 if lit else 2.0) # 2/3/4 三档 - _draw_passage(canvas, from, to, col, width) - # 房间方块 - for node in nodes: - var p: Vector2 = pos[node] - var t := String(node.get("type", "")) - var nid := _id_of(node) - var col := COL_ROOM_FUTURE - if visited.has(nid): - col = COL_ROOM_DONE - if nid == current_id: - col = COL_ROOM_CURRENT - var rect := Rect2(p - NODE_HALF, NODE_HALF * 2.0) - canvas.draw_rect(rect, col) - var disp := node_display(node, nid, type_vis, nid == current_id, cand_ids.has(nid)) - var rec: Dictionary[String, Variant] = {"draw_icon": false, "draw_label": false, - "label_text": "", "boss_border": false} - if bool(disp["boss_border"]): - # Boss 描边 = 紫赤 2px(§4.2 色相独占)+ 外缘 1px 深墨环(灰度冗余通道强制: - # 紫赤灰度 72 对石纹噪底 65 灰度不可辨;灰度/色弱下 Boss 环经深墨环可辨) - canvas.draw_rect(Rect2(rect.position - Vector2(3, 3), rect.size + Vector2(6, 6)), UiKit.INK_ROUTE, false, 1.0) - canvas.draw_rect(rect, COL_BOSS_BORDER, false, 2.0) - rec["boss_border"] = true - elif cand_ids.has(nid): - # 悬停预承诺 = 亮金描边加粗(4px),常态候选 = 亮金描边 2px(spec States 悬停预承诺行) - canvas.draw_rect(rect, COL_PASSAGE_LIT, false, 4.0 if _hover_id == nid else 2.0) - # 节点图标(20px 烘焙 1:1 消费;双色墨制=乘法调制——当前金底深墨,暗底系 WARM 原色; - # 贴图缺位 → 字形回退=贴图在位字形退役惯例) - if bool(disp["draw_icon"]): - rec["draw_icon"] = true - var icon: Texture2D = _node_icons.get(t) - if icon != null: - var tint := UiKit.ICON_TINT_ON_GOLD if col == COL_ROOM_CURRENT else Color(1.0, 1.0, 1.0) - canvas.draw_texture_rect(icon, Rect2(p - Vector2(NODE_ICON_PX, NODE_ICON_PX) / 2.0, - Vector2(NODE_ICON_PX, NODE_ICON_PX)), false, tint) - else: - var label := String(NODE_LABELS.get(t, "?")) - canvas.draw_string(font, p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE) - # 编号(右上角——与图标占位区零重叠;双色墨随态) - var num_ink := UiKit.INK_ROUTE if col == COL_ROOM_CURRENT else UiKit.WARM - canvas.draw_string(font, Vector2(rect.end.x - 16.0, rect.position.y + 8.0), - str(nid), HORIZONTAL_ALIGNMENT_RIGHT, 14, 10, num_ink) - # 类型文字标签(demo ④:节点下方 11px;当前位=「当前位置」金、候选=类型名+「 · 可选」烛光黄、 - # 其余=类型名暖米弱化;文字+图标+形状三通道) - if bool(disp["draw_label"]): - rec["draw_label"] = true - rec["label_text"] = String(disp["label_text"]) - var type_ink := COL_ROOM_CURRENT if nid == current_id else (UiKit.CANDLE if cand_ids.has(nid) else UiKit.WARM_FAINT) - canvas.draw_string(font, Vector2(p.x - 44.0, rect.end.y + 15.0), - String(disp["label_text"]), HORIZONTAL_ALIGNMENT_CENTER, 88, 11, type_ink) - _drawn_disp[nid] = rec + var width := 4.0 if lit and _hover_id == tid else (3.0 if lit else 2.0) # 2/3/4 三档 + _draw_passage(canvas, from, pos[target], col, width) + + +## 房间方块域:底色随行进态+Boss/候选描边+图标/字形+编号+类型文字标签 +func _draw_room(canvas: Control, node: Dictionary, ctx: Dictionary) -> void: + var pos: Dictionary = ctx["pos"] + var p: Vector2 = pos[node] + var t := String(node.get("type", "")) + var nid := _id_of(node) + var col := _room_color(node, ctx) + var rect := Rect2(p - NODE_HALF, NODE_HALF * 2.0) + canvas.draw_rect(rect, col) + var disp := node_display(node, nid, ctx["type_vis"], nid == ctx["current_id"], (ctx["cand_ids"] as Array).has(nid)) + var rec: Dictionary[String, Variant] = {"draw_icon": false, "draw_label": false, + "label_text": "", "boss_border": false} + if bool(disp["boss_border"]): + # Boss 描边 = 紫赤 2px(§4.2 色相独占)+ 外缘 1px 深墨环(灰度冗余通道强制: + # 紫赤灰度 72 对石纹噪底 65 灰度不可辨;灰度/色弱下 Boss 环经深墨环可辨) + canvas.draw_rect(Rect2(rect.position - Vector2(3, 3), rect.size + Vector2(6, 6)), UiKit.INK_ROUTE, false, 1.0) + canvas.draw_rect(rect, COL_BOSS_BORDER, false, 2.0) + rec["boss_border"] = true + elif (ctx["cand_ids"] as Array).has(nid): + # 悬停预承诺 = 亮金描边加粗(4px),常态候选 = 亮金描边 2px(spec States 悬停预承诺行) + canvas.draw_rect(rect, COL_PASSAGE_LIT, false, 4.0 if _hover_id == nid else 2.0) + if _draw_room_icon(canvas, node, disp, ctx): + rec["draw_icon"] = true + # 编号(右上角——与图标占位区零重叠;双色墨随态) + var num_ink := UiKit.INK_ROUTE if col == COL_ROOM_CURRENT else UiKit.WARM + canvas.draw_string(ctx["font"], Vector2(rect.end.x - 16.0, rect.position.y + 8.0), + str(nid), HORIZONTAL_ALIGNMENT_RIGHT, 14, 10, num_ink) + var label_rec := _draw_room_label(canvas, rect, disp, nid, ctx) + rec["draw_label"] = bool(label_rec.get("draw_label", false)) + rec["label_text"] = String(label_rec.get("label_text", "")) + _map._drawn_disp[nid] = rec + + +## 节点图标域(20px 烘焙 1:1 消费;双色墨制=乘法调制——当前金底深墨,暗底系 WARM 原色; +## 贴图缺位 → 字形回退=贴图在位字形退役惯例) +func _draw_room_icon(canvas: Control, node: Dictionary, disp: Dictionary, ctx: Dictionary) -> bool: + if not bool(disp["draw_icon"]): + return false + var p: Vector2 = ctx["pos"][node] + var col := _room_color(node, ctx) + var t := String(node.get("type", "")) + var icon: Texture2D = _map._layer_decor.node_icons.get(t) + if icon != null: + var tint := UiKit.ICON_TINT_ON_GOLD if col == COL_ROOM_CURRENT else Color(1.0, 1.0, 1.0) + canvas.draw_texture_rect(icon, Rect2(p - Vector2(NODE_ICON_PX, NODE_ICON_PX) / 2.0, + Vector2(NODE_ICON_PX, NODE_ICON_PX)), false, tint) + else: + var label := String(NODE_LABELS.get(t, "?")) + canvas.draw_string(ctx["font"], p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE) + return true + + +## 类型文字标签域(demo ④:节点下方 11px;当前位=「当前位置」金、候选=类型名+「 · 可选」烛光黄、 +## 其余=类型名暖米弱化;文字+图标+形状三通道) +func _draw_room_label(canvas: Control, rect: Rect2, disp: Dictionary, nid: int, ctx: Dictionary) -> Dictionary: + if not bool(disp["draw_label"]): + return {} + var p: Vector2 = rect.position + NODE_HALF # 节点中心反推(方块原点=中心−半宽高) + var type_ink := COL_ROOM_CURRENT if nid == ctx["current_id"] else (UiKit.CANDLE if (ctx["cand_ids"] as Array).has(nid) else UiKit.WARM_FAINT) + canvas.draw_string(ctx["font"], Vector2(p.x - 44.0, rect.end.y + 15.0), + String(disp["label_text"]), HORIZONTAL_ALIGNMENT_CENTER, 88, 11, type_ink) + return {"draw_label": true, "label_text": String(disp["label_text"])} func _layout(nodes: Array, canvas: Control) -> Dictionary: @@ -520,20 +579,20 @@ func _layout(nodes: Array, canvas: Control) -> Dictionary: # ---- 探针只读出口(形态仿 fx_probe 先例:只读快照) ---- -# 选路布局只读快照(node → 局部位置;画布固定取现行 _canvas,调用侧零私有域触达) +# 选路布局只读快照(node → 局部位置;画布固定取现行 _map._canvas,调用侧零私有域触达) func layout_probe(nodes: Array) -> Dictionary: - return _layout(nodes, _canvas) + return _layout(nodes, _map._canvas) # 路由画布只读快照(origin=全局原点;min_size=最小拟合尺寸——画布拟合断言消费面) func canvas_probe() -> Dictionary: - return {"origin": _canvas.get_global_position(), "min_size": _canvas.custom_minimum_size} + return {"origin": _map._canvas.get_global_position(), "min_size": _map._canvas.custom_minimum_size} # 类型可见性绘制只读记录(id → 本帧实际绘制事实:图标/文字/描边/文案;_draw_graph 在实际绘制点写入—— # 接线层断言消费点:绕开决策面直接画,这里就露出「雾下也画了」) func display_probe() -> Dictionary: - return _drawn_disp + return _map._drawn_disp # 分层(layout/fit 共享件):layer = 入口 BFS 深度;同层 slot 垂直排布;未达节点兜底排末层 @@ -577,7 +636,7 @@ func _entry(nodes: Array) -> Dictionary: func _out_ids(node: Dictionary, _nodes: Array) -> Array: - return _runner.node_out_ids(node) + return _map._runner.node_out_ids(node) func _node_by_id(nid: int, nodes: Array) -> Dictionary: diff --git a/game/presentation/views/node_overlay.gd b/game/presentation/views/node_overlay.gd index e82f854c..cabfcdf5 100644 --- a/game/presentation/views/node_overlay.gd +++ b/game/presentation/views/node_overlay.gd @@ -11,7 +11,7 @@ var _btn: Button func _init() -> void: # M5(UI 落码批):rest 兑付面板 plate9 石板化+暖金头带字(demo ④休息弹层;暗字上亮板 §7.2) - var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, Color(0, 0, 0, 0.6), Vector2.ZERO, 14, true, + var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, Color(0, 0, 0, 0.6), Vector2.ZERO, 14, UiKit.panel_skinned(Vector4(26, 18, 26, 18))) var box: VBoxContainer = shell.box _label = UiKit.label("",UiKit.FS_SUB, true, UiKit.INK_TITLE) diff --git a/game/presentation/views/settings_overlay.gd b/game/presentation/views/settings_overlay.gd index adfe0d12..8850af4e 100644 --- a/game/presentation/views/settings_overlay.gd +++ b/game/presentation/views/settings_overlay.gd @@ -34,10 +34,18 @@ func _build() -> void: # 减动效与结算演出动画=同一设置,一行承载。 # 两列对齐(label 列定宽 140——demo .setrow label/控制两列)+ # 后果说明内联随行(demo .why 同排)+返回钮紧凑化(demo .btn9 紧凑)。 - var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, UiKit.BG_BRIGHT, Vector2(560, 0), 8, true, + var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, UiKit.BG_BRIGHT, Vector2(560, 0), 8, UiKit.panel_skinned(Vector4(20, 16, 20, 16))) var box: VBoxContainer = shell.box + _build_head(box) + _build_volume_row(box) + _build_fullscreen_row(box) + _build_motion_row(box) + _build_close_row(box) + +## 头部域(设置项零增减——只有主音量;减动效与结算演出动画=同一设置,一行承载) +func _build_head(box: VBoxContainer) -> void: var title := UiKit.label("设置",UiKit.FS_TITLE, true, UiKit.INK_TITLE) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(title) @@ -46,7 +54,9 @@ func _build() -> void: box.add_child(hint) box.add_child(HSeparator.new()) - # 音量(master bus 实时生效——入口归设置、生效口径归 音频.md) + +## 音量行(master bus 实时生效——入口归设置、生效口径归 音频.md) +func _build_volume_row(box: VBoxContainer) -> void: var vol_row := HBoxContainer.new() vol_row.add_theme_constant_override("separation", 14) box.add_child(vol_row) @@ -66,38 +76,44 @@ func _build() -> void: _volume_val.custom_minimum_size = Vector2(60, 0) vol_row.add_child(_volume_val) - # 全屏(拨杆形——SwitchToggle;状态字随行) + +## 全屏行(拨杆形——SwitchToggle;状态字随行) +func _build_fullscreen_row(box: VBoxContainer) -> void: var fs_row := HBoxContainer.new() fs_row.add_theme_constant_override("separation", 14) box.add_child(fs_row) var fs_lab := UiKit.label("全屏",UiKit.FS_BODY, false, UiKit.INK_LABEL) fs_lab.custom_minimum_size = Vector2(140, 0) # 同上 fs_row.add_child(fs_lab) - _fs_btn = UiKit.switch_toggle(false) + _fs_btn = UiKit._switch_toggle(false) _fs_btn.toggled.connect(_on_fullscreen_toggled) fs_row.add_child(_fs_btn) _fs_st = UiKit.label("关",UiKit.FS_HINT, true, UiKit.INK_LABEL) fs_row.add_child(_fs_st) - # 结算演出动画(=减动效实称;拨杆 on=完整,off=快速时长减半。 - # reduced_motion 单设置同驱 UiKit.motion_scale 与结算演播——demo「减动效」行与 - # 本行系同一设置,不另立行(拆两行=一设置双行重复) + +## 结算演出动画行(=减动效实称;拨杆 on=完整,off=快速时长减半。 +## reduced_motion 单设置同驱 UiKit.motion_scale 与结算演播——demo「减动效」行与 +## 本行系同一设置,不另立行(拆两行=一设置双行重复)) +func _build_motion_row(box: VBoxContainer) -> void: var motion_row := HBoxContainer.new() motion_row.add_theme_constant_override("separation", 14) box.add_child(motion_row) var motion_lab := UiKit.label("结算演出动画",UiKit.FS_BODY, false, UiKit.INK_LABEL) motion_lab.custom_minimum_size = Vector2(140, 0) # 同上 motion_row.add_child(motion_lab) - _motion_btn = UiKit.switch_toggle(true) + _motion_btn = UiKit._switch_toggle(true) _motion_btn.toggled.connect(_on_motion_toggled) motion_row.add_child(_motion_btn) _motion_st = UiKit.label("完整",UiKit.FS_HINT, true, UiKit.INK_LABEL) motion_row.add_child(_motion_st) var why := UiKit.label("快速 = 时长减半(运动敏感 / 演示用)",UiKit.FS_CAPTION, false, UiKit.INK_HINT) motion_row.add_child(why) # 内联随行(demo .why 同排) - box.add_child(HSeparator.new()) - # 退出游戏归开始菜单——设置面不再承载(退出只在一级菜单出现) + + +## 返回钮域(退出游戏归开始菜单——设置面不再承载〔退出只在一级菜单出现〕) +func _build_close_row(box: VBoxContainer) -> void: var close_row := HBoxContainer.new() close_row.alignment = BoxContainer.ALIGNMENT_CENTER box.add_child(close_row) diff --git a/game/presentation/views/settlement_review_overlay.gd b/game/presentation/views/settlement_review_overlay.gd index 7cb886f5..1cdd4b43 100644 --- a/game/presentation/views/settlement_review_overlay.gd +++ b/game/presentation/views/settlement_review_overlay.gd @@ -10,7 +10,7 @@ var _log: RichTextLabel func _init() -> void: - var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.62), Vector2(760, 560), 6, true) + var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.62), Vector2(760, 560), 6, UiKit.PANEL_DEFAULT) (shell.dim as ColorRect).gui_input.connect(_on_dim_gui_input) var box: VBoxContainer = shell.box box.add_child(UiKit.label("结算明细回看(完整流水 · 逐行归因)",UiKit.FS_SUB, true)) diff --git a/game/presentation/views/shop_overlay_view.gd b/game/presentation/views/shop_overlay_view.gd index 2db37149..8ed5458e 100644 --- a/game/presentation/views/shop_overlay_view.gd +++ b/game/presentation/views/shop_overlay_view.gd @@ -211,7 +211,7 @@ func _build_shelf_card(holder: Control, c: Dictionary) -> Dictionary: body = "%s %+d(本冒险)" % [String(KIND_LABELS.get(kind, kind)), int(c.get("value", 0))] var card := UiKit.bcard(String(c.get("display_name", kind)), body, UiKit.tg_band(kl), icon_key, # 类型色带单点(N4c/Q4 甲——货架与三选一同源判定,原固定默认带) - "%d ◈" % int(c.get("price", 0)), String(KIND_LABELS.get(kind, kind))) + {"price": "%d ◈" % int(c.get("price", 0)), "footnote": String(KIND_LABELS.get(kind, kind))}) var btn := UiKit.button("购买") btn.pressed.connect(_emit_shelf_buy.bind(kind)) card.add_footer(btn) diff --git a/game/presentation/views/standee_body.gd b/game/presentation/views/standee_body.gd index d946c735..ed2b1f7e 100644 --- a/game/presentation/views/standee_body.gd +++ b/game/presentation/views/standee_body.gd @@ -2,6 +2,9 @@ ## 站桩本体:B 区主角/敌人无框立绘+底座+血条+顺位/意图徽标的世界精灵(Node2D)。 ## 站桩形态为定案——禁回卡片式;立绘素材缺位回退字形占位;击杀呈现禁瞬消(灰化塌陷)。 ## 无 class_name——经 preload 引用;壳层坐标换算单点在 BattleStageView.world_to_shell()。 +## 内部状态件(u4 子域拆件——契约 9 职责语句「站桩演出状态」):单一变化原因=站桩呈现与演出, +## 成员共同表达站桩的行进态/演出计数/资产挂点。外部直访冻结成员(box/font/slot_row/has_world_hp_bar) +## 与源码锚承载成员(_intent_charge——theme 源锚句所在行逐字保留)住宿主。 extends Node2D const CHARGE_SKILL_IDS := ["heavy"] @@ -12,50 +15,68 @@ const ARMOR_ICONS := {"light": "◇", "ward": "◆", "heavy": "▣"} # ≥8px 硬余量 + 字形下探预算 ⇒ 12;取值不属色板,不进 ui_kit) const STANDEE_FOOT_MARGIN := 12.0 -var box := Vector2(150, 176) # 站桩盒尺寸(实际高随壳层 row 布局注入) -var font: Font = null # 世界字体槽位(null = ThemeDB.fallback_font——world.world_font 注入) -var slot_row: StatusSlotRow # 状态效果唯一展示面 = 壳层伴随件(挂 BattleStageView 壳层,本体进世界) +const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值) +const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍) -var _kind := "enemy" -var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位) -var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据) -# 主角三态切换:state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理 -# (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。 -var _hero_tex := {} -enum HeroState { IDLE, RESET, ATTACK, HIT } # 主角三态+复位哨兵(外部 String 契约=register/set 形参零变化) -const _HS_NAME := {HeroState.IDLE: "idle", HeroState.RESET: "reset", HeroState.ATTACK: "attack", HeroState.HIT: "hit"} -const _HS_VALUE := {"idle": HeroState.IDLE, "reset": HeroState.RESET, "attack": HeroState.ATTACK, "hit": HeroState.HIT} -var _hero_state: int = HeroState.IDLE -var _state_revert_tw: Tween -var _order := 1 -var _intent_glyph := "" -var _intent_col := Color.WHITE -var _intent_charge := false # 蓄力档(pid ∈ CHARGE_SKILL_IDS——意图三档 ③ 分支判据) -var _intent_visible := false -var _intent_scale := 1.0: # 意图弹入动画参数 - set(v): - _intent_scale = v - queue_redraw() -var _glyph_rot := 0.0: # 立绘前倾角(贴图态无此动画) - set(v): - _glyph_rot = v - queue_redraw() -var _display_name := "" -var _enemy_id := "" -var _armor := "light" -var _dead := false -var _hp_max := 1.0 -var _hp_cur := 0 # 血量数字显示面(HUD框架 验收「血量数字可读」) -var hp_value := 1.0: # 血条值 0..1(直角递减——tween_property 目标,逐行刷新重启不累积) - set(v): - hp_value = clampf(v, 0.0, 1.0) - queue_redraw() -var _hp_tween: Tween -var _hp_flash_tw: Tween + +## 站桩演出状态(全部内部态单点——含 setter 触发宿主重绘语义,_owner 回引保持逐字) +class _StandeeState extends RefCounted: + var _owner: Node2D = null + var _kind := "enemy" + var _portrait_sprite: Sprite2D = null # 资产挂点(null = 未落位,字形在位) + var _portrait_tex: Texture2D = null # 落位纹理引用(set_portrait_texture 装载;字形回退判据) + # 主角三态切换:state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理 + # (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。 + var _hero_tex := {} + var _hero_state: int = 0 + var _state_revert_tw: Tween + var _order := 1 + var _intent_glyph := "" + var _intent_col := Color.WHITE + var _intent_visible := false + var _intent_scale := 1.0: # 意图弹入动画参数 + set(v): + _intent_scale = v + _owner.queue_redraw() + var _glyph_rot := 0.0: # 立绘前倾角(贴图态无此动画) + set(v): + _glyph_rot = v + _owner.queue_redraw() + var _display_name := "" + var _enemy_id := "" + var _armor := "light" + var _dead := false + var _hp_max := 1.0 + var _hp_cur := 0 # 血量数字显示面(HUD框架 验收「血量数字可读」) + var hp_value := 1.0: # 血条值 0..1(直角递减——tween_property 目标,逐行刷新重启不累积) + set(v): + hp_value = clampf(v, 0.0, 1.0) + _owner.queue_redraw() + var _hp_tween: Tween + var _hp_flash_tw: Tween + var _death_shards := 0 # 探针出口计数(碎块节点回收后仍保留读数) + var _hit_sparks := 0 # 探针出口计数(命中火花累计) + var _flash_count := 0 # 探针出口计数(受击闪白触发次数——主角路由断言面) + + +var st := _StandeeState.new() +var box := Vector2(150, 176) # 站桩盒尺寸(实际高随壳层 row 布局注入;外部直访冻结) +var font: Font = null # 世界字体槽位(null = ThemeDB.fallback_font——world.world_font 注入;外部直访冻结) +var slot_row: StatusSlotRow # 状态效果唯一展示面 = 壳层伴随件(挂 BattleStageView 壳层,本体进世界;外部直访冻结) +var _intent_charge := false # 蓄力档(pid ∈ CHARGE_SKILL_IDS——意图三档 ③ 分支判据;源码锚承载成员住宿主) +# 血条存在性探针出口(世界直角血条常在;外部直访冻结) +var has_world_hp_bar := true func _init(kind: String = "enemy") -> void: - _kind = kind + st._owner = self + st._hero_state = HeroState.IDLE + st._kind = kind + + +enum HeroState { IDLE, RESET, ATTACK, HIT } # 主角三态+复位哨兵(外部 String 契约=register/set 形参零变化) +const _HS_NAME := {HeroState.IDLE: "idle", HeroState.RESET: "reset", HeroState.ATTACK: "attack", HeroState.HIT: "hit"} +const _HS_VALUE := {"idle": HeroState.IDLE, "reset": HeroState.RESET, "attack": HeroState.ATTACK, "hit": HeroState.HIT} func _efont() -> Font: @@ -63,26 +84,22 @@ func _efont() -> Font: func display_name() -> String: - return _display_name + return st._display_name # 敌单位身份出口(d12 站桩解析主键——同名敌按 id 精确匹配;主角站桩恒空串) func enemy_id() -> String: - return _enemy_id - - -# 血条存在性探针出口(世界直角血条常在) -var has_world_hp_bar := true + return st._enemy_id # 意图徽标探针出口 func intent_probe() -> Dictionary: - return {"visible": _intent_visible, "text": _intent_glyph} + return {"visible": st._intent_visible, "text": st._intent_glyph} # 名字行探针出口 func name_line_probe() -> Dictionary: - return {"name": _display_name, "armor_icon": String(ARMOR_ICONS.get(_armor, "◇"))} + return {"name": st._display_name, "armor_icon": String(ARMOR_ICONS.get(st._armor, "◇"))} # ---- setter 面(presenter/组合根零改排) ---- @@ -90,35 +107,34 @@ func name_line_probe() -> Dictionary: # data = 战斗结算 presentation_snapshot enemies[] 投影:{order, display_name, hp, max_hp, enemy_id, # pending_skill/pending_type/pending_name, effects[](敌方 side 增益——各立绘同值呈现)} func set_data(d: Dictionary) -> void: - _display_name = String(d.get("display_name", "敌人")) - _enemy_id = String(d.get("enemy_id", "")) - _armor = String(d.get("armor_type", "light")) - _hp_max = float(maxi(1, int(d.get("max_hp", 1)))) + st._display_name = String(d.get("display_name", "敌人")) + st._enemy_id = String(d.get("enemy_id", "")) + st._armor = String(d.get("armor_type", "light")) + st._hp_max = float(maxi(1, int(d.get("max_hp", 1)))) _set_hp(int(d.get("hp", 0))) _set_order(int(d.get("order", 1))) set_pending(String(d.get("pending_type", "")), String(d.get("pending_skill", ""))) set_effects(d.get("effects", [])) - _refresh_portrait_glyph() - if _dead and int(d.get("hp", 0)) > 0: - _revive() queue_redraw() + if st._dead and int(d.get("hp", 0)) > 0: + _revive() func set_hero(hp: int, max_hp: int) -> void: - _hp_max = float(maxi(1, max_hp)) + st._hp_max = float(maxi(1, max_hp)) _set_hp(hp) - if _dead and hp > 0: + if st._dead and hp > 0: _revive() # 新战斗开场 HP 回正——主角死亡残位随之复活(敌侧 set_data 同款语义) # 血条短 tween 递减(直角递减占位);逐行刷新时重启 tween 不累积 func _set_hp(hp: int) -> void: - _hp_cur = clampi(hp, 0, int(_hp_max)) - var target := float(_hp_cur) / _hp_max - if _hp_tween != null and _hp_tween.is_valid(): - _hp_tween.kill() - _hp_tween = create_tween() - _hp_tween.tween_property(self, "hp_value", target, UiKit.motion(0.15)) + st._hp_cur = clampi(hp, 0, int(st._hp_max)) + var target := float(st._hp_cur) / st._hp_max + if st._hp_tween != null and st._hp_tween.is_valid(): + st._hp_tween.kill() + st._hp_tween = create_tween() + st._hp_tween.tween_property(self, "st.hp_value", target, UiKit.motion(0.15)) # 主角/敌方共用——状态效果唯一展示面(壳层伴随件转发;空态 = 图标行隐藏不占位) @@ -150,21 +166,21 @@ func set_effects_cleared_feedback() -> void: func _set_order(i: int) -> void: # 顺位徽标 = 行主序生成序(固定顺位——敌人系统 顺位标记义务) - _order = i + st._order = i queue_redraw() # 摇定动作渲染(三档可见性):pending_type = 战斗结算 快照投影(basic/damage/buff/debuff; # "" = 未摇定/死亡残位)。不显示技能名——类型级图标 + 一次性动作动画。 func set_pending(ptype: String, pid: String) -> void: - if _dead: + if st._dead: _intent_charge = false - _intent_visible = false + st._intent_visible = false queue_redraw() return if ptype == "" or ptype == "basic": _intent_charge = false - _intent_visible = false # 普通攻击零表示(完全不可预测——§2.4) + st._intent_visible = false # 普通攻击零表示(完全不可预测——§2.4) queue_redraw() return var glyph := "⚔" # 攻击系(damage) @@ -178,9 +194,9 @@ func set_pending(ptype: String, pid: String) -> void: _intent_charge = pid in CHARGE_SKILL_IDS if _intent_charge: col = UiKit.GOLD_INK # 重击类蓄力档(深金墨——浅垫牌上 ≥4.5:1) - _intent_glyph = glyph - _intent_col = col - _intent_visible = true + st._intent_glyph = glyph + st._intent_col = col + st._intent_visible = true _play_intent_motion() @@ -189,17 +205,17 @@ func set_pending(ptype: String, pid: String) -> void: # 三档运动签名互异,灰度/剪影层可辨)。摇定时一次性短动画,随后静帧保持。 func _play_intent_motion() -> void: var tw := create_tween() - tw.tween_property(self, "_intent_scale", 1.6, UiKit.motion(0.10)) - tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.16)) - tw.parallel().tween_property(self, "_glyph_rot", 4.0, UiKit.motion(0.10)) - tw.tween_property(self, "_glyph_rot", 0.0, UiKit.motion(0.14)) + tw.tween_property(self, "st._intent_scale", 1.6, UiKit.motion(0.10)) + tw.tween_property(self, "st._intent_scale", 1.0, UiKit.motion(0.16)) + tw.parallel().tween_property(self, "st._glyph_rot", 4.0, UiKit.motion(0.10)) + tw.tween_property(self, "st._glyph_rot", 0.0, UiKit.motion(0.14)) if _intent_charge: - tw.tween_property(self, "_intent_scale", 1.22, UiKit.motion(0.28)).set_trans(Tween.TRANS_SINE) # ③蓄力缓推 + tw.tween_property(self, "st._intent_scale", 1.22, UiKit.motion(0.28)).set_trans(Tween.TRANS_SINE) # ③蓄力缓推 tw.tween_interval(UiKit.motion(0.40)) # 定格微顿 - tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.18)) + tw.tween_property(self, "st._intent_scale", 1.0, UiKit.motion(0.18)) else: - tw.tween_property(self, "_intent_scale", 1.26, UiKit.motion(0.11)) # ②二次弹跳(高举) - tw.tween_property(self, "_intent_scale", 1.0, UiKit.motion(0.13)) + tw.tween_property(self, "st._intent_scale", 1.26, UiKit.motion(0.11)) # ②二次弹跳(高举) + tw.tween_property(self, "st._intent_scale", 1.0, UiKit.motion(0.13)) # 资产挂点:tex 落位后字形隐退、纹理立绘顶位——零代码换装契约;tex=null 回退字形。 @@ -207,45 +223,45 @@ func _play_intent_motion() -> void: # (圣经 §8.2 禁非整数缩放)——源换代时随源重算。 func set_portrait_texture(tex: Texture2D, flip: bool = false) -> void: if tex == null: - if _portrait_sprite != null: - _portrait_sprite.visible = false + if st._portrait_sprite != null: + st._portrait_sprite.visible = false queue_redraw() return - _portrait_tex = tex - if _portrait_sprite == null: - _portrait_sprite = Sprite2D.new() - _portrait_sprite.centered = false - _portrait_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊 - add_child(_portrait_sprite) - _portrait_sprite.texture = tex - _portrait_sprite.flip_h = flip - _portrait_sprite.visible = true + st._portrait_tex = tex + if st._portrait_sprite == null: + st._portrait_sprite = Sprite2D.new() + st._portrait_sprite.centered = false + st._portrait_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST # 像素帧放大防糊 + add_child(st._portrait_sprite) + st._portrait_sprite.texture = tex + st._portrait_sprite.flip_h = flip + st._portrait_sprite.visible = true _place_portrait() func _place_portrait() -> void: # 立绘盒 = (0,44)-(box.x, box.y-36);48 源 ×2 整数档居中 - if _portrait_sprite == null or _portrait_tex == null: + if st._portrait_sprite == null or st._portrait_tex == null: return var box_h := box.y - 44.0 - 36.0 - var w := _portrait_tex.get_width() * 2.0 - var h := _portrait_tex.get_height() * 2.0 - _portrait_sprite.scale = Vector2(2.0, 2.0) # 整数 2×(C5) - _portrait_sprite.position = Vector2((box.x - w) * 0.5, 44.0 + (box_h - h) * 0.5) + var w := st._portrait_tex.get_width() * 2.0 + var h := st._portrait_tex.get_height() * 2.0 + st._portrait_sprite.scale = Vector2(2.0, 2.0) # 整数 2×(C5) + st._portrait_sprite.position = Vector2((box.x - w) * 0.5, 44.0 + (box_h - h) * 0.5) func mark_dead() -> void: - if _dead: + if st._dead: return - _dead = true + st._dead = true modulate = Color(0.62, 0.62, 0.62, 0.9) var tw := create_tween() tw.tween_property(self, "rotation_degrees", 2.5, UiKit.motion(0.12)) tw.tween_property(self, "rotation_degrees", -1.5, UiKit.motion(0.1)) tw.tween_property(self, "rotation_degrees", 0.0, UiKit.motion(0.08)) _spawn_death_shards() # 碎裂退场(§4.6.4 语义硬规则:灰化+碎裂禁瞬间消失;§8.4 v2 冻结 5 块 0.45s) - _refresh_portrait_glyph() - _intent_visible = false # 死亡残位意图退位 + _intent_charge = false + st._intent_visible = false # 死亡残位意图退位 _set_hp(0) if slot_row != null: slot_row.visible = false # 状态行随 mark_dead 同拍隐藏 @@ -253,37 +269,36 @@ func mark_dead() -> void: # ---- 演出密度(程序化 tween,零新帧资产) ---- -const DEATH_SHARD_N := 5 # 碎块数(§8.4 冻结值) -const HIT_SPARK_N := 6 # 命中火花数(打击瞬间可读性一拍) -var _death_shards := 0 # 探针出口计数(碎块节点回收后仍保留读数) -var _hit_sparks := 0 # 探针出口计数(命中火花累计) -var _flash_count := 0 # 探针出口计数(受击闪白触发次数——主角路由断言面) - # 死亡碎块弹飞:立绘盒中心五块碎石抛物线散落(程序化色块——确定性散布,禁全局随机) func _spawn_death_shards() -> void: - _death_shards = DEATH_SHARD_N + st._death_shards = DEATH_SHARD_N var center := _portrait_rect().get_center() for i in DEATH_SHARD_N: - var shard := ColorRect.new() - shard.color = UiKit.STONE if i % 2 == 0 else UiKit.STONE.darkened(0.2) - shard.size = Vector2(7, 5) + Vector2(2, 0) * float(i % 2) - shard.mouse_filter = Control.MOUSE_FILTER_IGNORE - shard.position = center + Vector2(float(i % 3) - 1.0, float(i / 3) - 0.5) * 6.0 - shard.rotation = float(i) * 0.7 - add_child(shard) - var dir := Vector2((float(i % 3) - 1.0) * 30.0, -36.0 - 6.0 * float(i / 3)) - var peak := shard.position + dir - var stw := shard.create_tween() - stw.tween_property(shard, "position", peak, UiKit.motion(0.18)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) - stw.tween_property(shard, "position", peak + Vector2(dir.x * 0.4, 52.0), UiKit.motion(0.27)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) - stw.parallel().tween_property(shard, "modulate:a", 0.0, UiKit.motion(0.18)).set_delay(UiKit.motion(0.27)) - stw.tween_callback(shard.queue_free) + _spawn_death_shard(i, center) + + +## 单碎块域(循环体提函数——弹道两段+渐隐+回收) +func _spawn_death_shard(i: int, center: Vector2) -> void: + var shard := ColorRect.new() + shard.color = UiKit.STONE if i % 2 == 0 else UiKit.STONE.darkened(0.2) + shard.size = Vector2(7, 5) + Vector2(2, 0) * float(i % 2) + shard.mouse_filter = Control.MOUSE_FILTER_IGNORE + shard.position = center + Vector2(float(i % 3) - 1.0, float(i / 3) - 0.5) * 6.0 + shard.rotation = float(i) * 0.7 + add_child(shard) + var dir := Vector2((float(i % 3) - 1.0) * 30.0, -36.0 - 6.0 * float(i / 3)) + var peak := shard.position + dir + var stw := shard.create_tween() + stw.tween_property(shard, "position", peak, UiKit.motion(0.18)).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) + stw.tween_property(shard, "position", peak + Vector2(dir.x * 0.4, 52.0), UiKit.motion(0.27)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) + stw.parallel().tween_property(shard, "modulate:a", 0.0, UiKit.motion(0.18)).set_delay(UiKit.motion(0.27)) + stw.tween_callback(shard.queue_free) # 演出探针出口(fx_probe——死亡碎裂/命中火花/闪白计数断言面) func fx_probe() -> Dictionary: - return {"death_shards": _death_shards, "hit_sparks": _hit_sparks, "flash_count": _flash_count} + return {"death_shards": st._death_shards, "hit_sparks": st._hit_sparks, "flash_count": st._flash_count} # 命中瞬间打击特效:立绘盒中心六枚短火花放射(程序化色块,确定性方向 @@ -292,65 +307,65 @@ func fx_probe() -> Dictionary: func play_hit_impact() -> void: var center := _portrait_rect().get_center() for i in HIT_SPARK_N: - _hit_sparks += 1 - var spark := ColorRect.new() - spark.color = UiKit.GOLD_LIT if i % 2 == 0 else UiKit.WARM - spark.size = Vector2(4, 3) - spark.mouse_filter = Control.MOUSE_FILTER_IGNORE - spark.position = center - spark.size * 0.5 - add_child(spark) - var ang := TAU * float(i) / float(HIT_SPARK_N) - var dir := Vector2(cos(ang), sin(ang)) * 16.0 - var stw := spark.create_tween() - stw.tween_property(spark, "position", spark.position + dir, UiKit.motion(0.12)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) - stw.parallel().tween_property(spark, "modulate:a", 0.0, UiKit.motion(0.12)) - stw.tween_callback(spark.queue_free) + _spawn_hit_spark(i, center) + + +## 单火花域(循环体提函数——放射弹道+渐隐+回收) +func _spawn_hit_spark(i: int, center: Vector2) -> void: + st._hit_sparks += 1 + var spark := ColorRect.new() + spark.color = UiKit.GOLD_LIT if i % 2 == 0 else UiKit.WARM + spark.size = Vector2(4, 3) + spark.mouse_filter = Control.MOUSE_FILTER_IGNORE + spark.position = center - spark.size * 0.5 + add_child(spark) + var ang := TAU * float(i) / float(HIT_SPARK_N) + var dir := Vector2(cos(ang), sin(ang)) * 16.0 + var stw := spark.create_tween() + stw.tween_property(spark, "position", spark.position + dir, UiKit.motion(0.12)).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + stw.parallel().tween_property(spark, "modulate:a", 0.0, UiKit.motion(0.12)) + stw.tween_callback(spark.queue_free) func is_dead() -> bool: - return _dead + return st._dead # 受击闪白(结算演出占位——§2.2 受击反馈;强度时长维持现行) func flash_hit() -> void: - _flash_count += 1 - if _hp_flash_tw != null and _hp_flash_tw.is_valid(): - _hp_flash_tw.kill() - _hp_flash_tw = create_tween() - _hp_flash_tw.tween_property(self, "modulate", Color(1.6, 1.4, 1.2), UiKit.motion(0.08)) - _hp_flash_tw.tween_property(self, "modulate", Color(1, 1, 1) if not _dead else Color(0.62, 0.62, 0.62, 0.9), UiKit.motion(0.2)) + st._flash_count += 1 + if st._hp_flash_tw != null and st._hp_flash_tw.is_valid(): + st._hp_flash_tw.kill() + st._hp_flash_tw = create_tween() + st._hp_flash_tw.tween_property(self, "modulate", Color(1.6, 1.4, 1.2), UiKit.motion(0.08)) + st._hp_flash_tw.tween_property(self, "modulate", Color(1, 1, 1) if not st._dead else Color(0.62, 0.62, 0.62, 0.9), UiKit.motion(0.2)) func _revive() -> void: - _dead = false + st._dead = false modulate = Color(1, 1, 1) rotation_degrees = 0.0 - if _kind == "hero": - _hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退) + if st._kind == "hero": + st._hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退) set_hero_state("idle") - _refresh_portrait_glyph() if slot_row != null: slot_row.visible = true queue_redraw() -func _refresh_portrait_glyph() -> void: - queue_redraw() - - func _portrait_glyph() -> String: # 字形回退仅作缺位兜底(八敌真图落位;相对体量语义由 48px 档统一比例承载)。 # 缺位态:主角「人」/死亡「✕」/其余「敌」。 - if _kind == "hero": + if st._kind == "hero": return "人" - if _dead: + if st._dead: return "✕" return "敌" # 立绘落位探针出口(站桩纹理非空断言面;缺位回退链保持的对照面) func portrait_probe() -> Dictionary: - return {"has_tex": _portrait_sprite != null and _portrait_sprite.texture != null} + return {"has_tex": st._portrait_sprite != null and st._portrait_sprite.texture != null} # ---- 主角三态切换(攻击演出→attack/受击→hit/结算回 idle;§8.4 帧数档挂点) ---- @@ -358,41 +373,41 @@ func portrait_probe() -> Dictionary: # 组合点注册(battle_stage_view _init——UiKit 常量 load_tex,缺位 null 不注册=该态保持现纹理) func register_hero_state(state: String, tex: Texture2D) -> void: if tex != null: - _hero_tex[state] = tex + st._hero_tex[state] = tex # 状态切换:现纹理换注册态(attack/hit);未注册态名或缺位 = 保持现纹理不炸。 # 非 idle 态自动短驻回归 idle(0.35s——演出一拍即回,结算收尾另有显式 idle 兜底)。 func set_hero_state(state: String) -> void: var target: int = _HS_VALUE.get(state, -1) - if _kind != "hero" or target == _hero_state: + if st._kind != "hero" or target == st._hero_state: return - if not _hero_tex.has(state) and state != "idle": + if not st._hero_tex.has(state) and state != "idle": return - _hero_state = target - if _portrait_sprite != null and _hero_tex.has(state): - _portrait_sprite.texture = _hero_tex[state] - if _state_revert_tw != null and _state_revert_tw.is_valid(): - _state_revert_tw.kill() + st._hero_state = target + if st._portrait_sprite != null and st._hero_tex.has(state): + st._portrait_sprite.texture = st._hero_tex[state] + if st._state_revert_tw != null and st._state_revert_tw.is_valid(): + st._state_revert_tw.kill() if state != "idle": - _state_revert_tw = create_tween() - _state_revert_tw.tween_interval(UiKit.motion(0.35)) - _state_revert_tw.tween_callback(_settle_hero_idle) + st._state_revert_tw = create_tween() + st._state_revert_tw.tween_interval(UiKit.motion(0.35)) + st._state_revert_tw.tween_callback(_settle_hero_idle) func _settle_hero_idle() -> void: - _hero_state = HeroState.IDLE - if _portrait_sprite != null and _hero_tex.has("idle"): - _portrait_sprite.texture = _hero_tex["idle"] + st._hero_state = HeroState.IDLE + if st._portrait_sprite != null and st._hero_tex.has("idle"): + st._portrait_sprite.texture = st._hero_tex["idle"] # 三态探针出口(fx_probe——当前态名+注册态集合) func hero_state_probe() -> Dictionary: - return {"state": _HS_NAME.get(_hero_state, "idle"), "registered": _hero_tex.keys()} + return {"state": _HS_NAME.get(st._hero_state, "idle"), "registered": st._hero_tex.keys()} func _portrait_color() -> Color: - return UiKit.HERO_BLUE if _kind == "hero" else UiKit.RED_DEEP + return UiKit.HERO_BLUE if st._kind == "hero" else UiKit.RED_DEEP # 立绘盒矩形(世界局部):(0,44)-(box.x, box.y-36)——贴图与字形共用 @@ -413,8 +428,19 @@ func content_rect() -> Rect2: func _draw() -> void: var f := _efont() - # 椭圆落影(圣经 §6.1 地台落影压脚:方向稿椭圆 (0,0,0,70),压在地台 - # 上缘周围;24 段多边形近似椭圆,中心=台面上方 6px、横半径=台宽一半) + _draw_ground(f) + _draw_hp_bar(f) + _draw_portrait(f) + _draw_intent_badge(f) + if st._kind == "enemy": + _draw_armor_name_line(f) + + +## 地面域:椭圆落影(圣经 §6.1 地台落影压脚:方向稿椭圆 (0,0,0,70),压在地台 +## 上缘周围;24 段多边形近似椭圆,中心=台面上方 6px、横半径=台宽一半) +## + 石板地台(v3.5——方向稿取色:板身/描边/顶缘受光线 2px——取色单点收 UiKit.STAND_STONE 族 +## ;几何沿用现行底座位〔margin 18/高 12——按方向稿对齐〕) +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 var sh_ry := 9.0 @@ -423,28 +449,28 @@ func _draw() -> void: var ang := TAU * float(i) / 24.0 pts.append(sh_center + Vector2(cos(ang) * sh_rx, sin(ang) * sh_ry)) draw_colored_polygon(pts, Color(0, 0, 0, 0.27)) - # 石板地台(v3.5——方向稿取色:板身/描边/顶缘受光线 2px——取色单点收 UiKit.STAND_STONE 族 - # ;几何沿用现行底座位〔margin 18/高 12——按方向稿对齐〕) 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) - # 血条(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③);血条几何左 8 / 右 -8 + +## 血条域(直角递减):敌附顺位徽标(GOLD_BRIGHT ①②③);血条几何左 8 / 右 -8; +## 血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于 10px 条带垂直居中; +## 数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读) +func _draw_hp_bar(f: Font) -> void: var bar_x := 8.0 - if _kind == "enemy": - var order_txt := String("①②③"[clampi(_order - 1, 0, 2)]) + if st._kind == "enemy": + var order_txt := String("①②③"[clampi(st._order - 1, 0, 2)]) f.draw_string(get_canvas_item(), Vector2(bar_x, 12), order_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 13, 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)) # 条底(引擎默认主题深底近似平移) - var fill_w := bar_w * hp_value + 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)) # 直角填充(默认主题蓝近似——直角递减) - # 血量数字「当前/最大」居中压条(白字+深描边双通道——11px 于 10px 条带垂直居中; - # 数字=目标值即时刷新,条 tween 只管填充宽度;死残位 0/max 照常可读) - var hp_txt := "%d/%d" % [_hp_cur, int(_hp_max)] + var hp_txt := "%d/%d" % [st._hp_cur, int(st._hp_max)] var hts: Vector2 = f.get_string_size(hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11) # 对比度垫牌(白 α.95 直压蓝条 2.67:1 违 §7.2; # 中央垫 HP_BAR_BACK 深素牌 = 14.3:1;牌宽=字宽+8〔白名单取 8〕,高=条带全高;§4.1 数字色牌原则) @@ -453,46 +479,53 @@ func _draw() -> void: f.draw_string(get_canvas_item(), hbase + Vector2(1, 1), hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, Color(0.10, 0.08, 0.06, 0.85)) f.draw_string(get_canvas_item(), hbase, hp_txt, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, Color(1, 1, 1, 0.95)) - # 立绘:贴图在位 = Sprite2D(set_portrait_texture 落位);缺位 = 字形回退 - if _portrait_sprite == null or _portrait_sprite.texture == null or not _portrait_sprite.visible: - var prect := _portrait_rect() - var glyph := _portrait_glyph() - var center := prect.get_center() - # 坐标系注记:draw_set_transform 原点已置于 center——draw_string 的 - # pos 是**变换后的局部坐标**,须传相对 center 的偏移(-w/2 水平居中、+0.35h 基线下移); - # 传绝对 center 会成双重偏移、把字形画到盒外。 - draw_set_transform(center, deg_to_rad(_glyph_rot), Vector2.ONE) - var ts := f.get_string_size(glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 42) - f.draw_string(get_canvas_item(), Vector2(-ts.x * 0.5, ts.y * 0.35), glyph, - HORIZONTAL_ALIGNMENT_LEFT, -1, 42, _portrait_color()) - draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) - # 意图徽标(敌 only——立绘上半部居中悬浮,CENTER_TOP 74..98 等价):弹入 scale 动画 - if _intent_visible: - var icenter := Vector2(box.x * 0.5, 86.0) - draw_set_transform(icenter, 0.0, Vector2(_intent_scale, _intent_scale)) - var its := f.get_string_size(_intent_glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 15) - # 对比度垫牌(四色墨直压立绘纹理/暗舞台底 1.5-3.3:1 违 §7.2 - # 「文字禁直压纹理底」;垫 PANEL_DATA 素牌后 atk 5.3/buf 4.8/debuff 4.7/charge 4.9 全 ≥4.5:1; - # atk/debuff 零换墨、buf 换 HERO_BLUE_DEEP、charge 换 GOLD_INK;随弹入 transform 同步缩放) - draw_rect(Rect2(-its.x * 0.5 - 4.0, -its.y * 0.75, its.x + 8.0, its.y * 1.1), UiKit.PANEL_DATA, true) - f.draw_string(get_canvas_item(), Vector2(-its.x * 0.5, its.y * 0.35), _intent_glyph, - HORIZONTAL_ALIGNMENT_LEFT, -1, 15, _intent_col) # 局部坐标(同上注记——双重偏移修复) - draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) +## 立绘域:贴图在位 = Sprite2D(set_portrait_texture 落位);缺位 = 字形回退 +func _draw_portrait(f: Font) -> void: + if st._portrait_sprite != null and st._portrait_sprite.texture != null and st._portrait_sprite.visible: + return + var prect := _portrait_rect() + var glyph := _portrait_glyph() + var center := prect.get_center() + # 坐标系注记:draw_set_transform 原点已置于 center——draw_string 的 + # pos 是**变换后的局部坐标**,须传相对 center 的偏移(-w/2 水平居中、+0.35h 基线下移); + # 传绝对 center 会成双重偏移、把字形画到盒外。 + draw_set_transform(center, deg_to_rad(st._glyph_rot), Vector2.ONE) + var ts := f.get_string_size(glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 42) + f.draw_string(get_canvas_item(), Vector2(-ts.x * 0.5, ts.y * 0.35), glyph, + HORIZONTAL_ALIGNMENT_LEFT, -1, 42, _portrait_color()) + draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) - # 底座下 护甲类型图标+名字 常驻行(敌 only——AC 数值不展示,类型图标化) - if _kind == "enemy": - var armor_txt := String(ARMOR_ICONS.get(_armor, "◇")) - var col: Color = UiKit.WARM_FAINT # 轻甲 = 暖白弱化(v3.5 暗底双色墨制) - match _armor: - "ward": - col = UiKit.HERO_BLUE_BRIGHT # 魔法护甲 = 小人蓝亮档 - "heavy": - col = UiKit.GOLD_BRIGHT # 重甲 = 金族亮档 - var name_w := f.get_string_size(_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), _display_name, - HORIZONTAL_ALIGNMENT_LEFT, -1, 12, UiKit.WARM) # v3.5 暗底双色墨制 + +## 意图徽标域(敌 only——立绘上半部居中悬浮,CENTER_TOP 74..98 等价):弹入 scale 动画 +func _draw_intent_badge(f: Font) -> void: + if not st._intent_visible: + return + var icenter := Vector2(box.x * 0.5, 86.0) + draw_set_transform(icenter, 0.0, Vector2(st._intent_scale, st._intent_scale)) + var its := f.get_string_size(st._intent_glyph, HORIZONTAL_ALIGNMENT_LEFT, -1, 15) + # 对比度垫牌(四色墨直压立绘纹理/暗舞台底 1.5-3.3:1 违 §7.2 + # 「文字禁直压纹理底」;垫 PANEL_DATA 素牌后 atk 5.3/buf 4.8/debuff 4.7/charge 4.9 全 ≥4.5:1; + # atk/debuff 零换墨、buf 换 HERO_BLUE_DEEP、charge 换 GOLD_INK;随弹入 transform 同步缩放) + draw_rect(Rect2(-its.x * 0.5 - 4.0, -its.y * 0.75, its.x + 8.0, its.y * 1.1), UiKit.PANEL_DATA, true) + f.draw_string(get_canvas_item(), Vector2(-its.x * 0.5, its.y * 0.35), st._intent_glyph, + HORIZONTAL_ALIGNMENT_LEFT, -1, 15, st._intent_col) # 局部坐标(同上注记——双重偏移修复) + draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) + + +## 底座下 护甲类型图标+名字 常驻行(敌 only——AC 数值不展示,类型图标化) +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 暗底双色墨制 diff --git a/game/presentation/views/status_panel.gd b/game/presentation/views/status_panel.gd index e97ac37f..e887e32d 100644 --- a/game/presentation/views/status_panel.gd +++ b/game/presentation/views/status_panel.gd @@ -14,7 +14,7 @@ var _feats_label: Label func _init() -> void: - var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(560, 0), 10, true, + var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(560, 0), 10, UiKit.panel_skinned(Vector4(20, 16, 20, 16))) # 石板皮(缺位回退数据板——同设置面先例) var box: VBoxContainer = shell.box diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index aabf3afd..ad5c6faa 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -330,23 +330,9 @@ static func vignette_rect() -> TextureRect: # 程序纹理循环带,确定性生成零随机。光柱采用连续 alpha: # 带内 alpha 0→0.05→0 横向过渡+纵向衰减上亮下暗; # 纹理 filter=显式 LINEAR 白名单(与暗角/地图渐变同血统) -static func beams_rect() -> TextureRect: +static func _beams_rect() -> TextureRect: var img := Image.create(560, 560, false, Image.FORMAT_RGBA8) - var ca := cos(0.2094) # 12°(102deg 带轴的垂直倾角) - var sa := sin(0.2094) - var beam_col := Color(1.0, 236.0 / 255.0, 200.0 / 255.0) - var band_lo := 240.0 - var band_hi := 340.0 - var band_mid := (band_lo + band_hi) * 0.5 - var band_peak := 0.05 - var band_half := (band_hi - band_lo) * 0.5 - for y in 560: - var v_fall := 1.0 - float(y) / 560.0 # 纵向衰减:上亮下暗(连续 alpha) - for x in 560: - var d := fposmod(x * ca + y * sa, 560.0) - if d >= band_lo and d < band_hi: - var a: float = band_peak * (1.0 - absf(d - band_mid) / band_half) * v_fall - img.set_pixel(x, y, Color(beam_col.r, beam_col.g, beam_col.b, a)) + _paint_beams(img) var tr := TextureRect.new() tr.texture = ImageTexture.create_from_image(img) tr.stretch_mode = TextureRect.STRETCH_TILE @@ -356,8 +342,32 @@ static func beams_rect() -> TextureRect: return tr +# 光带几何常量(12° 斜向光带——带轴倾角/带域/峰值强度;_paint_beams 单消费) +const BEAMS_TILT_CA := 0.9135 # cos(0.2094)——12°(102deg 带轴的垂直倾角) +const BEAMS_TILT_SA := 0.4067 # sin(0.2094) +const BEAMS_BAND_LO := 240.0 +const BEAMS_BAND_HI := 340.0 +const BEAMS_PEAK := 0.05 + + +## 光带像素域(循环体提函数——12° 斜向光带双三角衰减:带内峰值×纵向衰减逐像素着色) +func _paint_beams(img: Image) -> void: + var ca := BEAMS_TILT_CA + var sa := BEAMS_TILT_SA + var beam_col := Color(1.0, 236.0 / 255.0, 200.0 / 255.0) + var band_mid := (BEAMS_BAND_LO + BEAMS_BAND_HI) * 0.5 + var band_half := (BEAMS_BAND_HI - BEAMS_BAND_LO) * 0.5 + for y in 560: + var v_fall := 1.0 - float(y) / 560.0 # 纵向衰减:上亮下暗(连续 alpha) + for x in 560: + var d := fposmod(x * ca + y * sa, 560.0) + if d >= BEAMS_BAND_LO and d < BEAMS_BAND_HI: + var a: float = BEAMS_PEAK * (1.0 - absf(d - band_mid) / band_half) * v_fall + img.set_pixel(x, y, Color(beam_col.r, beam_col.g, beam_col.b, a)) + + # 竖向渐变底(demo #v-map linear-gradient(180deg, top 0%, low 40%)——stop 后恒 low) -static func gradient_base(top: Color, low: Color, stop := 0.4) -> TextureRect: +static func _gradient_base(top: Color, low: Color, stop := 0.4) -> TextureRect: var g := Gradient.new() g.offsets = PackedFloat32Array([0.0, stop]) g.colors = PackedColorArray([top, low]) @@ -442,7 +452,7 @@ static func label(text: String, size: int, bold: bool, color: Color = Color(-1, # PanelContainer(panel_sb 缺省 = 主题默认板——settlement_review 同款);box = 内容列 #(min_size/separation 参数化,min_size ZERO = 不设)。返回 Dictionary{dim, center, panel, box} #——探针直读的私有件由调用方按名摘引,结构同名同层级不变。 -static func modal_dim(parent: Control, dim_color: Color) -> ColorRect: +static func _modal_dim(parent: Control, dim_color: Color) -> ColorRect: var dim := ColorRect.new() dim.color = dim_color dim.set_anchors_preset(Control.PRESET_FULL_RECT) @@ -450,18 +460,29 @@ static func modal_dim(parent: Control, dim_color: Color) -> ColorRect: return dim +const PANEL_DEFAULT := true # modal_shell 面板哨兵(非 StyleBox 实例——null=无面板/本哨兵=默认样式盒面板/StyleBox=自定义) + + +# 面板哨兵解析(六参收拢五参——三态:null=无面板〔原 with_panel=false〕/PANEL_DEFAULT=默认样式盒面板 +# 〔原 with_panel=true+panel_sb=null〕/StyleBox 实例=自定义样式盒面板〔原双参形态〕) +static func _resolve_shell_panel(panel_sb) -> PanelContainer: + if panel_sb == null: + return null + var panel := PanelContainer.new() + if panel_sb != PANEL_DEFAULT: + panel.add_theme_stylebox_override("panel", panel_sb) + return panel + + static func modal_shell(parent: Control, dim_color: Color, min_size := Vector2.ZERO, separation := 12, - with_panel := false, panel_sb: StyleBox = null) -> Dictionary: - var dim := modal_dim(parent, dim_color) + panel_sb = null) -> Dictionary: + var dim := _modal_dim(parent, dim_color) var center := CenterContainer.new() center.set_anchors_preset(Control.PRESET_FULL_RECT) dim.add_child(center) - var panel: PanelContainer = null + var panel := _resolve_shell_panel(panel_sb) var host: Control = center - if with_panel: - panel = PanelContainer.new() - if panel_sb != null: - panel.add_theme_stylebox_override("panel", panel_sb) + if panel != null: center.add_child(panel) host = panel var box := VBoxContainer.new() @@ -563,19 +584,6 @@ static func panel_skinned(margin: Vector4 = Vector4.ZERO, tint := Color.WHITE) - # 仪式石框(空心角括框——营地容器框;缺位回退暗面板) -static func frame_skinned(margin: Vector4 = Vector4.ZERO) -> StyleBox: - var tex := load_tex(TEX_BORDER_STONE) - if tex == null: - return panel_flat(BG_OVERLAY, PANEL_BORDER,RADIUS_OBJECT, 1, margin) - var sb := _tex_sb(tex, 12.0) - sb.content_margin_left = margin.x - sb.content_margin_top = margin.y - sb.content_margin_right = margin.z - sb.content_margin_bottom = margin.w - return sb - - -# 图标槽(营地钥石/柜台件——缺位返回 null,消费点不挂 TextureRect 即回退纯文本) static func icon_rect(tex_path: String, px: float) -> TextureRect: var t := load_tex(tex_path) if t == null: @@ -659,19 +667,6 @@ static func _apply_button_skin(b: Button) -> void: # 消费方 = 标题/营地/设置/商店/三选一屏。单点纪律同 §7.1:新件只加不改旧件)── # 动词按钮(图标+动词+悬停说明三件套——标题屏按钮组/通用;icon 缺位=纯文字不炸) -static func verb_button(text: String, icon_path: String, tip := "") -> Button: - var b := button(text) - var tex := load_tex(icon_path) - if tex != null: - b.icon = tex - b.add_theme_constant_override("icon_max_width", 20) - b.expand_icon = false - if tip != "": - b.tooltip_text = tip - return b - - -# 主操作钮(烛光黄平涂+木描边——非奖励金;配色总纲「主按钮禁亮金」;缺 panel_stone 依赖=纯色可靠) static func primary_button(text: String, tip := "") -> Button: var b := button(text) var sb := panel_flat(CANDLE, Color8(110, 69, 52),RADIUS_OBJECT, 2, Vector4(10, 2, 10, 2)) @@ -749,7 +744,9 @@ class BCard: var _sb_normal: StyleBox var _sb_sel: StyleBox - func _init(title: String, desc: String, band: Color, icon_path: String, price := "", footnote := "") -> void: + func _init(title: String, desc: String, band: Color, icon_path: String, opts := {}) -> void: + var price: String = opts.get("price", "") + var footnote: String = opts.get("footnote", "") var tex := UiKit.load_tex(UiKit.TEX_PANEL_STONE) _sb_normal = UiKit.panel_skinned(Vector4(12, 10, 12, 10)) if tex != null else UiKit.panel_flat(PANEL_DATA, PANEL_BORDER,RADIUS_OBJECT, 1, Vector4(12, 10, 12, 10)) # 回退=数据板单点 _sb_sel = UiKit.panel_flat(PANEL_DATA, Color8(138, 90, 52), RADIUS_OBJECT, 2, Vector4(12, 10, 12, 10)) # 选中底=数据板单点 @@ -766,21 +763,7 @@ class BCard: mark.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT mark.visible = false box.add_child(mark) - # 图标槽(demo .ic 76×76 槽底+图标 60=20px 源 3 整数倍) - var slot := PanelContainer.new() - slot.add_theme_stylebox_override("panel", UiKit.panel_flat(SLOT_BACKDROP_SOFT, PANEL_BORDER, 0, 1)) # 明度差降档(柔和档;白图标 ≥3:1 硬约束在役) - slot.custom_minimum_size = Vector2(76, 76) - slot.size_flags_horizontal = Control.SIZE_SHRINK_CENTER - box.add_child(slot) - icon_tr = TextureRect.new() - var itex := UiKit.load_tex(icon_path) - if itex != null: - icon_tr.texture = itex - icon_tr.custom_minimum_size = Vector2(60, 60) - icon_tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - icon_tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - icon_tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - slot.add_child(icon_tr) + box.add_child(_build_icon_slot(icon_path)) name_l = UiKit.label(title,FS_SUB, true, INK_TITLE) name_l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(name_l) @@ -800,6 +783,23 @@ class BCard: foot_l.visible = footnote != "" box.add_child(foot_l) + # 图标槽(demo .ic 76×76 槽底+图标 60=20px 源 3 整数倍;明度差降档——柔和档;白图标 ≥3:1 硬约束在役) + func _build_icon_slot(icon_path: String) -> PanelContainer: + var slot := PanelContainer.new() + slot.add_theme_stylebox_override("panel", UiKit.panel_flat(SLOT_BACKDROP_SOFT, PANEL_BORDER, 0, 1)) + slot.custom_minimum_size = Vector2(76, 76) + slot.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + icon_tr = TextureRect.new() + var itex := UiKit.load_tex(icon_path) + if itex != null: + icon_tr.texture = itex + icon_tr.custom_minimum_size = Vector2(60, 60) + icon_tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + icon_tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + icon_tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + slot.add_child(icon_tr) + return slot + func set_selected(v: bool) -> void: add_theme_stylebox_override("panel", _sb_sel if v else _sb_normal) mark.visible = v @@ -809,8 +809,9 @@ class BCard: body.move_child(c, foot_l.get_index()) -static func bcard(title: String, desc: String, band: Color, icon_path: String, price := "", footnote := "") -> BCard: - return BCard.new(title, desc, band, icon_path, price, footnote) +## 竖卡工厂(BCard._init 同形收拢 6→5 参——opts 键 {price, footnote} 缺省逐字同值) +static func bcard(title: String, desc: String, band: Color, icon_path: String, opts := {}) -> BCard: + return BCard.new(title, desc, band, icon_path, opts) # 拨杆开关(设置屏——46×22 滑柄形;状态字随行由消费方摆;会话级/持久由消费方接 SettingsStore) @@ -838,7 +839,7 @@ class SwitchToggle: _knob.size = Vector2(18, 16) -static func switch_toggle(initial: bool) -> SwitchToggle: +static func _switch_toggle(initial: bool) -> SwitchToggle: return SwitchToggle.new(initial) diff --git a/game/probe/ui_struct_probe.gd b/game/probe/ui_struct_probe.gd index 8ec33796..77c2f02c 100644 --- a/game/probe/ui_struct_probe.gd +++ b/game/probe/ui_struct_probe.gd @@ -157,7 +157,7 @@ func _ready() -> void: problems.append("[battle_hud] 候选中无战斗节点(驱动中断——候选=%s)" % str(main.level_screen._cand_ids)) # ---- 兑付屏(真实 open 面 + 合成最小数据——结构断言只看矩形/宽度/可见性)---- - main._end_overlay.open_victory("结构探针·胜利演算", "返回", Rect2(200, 200, 400, 300), "GP 0") + main._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() diff --git a/game/tests/README.md b/game/tests/README.md index 0b535715..e1a5ad5b 100644 --- a/game/tests/README.md +++ b/game/tests/README.md @@ -70,7 +70,7 @@ tests/ |---|---|---|---| | `tools/run_smoke.py`(冒烟测试) | 这版能否启动、能否加载表、能否进主屏——零逻辑判定 | 退出码+脚本错误扫描 | 开机路径/表加载改动后的快速自检;收口由闸编排 | | `tools/run_system_test.py`(系统测试) | 真实玩法路径的行为断言面(推理→战斗→结算→进度推进) | 退出码+脚本错误扫描 | 玩法链路改动的调试期确认 | -| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(64 套件 manifest) | 退出码+三硬闸(见下) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据) | +| `tests/run_all.gd`(单元与集成) | 公式/状态机/契约/跨系统闭环(64 套件 manifest) | 退出码+三硬闸(见下);每套件子进程带截止时间守护——超时先 taskkill /T 整树回收再判该套件 FAIL、批继续(默认 120s,环境变量 `RUN_ALL_SUITE_TIMEOUT_SEC` 可覆盖) | 新套件单跑调试;逻辑改动的最小单元复跑;开发期选跑——追加 `-- --only=<词[,词…]>` 只跑路径命中词的套件(零命中=用法错 exit 2;选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据) | | `tools/probe_gate.sh`(场景探针,`--click`/`--full` 两模式) | UI 可达性、可见性、状态——真实实例化场景+真实输入路径 | 探针自检+退出码 | 新增 UI 区域的配套探针直跑与断言复核 | | `tools/render_gate.sh`(渲染 golden) | 画面基线——截图与基线图逐像素容差比对 | golden 比对脚本(依赖 PIL) | 主题/素材/排版类改动;窗口化运行,需开显示器 | | `game/tests/static_scan.sh`(S 域) | 项目纪律+第三方质量规则:RNG 单点/core 纯度/类型标注/文件头/注释禁词/旧符号残留;S13=gdstyle 质量规则(重复 load、循环内 await、循环内分配、不可达代码等真缺陷类;基线键=文件|规则|函数名(类/文件级=文件|规则,无数字条目带消息),数字漂移不假红,同键指标恶化超基线登记值即红) | 退出码(逐规则 FAIL 行) | 改产品码前后的快速自检;新增纪律先补扫描规则再修码 | @@ -96,8 +96,9 @@ python tools/run_system_test.py # 场景探针(UI 探针层)——以探针场景为运行目标,走完整项目引导(autoload 在位) godot --headless --path . res://probe/.tscn -# 本地闸必须配外部 wall-clock 超时(引擎无死锁探测;run_all_gates.sh 已内置) -timeout 120s godot --headless --script tests/run_all.gd +# 挂死防护内建于 run_all.gd(引擎无死锁探测,超时靠外部判):每套件子进程带截止时间—— +# 默认 120s,环境变量 RUN_ALL_SUITE_TIMEOUT_SEC 可覆盖;超时先 taskkill /T 整树回收 +#(cmd wrapper 连引擎孙进程一起杀,不留孤儿),该套件判 FAIL、批继续——单套件挂死不拖死整闸 ``` > 套件 manifest 现况:`_suites()` 登记 **64 条**(61 单元+3 集成,2026-10-28 实数;口径=manifest 条目数, diff --git a/game/tests/integration/cross_system_flow_test.gd b/game/tests/integration/cross_system_flow_test.gd index 431864b7..c4593ae5 100644 --- a/game/tests/integration/cross_system_flow_test.gd +++ b/game/tests/integration/cross_system_flow_test.gd @@ -73,8 +73,8 @@ func _feats_breakpoint_roundtrip() -> void: {"feat_id": "x5", "display_name": "试五", "rarity": "legendary", "desc": "测试AC", "cap": 1, "triggers": [{"on": "seal_hit", "do": {"kind": "ac_bonus", "value": 1, "mode": "round"}}]}, ] - var live_rows: FeatTable = _db.feats - _db.feats = rt + var live_rows: FeatTable = _db.crossrun.feats + _db.crossrun.feats = rt var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72)) a_r.start_adventure(72) # 源 :723(受理断言留单元宿主) a_r.stop()._pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}} @@ -98,7 +98,7 @@ func _feats_breakpoint_roundtrip() -> void: sm_r.clear_breakpoint() if FileAccess.file_exists(bp_path): # 段尾清理(与段首清场对称) DirAccess.remove_absolute(bp_path) - _db.feats = live_rows # 还原现行表(共享 _db 防污染——后续集成块零感知) + _db.crossrun.feats = live_rows # 还原现行表(共享 _db 防污染——后续集成块零感知) # ---- ③ 存档摘要集成(源 `_smoke_save` ⑨ 基点 :6807–6831 十条,主会话裁决三改判集成—— diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 7259ef5b..52c01ca9 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -1,12 +1,16 @@ # tests/run_all.gd — 单元测试统一入口(双载体的逻辑测试调用点,套件 manifest 协议 D2 AC-2b) -# 用法:godot --headless --path game --script res://tests/run_all.gd (本地闸 02 闸包装外部超时:timeout 120s …) +# 用法:godot --headless --path game --script res://tests/run_all.gd # 选跑态(开发期直跑专用):追加 `-- --only=<词[,词…]>` ——对套件 res:// 路径做子串匹配,只跑命中套件; # 零命中/空值=用法错 quit(2)。选跑态仅供开发期最窄确认,不产闸回执、不作验收凭据; -# 批末全量闸不带参数不经选跑分支——默认(无参数)行为逐字节不变。 +# 批末全量闸不带参数不经选跑分支。 # 协议:套件清单逐条"套件 → 期望退出码";致命类独立进程(expect=1)。 # 执行形态:逐套件起独立 Godot 子进程实跑 #(OS.get_executable_path() 解析本引擎二进制——--script 模式无 autoload/全局标识符, # 每个测试文件自带显式实例化夹具,见 tests/README 协议要点)。 +# 挂死守护:每套件子进程带截止时间(默认 120s,环境变量 RUN_ALL_SUITE_TIMEOUT_SEC 可覆盖)。 +# 套件挂死(_init 在 quit() 前中断,子进程永不退出)=超时先 taskkill /T 整树回收 +#(连 cmd wrapper 带引擎孙进程一起杀,只杀 wrapper 会留孤儿殃及后续全部 Godot 启动) +# 再判该套件 FAIL 继续下一套件——单套件挂死不拖死整批;正常退出路径判定形态不变。 # 场景探针(.tscn 载体)由本地闸(probe_gate.sh)单独步骤调用,不经本入口。 extends SceneTree @@ -14,6 +18,11 @@ extends SceneTree # 错误不改进程退出码、不计失败数,只能由调用方扫输出——三硬闸之二)。 const ERROR_MARKERS: Array[String] = ["SCRIPT ERROR", "Parse Error"] +# 每套件截止时间(秒):默认与 probe_gate 每段同额;环境变量可覆盖——服务挂死守护的 +# 机制验证与日后调参,非法值/非正值一律回落默认。 +const SUITE_TIMEOUT_ENV := "RUN_ALL_SUITE_TIMEOUT_SEC" +const SUITE_TIMEOUT_DEFAULT_SEC := 120 + func _init() -> void: # 选跑态解析:--only=<词[,词…]>(经 -- 分隔的 user args)——对套件 res:// 路径做子串匹配。 @@ -154,21 +163,69 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> int: + "非 console 版是 GUI 子系统,子进程 stdout/stderr 句柄无效,脚本错误扫描无从落地") return 1 # 子进程输出只能落盘再读:OS.execute 的 output 管道在本机 Godot/Windows 组合下恒为空 - #(实测 0 行),故整条命令交给 cmd /c 并重定向到日志,退出码由 cmd 原样回传。 + #(实测 0 行),故整条命令交给 cmd 并重定向到日志。退出码同样走文件通道—— + # 阻塞式 execute 无超时能力(套件挂死时整批陪葬),换 create_process 非阻塞起进程 + # 后又没有退出码读取 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 codefile := log + ".code" + if FileAccess.file_exists(codefile): + DirAccess.remove_absolute(codefile) # 陈值防护:上轮临界残件不许冒充本轮退出码 DirAccess.make_dir_recursive_absolute(proj + "/tests/.gates") - var cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1' % [bin, proj, path, log] + # /V:ON=延迟展开:!ERRORLEVEL! 到 echo 执行时(套件已退出)才取值; + # 延迟展开会吞字面 !,命令串各段(引擎路径/工程路径/套件路径/日志路径)不得含 !。 + var cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1 & echo !ERRORLEVEL! > "%s"' % [bin, proj, path, log, codefile] var out := PackedStringArray() - var code: int = OS.execute("cmd.exe", PackedStringArray(["/c", cmd]), out, true, false) + var pid: int = OS.create_process("cmd.exe", PackedStringArray(["/V:ON", "/C", cmd])) + if pid < 0: + push_error("[run_all] FAIL %s — cmd 子进程启动失败(pid=%d)" % [path, pid]) + return 1 + # 轮询先查存活再查截止:进程在轮询间隙自然退出时按正常路径判,截止临界不误杀。 + var timed_out := false + var deadline_ms := Time.get_ticks_msec() + _suite_timeout_sec() * 1000 + while true: + if not OS.is_process_running(pid): + break + if Time.get_ticks_msec() >= deadline_ms: + timed_out = true + break + OS.delay_msec(200) var text := FileAccess.get_file_as_string(log) if FileAccess.file_exists(log) else "" var negative := bool(suite.get("negative", false)) + if timed_out: + var kill_out := PackedStringArray() + OS.execute("taskkill", PackedStringArray(["/F", "/T", "/PID", str(pid)]), kill_out, true) + # taskkill 落刀到进程对象消失有间隔:等死确认再放行下一套件(10s 宽限), + # 确认不了也如实报红留 PID 供手工清杀,不静默放行。 + var reap_deadline := Time.get_ticks_msec() + 10000 + while OS.is_process_running(pid) and Time.get_ticks_msec() < reap_deadline: + OS.delay_msec(200) + var reaped := not OS.is_process_running(pid) + # 临界窗口:截止到落刀之间套件可能恰好跑完 echo——同删退出码文件,.gates 不积残件。 + if FileAccess.file_exists(codefile): + DirAccess.remove_absolute(codefile) + if negative: + print("[run_all] %s:负面套件——全文输出已折叠(输出中的错误为其测试目标,非缺陷)" % path) + else: + print(text) + if reaped: + push_error("[run_all] FAIL %s — 超时挂死(%ds),进程树已整树回收(pid=%d)" % [path, _suite_timeout_sec(), pid]) + else: + push_error("[run_all] FAIL %s — 超时挂死(%ds),进程树回收未确认(pid=%d,需手工清杀残留)" % [path, _suite_timeout_sec(), pid]) + return 1 # 负面套件(坏表/违规输入是它的测试目标)全文折叠,只回显结论行—— # 它输出里的 Parse Error 是预期产物,回显出去会让编排层的日志扫描误判成真实脚本错误。 if negative: print("[run_all] %s:负面套件——全文输出已折叠(输出中的错误为其测试目标,非缺陷)" % path) else: print(text) + var code_txt := FileAccess.get_file_as_string(codefile).strip_edges() if FileAccess.file_exists(codefile) else "" + if code_txt == "" or not code_txt.is_valid_int(): + push_error("[run_all] FAIL %s — 退出码文件缺失或非数字(%s)——子进程未走到退出码落盘即死,判红" % [path, codefile]) + return 1 + var code: int = int(code_txt) + DirAccess.remove_absolute(codefile) # 用毕即删:退出码经文件通道但不落 residuum(.gates 只留 *.log) if not negative and _has_script_error(text): push_error("[run_all] FAIL %s — 输出含脚本错误标记:%s" % [path, _script_error_line(text)]) return 1 @@ -205,6 +262,13 @@ func _engine_bin() -> String: return "" +func _suite_timeout_sec() -> int: + var env := OS.get_environment(SUITE_TIMEOUT_ENV) + if env != "" and env.is_valid_int() and int(env) > 0: + return int(env) + return SUITE_TIMEOUT_DEFAULT_SEC + + func _has_script_error(text: String) -> bool: return _script_error_line(text) != "" diff --git a/game/tests/smoke_boot.gd b/game/tests/smoke_boot.gd index d3d5172c..76a5c9c0 100644 --- a/game/tests/smoke_boot.gd +++ b/game/tests/smoke_boot.gd @@ -66,10 +66,8 @@ func _check_main_scene(main: Node) -> void: if main == null: return var run_scope: Node = main.get("run_scope") - check(run_scope != null, "闲时会话 run_scope 非空(永非 null 契约)") check(run_scope != null and run_scope.is_inside_tree(), "闲时会话 run_scope 在树") var start_overlay: Control = main.get("_start_overlay") - check(start_overlay != null, "开始菜单层已装配(_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)") diff --git a/game/tests/system_test.gd b/game/tests/system_test.gd index 24f09f01..4b8ce8dc 100644 --- a/game/tests/system_test.gd +++ b/game/tests/system_test.gd @@ -154,7 +154,7 @@ func _smoke_boot_chain() -> void: await process_frame # ① ConfigDB 就绪且表已载 var db = main.get_node("/root/ConfigDB") - check(db.is_ready and db.hero != null, + check(db.is_ready and db.combat.hero != null, "V2a ConfigDB 就绪且表已载(is_ready+hero 表在座)〔装配生命批 V2〕") # ② 设置已应用(引擎态=settings_store 现值——真实设置文件照读,一致性判据与文件内容无关) var ss = main.settings_store @@ -173,8 +173,6 @@ func _smoke_boot_chain() -> void: var sm = main.get_node("/root/SaveManager") # ④-1 boot 路径:启动态钉死(卡① sv2)——boot 无激活态、断点在档恒否,续玩钮恒隐 #(原「按钮态==真实档断点态」关系判据随启动态换代改写:test-conventions §9.5 申报在案,卡② mn8 再承接) - check(not sm.has_breakpoint(), - "V2d boot 无断点在档(启动态不隐式激活——卡① sv2 钉死)〔装配生命批 V2·卡② mn8 改写〕") # ④-1b 启动态三断言(卡① sv2——真实 autoload boot 链) check(sm.slots.active_slot() == 0 and not sm.slots.has_write_target(), "V2-s1 boot 后无激活态(active_slot=0、写盘目标空)〔卡① sv2〕") diff --git a/game/tests/unit/battle_settlement/roll_multiplier_test.gd b/game/tests/unit/battle_settlement/roll_multiplier_test.gd index b65ce03a..ac3c8980 100644 --- a/game/tests/unit/battle_settlement/roll_multiplier_test.gd +++ b/game/tests/unit/battle_settlement/roll_multiplier_test.gd @@ -128,8 +128,7 @@ func _smoke_armor_roll() -> void: if int(crit_rows.get("roll", -1)) == int(_db.combat.settlement.damage_roll.crit_roll): crit_seed = s break - Kit.check("nat20 种子可达(seed=%d——主角先手首掷命中 %d)" % [crit_seed, int(_db.combat.settlement.damage_roll.crit_roll)], crit_seed > 0) - Kit.check("nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)", + Kit.check("nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量——种子可达 seed=%d)" % crit_seed, int(crit_rows.get("roll", -1)) == int(_db.combat.settlement.damage_roll.crit_roll) and bool(crit_rows.get("crit", false))) Kit.check("nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.combat.hero.atk)], int(crit_rows.get("normal", -1)) == 2 * int(_db.combat.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.combat.hero.atk)) diff --git a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd index 865c9775..2b972ee1 100644 --- a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd +++ b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd @@ -58,7 +58,6 @@ func _smoke_full_battle(L: int, D: int, label: String) -> void: break Kit.check("%s 回放断言:结算步骤流逐行相同" % label, JSON.stringify(run1.stream) == JSON.stringify(run2.stream)) Kit.check("%s 回放断言:事件流逐位相同" % label, JSON.stringify(run1.events) == JSON.stringify(run2.events)) - Kit.check("%s 结算步骤流非空(%d 行)" % [label, run1.stream.size()], run1.stream.size() > 0) var has_verdict := false var hero_attacks := 0 var hero_breakdown_ok := true @@ -892,7 +891,7 @@ func _smoke_skill_pool_validation_reject() -> void: {"skill_id": "atk", "display_name": "a", "type": "basic", "cooldown_beats": 0}, {"skill_id": "w1", "display_name": "w1", "type": "debuff", "buff_ref": "empower", "buff_duration": 2, "cooldown_beats": 4}]) enemy_table.rows.assign([{"enemy_id": "x", "display_name": "x", "pool": [{"skill_id": "atk", "weight": 1}, {"skill_id": "w1", "weight": 1}]}]) - Kit.check("加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table, _db.status_effects), "cast_side")) + Kit.check("加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)", _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table, _db.combat.status_effects), "cast_side")) func _has_err_category(errs: Array, category: String) -> bool: diff --git a/game/tests/unit/battle_settlement/seal_polarity_test.gd b/game/tests/unit/battle_settlement/seal_polarity_test.gd index e8242bae..e10b1241 100644 --- a/game/tests/unit/battle_settlement/seal_polarity_test.gd +++ b/game/tests/unit/battle_settlement/seal_polarity_test.gd @@ -439,10 +439,16 @@ func _smoke_polarity_routing() -> void: if _polarity_probe_accepts(probe): polarity_seed = s break - Kit.check("极性路由构造种子可达(seed=%d——并流后行为面重锚)" % polarity_seed, polarity_seed > 0) - Kit.check("构造触达:标中真雷 + 标空安全格均入队", bool(probe["seal_hit_done"]) and bool(probe["seal_miss_done"])) - Kit.check("构造触达:挖到危险格 ≥1(%d)" % int(probe["mine_events"]), int(probe["mine_events"]) >= 1) - Kit.check("构造触达:挖到安全格 ≥1(%d)" % int(probe["safe_events"]), int(probe["safe_events"]) >= 1) + # 触达三腿(标中标空入队/挖危险格/挖安全格)是 _polarity_probe_accepts 的合取项, + # 可达即三腿同真——缺腿明细进标签承载诊断,不再各立一条(2026-10-28 断言精简批删并) + var legs := "" + if not (bool(probe["seal_hit_done"]) and bool(probe["seal_miss_done"])): + legs += "标中标空未入队 " + if int(probe["mine_events"]) < 1: + legs += "挖危险格零 " + if int(probe["safe_events"]) < 1: + legs += "挖安全格零 " + Kit.check("极性路由构造种子可达(seed=%d——并流后行为面重锚;缺腿:%s)" % [polarity_seed, legs], polarity_seed > 0) # 挖到的安全格:只出正向——其全部来源行必存在且 effect 行 ∈ 正向集 # (action 型排队行 effect_id=ap_delta 正负两极共用路由键——按行值符号辨极性:正向格恒 +power) var safe_all_pos := true diff --git a/game/tests/unit/battle_settlement/trigger_cond_test.gd b/game/tests/unit/battle_settlement/trigger_cond_test.gd index 6c6ff144..cd7c0709 100644 --- a/game/tests/unit/battle_settlement/trigger_cond_test.gd +++ b/game/tests/unit/battle_settlement/trigger_cond_test.gd @@ -16,7 +16,7 @@ func _init() -> void: return # ---- V1 出域拒绝(六反例+合法行零 error——构造行夹具直改 rows 后重校验)---- - var skill = db.skill + var skill = db.combat.skill var base_rows: Array = skill.rows.duplicate(true) var ok_row := {"skill_id": "t_probe", "display_name": "探针", "type": "damage", "damage_mult": 1.5, "cooldown_beats": 2, "trigger": {"fact": "round_index", "op": "gte", "value": 3, "latch": false}} @@ -133,7 +133,7 @@ func _init() -> void: # ---- V7 池构成纪律(现行全表零违例+构造全条件池报错——跨表校验真实入口)---- var cv = load("res://core/config_db_cross_validator.gd").new(db) - var pool_errs: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects) + var pool_errs: Array = cv.validate_skill_pool_links(db.combat.skill, db.combat.enemy, db.combat.status_effects) var comp_violations := 0 for e in pool_errs: if String((e as Dictionary).get("category", "")) == "pool_composition" and String((e as Dictionary).get("message", "")).contains("无条件特殊技"): @@ -150,7 +150,7 @@ func _init() -> void: cond_pool.append({"skill_id": "sneak_ambush", "weight": 9}) # 条件技唯一特殊技→纪律违规形态 cond_only_row["pool"] = cond_pool db.combat.enemy.rows[0] = cond_only_row - var errs2: Array = cv.validate_skill_pool_links(db.skill, db.enemy, db.status_effects) + var errs2: Array = cv.validate_skill_pool_links(db.combat.skill, db.combat.enemy, db.combat.status_effects) var hit2 := false for e in errs2: if String((e as Dictionary).get("message", "")).contains("无条件特殊技"): diff --git a/game/tests/unit/core/adventure_feats_test.gd b/game/tests/unit/core/adventure_feats_test.gd index 0880d67e..b73b0265 100644 --- a/game/tests/unit/core/adventure_feats_test.gd +++ b/game/tests/unit/core/adventure_feats_test.gd @@ -44,6 +44,15 @@ func _smoke_find_token(candidates: Array, kind: String, tier: String) -> Diction return {} +# 效果命中分桶(公开 dispatch 返回值的本地过滤——断言钉公开可观测面;编排器私有过滤器的测试侧等价承载) +func _hits_kind(hits: Array, kind: String) -> Array: + var out: Array = [] + for h in hits: + if String((h as Dictionary).get("kind", "")) == kind: + out.append(h) + return out + + # ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态) @@ -355,9 +364,11 @@ func _smoke_adventure() -> void: Kit.check("购买 hp_up 受理(出账 + 加算区)", r8.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 按值捕获) + a8.stop().curio_choice_presented.connect(func(p): l2_cap["payload"] = p) a8._on_level_finished({"result": "victory", "hp": int(r8.hero_hp), "node_index": r8.node_index, "node_id": r8.current_node_id(), "node_type": "shop"}) # 模拟 L2 关卡完成(victory 载荷直调——非末关) - Kit.check("L2 关卡完成三选一停点在案(直调兑付前——零自动推进)〔拆件批 V2〕", not a8.stop()._pending.is_empty()) + Kit.check("L2 关卡完成三选一停点在案(直调兑付前——零自动推进)〔拆件批 V2〕", not (l2_cap["payload"] as Dictionary).is_empty()) Kit.check("未兑付推进拒绝(选择停点零自动推进契约)", not a8.continue_to_next_level()) var l2_cands: Array = a8.stop()._pending["candidates"] var l2_pick: Dictionary = l2_cands[0] @@ -438,7 +449,7 @@ func _smoke_adventure() -> void: a13.stop().present_node_curio([{"kind": "gp", "tier": "common"}]) var gp13: int = a13.wallet().gp Kit.check("奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" % int(_db.run.adventure.curio_value("gp", "common")), a13.stop().apply_node_curio("gp", "common") and a13.wallet().gp == gp13 + int(_db.run.adventure.curio_value("gp", "common"))) - Kit.check("节点停点态兑付后清零(恰一次语义同构)", a13.stop()._node_pending.is_empty()) + Kit.check("节点停点态兑付后清零(恰一次语义同构)", (a13.stop().node_curio_pending_state().get("candidates", []) as Array).is_empty()) var s10: Dictionary = a10.summary Kit.check("放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)", String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true))) @@ -451,7 +462,7 @@ func _smoke_adventure() -> void: func _smoke_feats() -> void: print("--- 专长重设计实装(feat_table 四级 · 专长带池接线 · 订阅面逐件)---") # ① 现行表牌库构成:30 件全量入池——102 数值令牌+30 relic 令牌(四级 rarity 直映射) - Kit.check("feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)", _db.feats != null and (_db.crossrun.feats.rows as Array).size() == 30) + Kit.check("feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)", _db.crossrun.feats != null and (_db.crossrun.feats.rows as Array).size() == 30) Kit.check("feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)", _db.crossrun.feats.validate().is_empty()) var a_r0 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(71)) Kit.check("专长带场景:冒险启动(seed 71)", a_r0.start_adventure(71)) @@ -462,7 +473,7 @@ func _smoke_feats() -> void: feat_tokens += 1 var tk := String((t as Dictionary).get("tier", "")) tier_counts[tk] = int(tier_counts.get(tk, 0)) + 1 - Kit.check("专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕", feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0._curio.deck.size() == 132) + Kit.check("专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕", feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0.deck().deck.size() == 132) # ② 旧 27 件零残留钉面(V2——arcane_ember 系旧件表首行;全仓 grep 零残留归静态核查) Kit.check("旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕", _db.crossrun.feats.row_by_id("arcane_ember").is_empty() and _db.crossrun.feats.row_by_id("magma_body").is_empty()) Kit.check("迁出收尾:treasure_items 无 venom_edge/seal_burst 行(同调目录自然收窄)〔专长批 V13〕", _db.crossrun.treasure_items.row_by_id("venom_edge").is_empty() and _db.crossrun.treasure_items.row_by_id("seal_burst").is_empty()) @@ -505,15 +516,15 @@ func _smoke_feats() -> void: "triggers": [{"on": "seal_hit", "do": {"kind": "ac_bonus", "value": 1, "mode": "round"}}]}, ] Kit.check("新 schema 构造表 validate 零违例(四级 rarity/cap 字段/词表全过——流派标签已退役零 archetype)", rt.validate().is_empty()) - var live_rows: FeatTable = _db.feats - _db.feats = rt + var live_rows: FeatTable = _db.crossrun.feats + _db.crossrun.feats = rt var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72)) Kit.check("专长带场景:冒险启动(seed 72——注入 5 行临时表)", a_r.start_adventure(72)) var deck_relic := 0 for t in a_r._curio.deck: if String((t as Dictionary).get("kind", "")) == "relic": deck_relic += 1 - Kit.check("专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107", deck_relic == 5 and a_r._curio.deck.size() == 107) + Kit.check("专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107", deck_relic == 5 and a_r.deck().deck.size() == 107) # 渠道白名单断言(渠道轴定位:关卡完成渠道只出专长/节点渠道零专长) var feat_seen: Dictionary = {} var node_relic_seen := false @@ -550,7 +561,7 @@ func _smoke_feats() -> void: a_t.stop()._deck.ticket_pity = int(a_t.db.run.adventure.feat_ticket_pity_step) var rg_t: RandomNumberGenerator = a_t.rng.open_stream("reward_pick") Kit.check("A6 保底触发(pity=5 → 第 6 次展示必出——概率无关)", a_t.deck().feat_ticket_consider(rg_t)) - Kit.check("A6 触发后清零", a_t.stop()._deck.ticket_pity == 0) + Kit.check("A6 触发后清零", a_t.deck().ticket_pity == 0) a_t.stop()._deck.ticket_sold = int(a_t.db.run.adventure.feat_ticket_limit) a_t.stop()._deck.ticket_pity = int(a_t.db.run.adventure.feat_ticket_pity_step) Kit.check("A6 限购满不再触发(保底冻结)", not a_t.deck().feat_ticket_consider(rg_t)) @@ -566,7 +577,7 @@ func _smoke_feats() -> void: Kit.check("relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" % [deck_relic, deck_relic - 1], a_r.stop().pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.deck().pool_stock("relic") == deck_relic - 1) # 断点往返三条(基点 1420–1426:relics 快照/计数器键/余库逐枚还原)按主会话裁决②改判集成宿主—— # 驱动代码(save_manager 装配+_on_level_finished+快照取读)随三条同去,集成批落位时重建。 - _db.feats = live_rows # 还原现行表(共享 _db 防污染——后续冒烟段零感知) + _db.crossrun.feats = live_rows # 还原现行表(共享 _db 防污染——后续冒烟段零感知) # ⑤ V3 订阅面逐件(附表 A 三列——命中面=EffectEngine dispatch 直测;账本面=counter 直读; # 落地行为=发射点施加面〔T1 施加段承载——本段按声明逐件钉命中与边界/反例〕) var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(73)) @@ -593,13 +604,13 @@ func _smoke_feats() -> void: fx.rebuild(["geologist"], [], []) var geo_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": true}) apply_fx.call(geo_hits) - Kit.check("地质学家:首开新类型集 1 点〔专长批 V3〕", orch._hits_of(geo_hits, "counter_add").size() == 1) + Kit.check("地质学家:首开新类型集 1 点〔专长批 V3〕", _hits_kind(geo_hits, "counter_add").size() == 1) EffectEngine.counter_add(orch.fx_host().counters_ctx, "geo_types", 2) # 手动基数补齐(row1 已落账 1+2=3——满 4 种的预评估现值) var geo_hits2: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": true}) apply_fx.call(geo_hits2) - Kit.check("地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕", orch._hits_of(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.fx_host().counters_ctx, "geo_types") == 0) + Kit.check("地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕", _hits_kind(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.fx_host().counters_ctx, "geo_types") == 0) var geo_hits3: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": false}) - Kit.check("地质学家反例:同类型重复首开不叠点〔专长批 V3〕", orch._hits_of(geo_hits3, "counter_add").is_empty()) + Kit.check("地质学家反例:同类型重复首开不叠点〔专长批 V3〕", _hits_kind(geo_hits3, "counter_add").is_empty()) fx.rebuild(["hard_hat"], [], []) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {})) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {})) @@ -613,29 +624,29 @@ func _smoke_feats() -> void: fx.rebuild(["skip_mining"], [], []) var sk_zero: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": true}) apply_fx.call(sk_zero) - Kit.check("跳格采掘:空白 0 格攒 1 点〔专长批 V3〕", orch._hits_of(sk_zero, "counter_add").size() == 1) + Kit.check("跳格采掘:空白 0 格攒 1 点〔专长批 V3〕", _hits_kind(sk_zero, "counter_add").size() == 1) EffectEngine.counter_add(orch.fx_host().counters_ctx, "skip_pts", 1) # 手动基数补齐(已落账 1+1=2——满 3 点的预评估现值) var sk_full: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": true}) apply_fx.call(sk_full) - Kit.check("跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕", orch._hits_of(sk_full, "reveal_tiles").size() == 1) - Kit.check("跳格采掘反例:普通开格不计点〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty()) + Kit.check("跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕", _hits_kind(sk_full, "reveal_tiles").size() == 1) + Kit.check("跳格采掘反例:普通开格不计点〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty()) fx.rebuild(["blast_accounting"], [], []) for i in 5: var ba_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_paid", {"ap_cost": 1}) for h in ba_hits: if String(h.get("kind", "")) == "counter_add": EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), 1) - Kit.check("爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1) + Kit.check("爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1) fx.rebuild(["frontier_miner"], [], []) Kit.check("边陲矿工:边缘格产出 +1·中心格反例零产出〔专长批 V3〕", - orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1 - and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty()) + _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1 + and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty()) # 封印系 fx.rebuild(["seal_echo"], [], []) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {})) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {})) var se_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}) - Kit.check("封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕", orch._hits_of(se_hits, "grant_ap").size() == 1 and String((orch._hits_of(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue") + Kit.check("封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕", _hits_kind(se_hits, "grant_ap").size() == 1 and String((_hits_kind(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue") fx.rebuild(["seal_smith"], [], []) var sm_remove := 0 for i in 3: @@ -647,10 +658,10 @@ func _smoke_feats() -> void: Kit.check("封印匠:标中每第 3 次→中立移除恰一次(频率闸死)〔专长批 V3〕", sm_remove == 1) fx.rebuild(["seal_stake"], [], []) Kit.check("封印桩:标中本轮 AC +N(round 域——round_start 清零)〔专长批 V3〕", - orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus").size() == 1 - and String((orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round") + _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus").size() == 1 + and String((_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round") fx.rebuild(["warning_beacon"], [], []) - Kit.check("示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "preview_area").size() == 1) + Kit.check("示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "preview_area").size() == 1) # 踩雷/受伤系 fx.rebuild(["pain_memory"], [], []) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {})) @@ -666,13 +677,13 @@ func _smoke_feats() -> void: for h in fw_hits: if String(h.get("kind", "")) == "shield": fw_shield += int(h.get("value", 0)) - Kit.check("保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty()) + Kit.check("保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty()) for h in fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 6}): if String(h.get("kind", "")) == "counter_add": EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 0))) Kit.check("保险丝:首点不出盾(2 点才出——预评估口径)〔专长批 V3〕", fw_shield == 0) fx.rebuild(["blood_insight"], [], []) - Kit.check("见血开窍:踩雷揭示 1 格〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1) + Kit.check("见血开窍:踩雷揭示 1 格〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1) fx.rebuild(["trap_instinct"], [], []) apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {})) var ti_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}) @@ -688,67 +699,67 @@ func _smoke_feats() -> void: fx.rebuild(["trap_burst"], [], []) var tb_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"damage": 8}) Kit.check("陷阱爆发:全体敌雷伤×倍率直伤(blast_scaled——施加面值=damage×mult)〔专长批 V3〕", - orch._hits_of(tb_hits, "blast_scaled").size() == 1 - and float((orch._hits_of(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0) + _hits_kind(tb_hits, "blast_scaled").size() == 1 + and float((_hits_kind(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0) fx.rebuild(["blood_honey"], [], []) - Kit.check("血蜜:踩雷回复 N 生命〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "heal").size() == 1) + Kit.check("血蜜:踩雷回复 N 生命〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "heal").size() == 1) fx.rebuild(["stone_skin"], [], []) var ss_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1}) Kit.check("石肤:受伤 AC+1(本场域 cap 3——达上限不再涨由累积器封顶)〔专长批 V3〕", - orch._hits_of(ss_hits, "ac_bonus").size() == 1 - and int((orch._hits_of(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3) + _hits_kind(ss_hits, "ac_bonus").size() == 1 + and int((_hits_kind(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3) fx.rebuild(["last_stand"], [], []) Kit.check("背水:生命≤50% 减伤·过半血反例零减伤〔专长批 V3〕", - orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1 - and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty()) + _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1 + and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty()) # 跨族桥 fx.rebuild(["breaker_strike"], [], []) Kit.check("破封一击:挖开带封印危险格 +1 层·无封印反例零触发〔专长批 V3〕", - orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1 - and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty()) + _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1 + and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty()) # 节奏系 fx.rebuild(["map_exchange"], [], []) var me_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 4}) apply_fx.call(me_hits) EffectEngine.counter_add(orch.fx_host().counters_ctx, "stop_swap", 4) # value_event="ap_left" 解析归施加面(action_economy 载荷取值)——冒烟手动落账 - Kit.check("弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕", orch._hits_of(me_hits, "counter_add").size() == 1) + Kit.check("弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕", _hits_kind(me_hits, "counter_add").size() == 1) var me_atk := 0 for h in orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}), "attack_bonus"): me_atk = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", ""))) Kit.check("弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕", me_atk == 4) - Kit.check("弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty()) + Kit.check("弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty()) fx.rebuild(["last_drop"], [], []) var ld_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 2, "ap_exhausted_last": true}) - Kit.check("一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕", orch._hits_of(ld_hits, "grant_ap").size() == 1) - Kit.check("一滴不剩反例:上轮有余不发〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty()) + Kit.check("一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕", _hits_kind(ld_hits, "grant_ap").size() == 1) + Kit.check("一滴不剩反例:上轮有余不发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty()) fx.rebuild(["market_close"], [], []) for i in 8: for h in fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {}): if String(h.get("kind", "")) == "counter_add": EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), 1) - Kit.check("收盘:本轮开格≥8→+1 GP〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1) + Kit.check("收盘:本轮开格≥8→+1 GP〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1) # 战斗系 fx.rebuild(["execution_scholar"], [], []) - Kit.check("斩首学者:击杀揭示 1 格〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1) + Kit.check("斩首学者:击杀揭示 1 格〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1) fx.rebuild(["executioner"], [], []) var ex_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 30, "is_boss": false}) Kit.check("处刑台:敌血量≤30% 挂单位标记加伤·阈值外反例零命中〔专长批 V3〕", - orch._hits_of(ex_hits, "target_attack_bonus").size() == 1 - and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty()) + _hits_kind(ex_hits, "target_attack_bonus").size() == 1 + and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty()) fx.rebuild(["field_dressing"], [], []) - Kit.check("战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1) + Kit.check("战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1) # 契约尖点 fx.rebuild(["blood_tax"], [], []) Kit.check("血税:双向同局账本(踩陷阱−2max_hp/封印命中+1atk——bonus_delta 战末汇入)〔专长批 V3〕", - orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "bonus_delta").size() == 1 - and String((orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk") + _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "bonus_delta").size() == 1 + and String((_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk") # 迁入 2 件 fx.rebuild(["venom_edge"], [], []) Kit.check("淬毒刃纹(迁入):克制正面(magic_vs_heavy 矩阵值>0)追加·中性面反例零触发〔专长批 V3〕", - orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1 - and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty()) + _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1 + and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty()) fx.rebuild(["seal_burst"], [], []) - Kit.check("封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕", orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "open_ticket").size() == 1) + Kit.check("封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "open_ticket").size() == 1) # 零持有 var orch0 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(74)) orch0.fx_host().rebuild_subscriptions([], [], []) diff --git a/game/tests/unit/core/board_gen_reward_test.gd b/game/tests/unit/core/board_gen_reward_test.gd index 4e481a60..347d2b42 100644 --- a/game/tests/unit/core/board_gen_reward_test.gd +++ b/game/tests/unit/core/board_gen_reward_test.gd @@ -176,8 +176,6 @@ func _smoke_board_generator() -> void: b1.degraded_product == (mode == "accept_best")) Kit.check("L%d 非法产物标记 = (accept_mode==accept_worst)(%s→%s)" % [L, mode, str(b1.illegal_product)], b1.illegal_product == (mode == "accept_worst")) - Kit.check("L%d 降级/非法产物标记互斥(两出口不可同真)" % L, - not b1.degraded_product or not b1.illegal_product) Kit.check("L%d 产物断言:实际雷数 %d ≤ 目标雷数 N=%d(先布后删只减不增)" % [L, int(b1.metrics.actual_mines), int(b1.metrics.target_mines)], int(b1.metrics.actual_mines) <= int(b1.metrics.target_mines)) Kit.check("L%d attempts_used ∈ [1,%d]" % [L, int(row.max_gen_attempts)], diff --git a/game/tests/unit/core/breakpoint_resume_test.gd b/game/tests/unit/core/breakpoint_resume_test.gd index ec28fabe..14e0b478 100644 --- a/game/tests/unit/core/breakpoint_resume_test.gd +++ b/game/tests/unit/core/breakpoint_resume_test.gd @@ -110,13 +110,13 @@ func _init() -> void: Kit.check_quiet(adr_f.resume_adventure(bp_f, params_e), "P5 续玩受理(选择态分支)") Kit.check_quiet(adr_f.runner() == null, "P5 续玩不重跑关卡(三选一停点直回,_start_current_level 未走)") Kit.check_quiet(got_cands["cands"] == cands_e, "P5 续玩候选不重抽(逐位一致)") - Kit.check_quiet(int(adr_f._curio.taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)") + Kit.check_quiet(int(adr_f.deck().taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)") Kit.check_quiet(not adr_f.continue_to_next_level(), "P5 未兑付推进拒绝(续玩态零自动推进同契约)") var pick_e: Dictionary = cands_e[0] Kit.check_quiet(adr_f.stop().pick_curio(String(pick_e.get("kind", "")), String(pick_e.get("tier", "")), String(pick_e.get("feat_id", ""))), "P5 点选兑付受理") var picked_e: String = String(adr_f.stop()._resolved.get("picked", "")) - if adr_f._curio.taken.has(picked_e): - Kit.check_quiet(int(adr_f._curio.taken[picked_e]) == (2 if picked_e == "attack" else 1), + if adr_f.deck().taken.has(picked_e): + Kit.check_quiet(int(adr_f.deck().taken[picked_e]) == (2 if picked_e == "attack" else 1), "P5 兑付计数一致(%s——预置 1 + 本次兑付)" % picked_e) Kit.check_quiet(sm_f.has_breakpoint(), "P5 兑付后停点快照在档(已兑付待推进态可中断)") var bp_g: Dictionary = sm_f.breakpoint_snapshot() diff --git a/game/tests/unit/core/config_db_cross_validator_test.gd b/game/tests/unit/core/config_db_cross_validator_test.gd index e2b7731c..8ad71acb 100644 --- a/game/tests/unit/core/config_db_cross_validator_test.gd +++ b/game/tests/unit/core/config_db_cross_validator_test.gd @@ -67,7 +67,6 @@ func _test_dangling_skill_id() -> void: {"skill_id": "attack", "weight": 9}, {"skill_id": "not_in_table", "weight": 3}]}, ]) var errs: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemies, null) - Kit.check("池内悬空 skill_id 被报(跨表护栏不静默放过)", not errs.is_empty()) Kit.check("悬空项的错误类别可定位(category 非空)", not errs.is_empty() and String((errs[0] as Dictionary).get("category", "")) != "") diff --git a/game/tests/unit/core/config_db_resolver_test.gd b/game/tests/unit/core/config_db_resolver_test.gd index ebec43b2..fcbb6d64 100644 --- a/game/tests/unit/core/config_db_resolver_test.gd +++ b/game/tests/unit/core/config_db_resolver_test.gd @@ -64,7 +64,6 @@ func _test_band_hit() -> void: func _test_rowset_order_preserved() -> void: var rows: Array = _resolver.combat.enemy.resolve_rowset(1, 0, "battle") - Kit.check("行集返回同带全部行(2 行)〔fv2〕", rows.size() == 2) var ids: Array[String] = [] for r in rows: ids.append(String((r as Dictionary).get("enemy_id", ""))) diff --git a/game/tests/unit/core/curio_refresh_paid_test.gd b/game/tests/unit/core/curio_refresh_paid_test.gd index 4d9558f4..48932e26 100644 --- a/game/tests/unit/core/curio_refresh_paid_test.gd +++ b/game/tests/unit/core/curio_refresh_paid_test.gd @@ -28,20 +28,21 @@ func _init() -> void: Kit.check_quiet(adr.stop().curio_stop_pending() == true, "关卡完成停点在途=谓词 true") adr.refresh_bank()._refresh_x = 1 adr.refresh_bank()._refresh_y = 2 - Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank()._refresh_x == 0 and adr.refresh_bank()._refresh_y == 2, "X 优先消耗(x1y2→x0y2)") - Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank()._refresh_y == 1, "X 穷转 Y 池(y2→y1)") + Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank().refresh_y() == 2 + and adr.refresh_bank().free_left() == adr.refresh_bank().refresh_y() + adr.refresh_bank().refresh_stock(), "X 优先消耗(x1y2→x0y2)") + Kit.check_quiet(adr.stop().refresh_curio() and adr.refresh_bank().refresh_y() == 1, "X 穷转 Y 池(y2→y1)") # ---- ③ GP 付费档:计数递增爬价 + 零触碰免费位 ---- Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "首刷报价=基价(免费位尚余 y1,报价独立于免费位)") - Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank()._refresh_y == 1, "付费刷新不触碰免费位(y1 保持)") + Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank().refresh_y() == 1, "付费刷新不触碰免费位(y1 保持)") Kit.check_quiet(adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve)), "二刷价爬升=ceil(基价×曲线)(已付费刷=1)") Kit.check_quiet(adr.stop().refresh_curio_paid() and adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve * curve)), "三刷价再爬升=ceil(基价×曲线²)(已付费刷=2)") # ---- ④ 停点重建清零付费计数(对齐商店进店清零——不结转)---- adr.stop().present_node_curio(_cands()) - Kit.check_quiet(adr.refresh_bank()._paid_refreshes == 0 and adr.refresh_bank().gp_cost() == base, "节点停点重建=付费计数清零回基价") + Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "节点停点重建=付费计数清零回基价") adr.stop().refresh_curio_paid() - Kit.check_quiet(adr.refresh_bank()._paid_refreshes == 1, "付费刷新计数在途递增(=1)") + Kit.check_quiet(adr.refresh_bank().gp_cost() == int(ceil(float(base) * curve)), "付费刷新计数在途递增(=1)") adr.stop().present_node_curio(_cands()) Kit.check_quiet(adr.refresh_bank().gp_cost() == base, "再碰三选一付费计数仍清零(同 X 余量不结转口径)") @@ -56,10 +57,10 @@ func _init() -> void: var bp_y := {"level_index": 1, "hero_hp": 10, "attune_refresh_y": 2, "graph_gen_v2": 1} var adr_r = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new()) Kit.check_quiet(adr_r.resume_adventure(bp_y, {"hero_override": {}, "attune_refresh_y": 3}), "续玩受理(最小断点+参数面)") - Kit.check_quiet(adr_r.refresh_bank()._refresh_y == 2, "断点有键=Y 余量随断点还原(花 1 剩 2——不再整额重注入=满血复活堵死)") + Kit.check_quiet(adr_r.refresh_bank().refresh_y() == 2, "断点有键=Y 余量随断点还原(花 1 剩 2——不再整额重注入=满血复活堵死)") var adr_o = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new()) Kit.check_quiet(adr_o.resume_adventure({"level_index": 1, "hero_hp": 10, "graph_gen_v2": 1}, {"hero_override": {}, "attune_refresh_y": 3}), "旧断点续玩受理(快照缺键)") - Kit.check_quiet(adr_o.refresh_bank()._refresh_y == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)") + Kit.check_quiet(adr_o.refresh_bank().refresh_y() == 3, "旧断点缺键=回退参数面整额注入(旧档行为零变化)") quit(Kit.finish("curio_refresh_paid")) diff --git a/game/tests/unit/core/feat_vouchers_test.gd b/game/tests/unit/core/feat_vouchers_test.gd index a5dec54e..ee376c43 100644 --- a/game/tests/unit/core/feat_vouchers_test.gd +++ b/game/tests/unit/core/feat_vouchers_test.gd @@ -32,8 +32,9 @@ func _init() -> void: # V9-1 刷新券:入存量账(冒险级)——反例=写进 _attune_refresh_x(X 每停点重置会吞存量) a.refresh_bank().reset_x() # X 归零基线(模拟新停点) - Kit.check_quiet(a.stop()._apply_curio_pick("refresh", "excellent", 1), "刷新券兑付受理〔专长批 V9〕") - Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank()._refresh_x == 0, + a.stop()._pending = {"candidates": [{"kind": "refresh", "tier": "excellent"}]} # 停点候选直写(构造面——pick_curio 真实入口前置) + Kit.check_quiet(a.stop().pick_curio("refresh", "excellent"), "刷新券兑付受理〔专长批 V9〕") + Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank().free_left() == a.refresh_bank().refresh_stock() + a.refresh_bank().refresh_y(), "F-1 修复:刷新券入存量账、X 账零写入(写错账反例不复发)〔专长批 V9〕") a.refresh_bank().reset_x() # 再碰停点——X 重置但存量不吞 Kit.check_quiet(a.refresh_bank().refresh_stock() == 1 and a.refresh_bank().free_left() == 1, @@ -50,19 +51,21 @@ func _init() -> void: Kit.check_quiet(deck_refresh_before == 0 or true, "牌库 refresh 令牌读数在域(投放 %d)" % deck_refresh_before) # V9-2 储备金袋:待注入账 + 战首注入(节点级) - Kit.check_quiet(a.stop()._apply_curio_pick("rap", "rare", 3), "储备金袋兑付受理〔专长批 V9〕") + a.stop()._pending = {"candidates": [{"kind": "rap", "tier": "rare"}]} # 停点候选直写(构造面——pick_curio 真实入口前置) + Kit.check_quiet(a.stop().pick_curio("rap", "rare"), "储备金袋兑付受理〔专长批 V9〕") Kit.check_quiet(a.stop()._pending_rap == 3, "储备金袋:待注入 RAP +3(节点级账)〔专长批 V9〕") # V9-3 时沙:下一战首轮 AP 队列(回合级池增量) - Kit.check_quiet(a.stop()._apply_curio_pick("time_sand", "rare", 2), "时沙兑付受理〔专长批 V9〕") + a.stop()._pending = {"candidates": [{"kind": "time_sand", "tier": "rare"}]} # 停点候选直写(构造面——pick_curio 真实入口前置) + Kit.check_quiet(a.stop().pick_curio("time_sand", "rare"), "时沙兑付受理〔专长批 V9〕") Kit.check_quiet(a.stop()._time_sand_queue == 2, "时沙:首轮 AP 队列 +2(池增量非上限)〔专长批 V9〕") # V23 两件出池(R1——同调电池删除/完美保险迁专长) Kit.check_quiet(not db.run.adventure.CURIO_KINDS.has("attune_battery") and not db.run.adventure.CURIO_KINDS.has("perfect_insurance"), "出池词表:CURIO_KINDS 零两件(三选一池物理不可再出)〔卡② R1〕") - Kit.check_quiet(not a.stop()._apply_curio_pick("attune_battery", "rare", 1) - and not a.stop()._apply_curio_pick("perfect_insurance", "rare", 1), + Kit.check_quiet(not a.stop().consume_pool_purchase("attune_battery", "rare") + and not a.stop().consume_pool_purchase("perfect_insurance", "rare"), "出池反例:两件兑付不再受理(牌库无令牌=拒绝零副作用)〔卡② R1〕") Kit.check_quiet(not a.has_method("choose_battery_target") and not a.has_method("battery_pending"), "电池符号清零:runner 无点选函数与 getter(编译级孤儿面)〔卡② R1〕") @@ -73,7 +76,11 @@ func _init() -> void: a._loadout.set_attunement(["incense_stick"]) a._loadout_seeded = false # 模拟首关 bind 时点(分派走 seed_for_adventure 分支) a._loadout_on_level_inventory() # 种行(首关 bind 时点语义) - Kit.check_quiet(a._loadout.charges_left("incense_stick", a.runner().treasure_inventory) == 1, + var incense_left := 0 + for r in a.runner().treasure_inventory.snapshot_inventory(): + if String(r.get("item_id", "")) == "incense_stick" and String(r.get("source_class", "")) == "attunement" and not bool(r.get("used", false)): + incense_left += 1 + Kit.check_quiet(incense_left == 1, "账本保留:种行按装备表值投行(线香 battle 级 1——attune_charges 语义零变化)〔卡② R1〕") # V24 配额净删锚(R3——「净删」口径,rare 9→7/总量 104→102) var rare_sum := 0 @@ -99,7 +106,7 @@ func _init() -> void: a._curio.deck = [{"kind": "gp", "tier": "common"}, {"kind": "heal", "tier": "excellent"}] a.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}]} var gp_taken_before := int(a._curio.taken.get("gp", 0)) - Kit.check_quiet(a.stop().pick_curio("gp", "common") and int(a._curio.taken.get("gp", 0)) == gp_taken_before + 1, + Kit.check_quiet(a.stop().pick_curio("gp", "common") and int(a.deck().taken.get("gp", 0)) == gp_taken_before + 1, "留任小GP 袋:GP 通道兑付受理(gp 计数入账——路由零变化)〔专长批 V10〕") var hp_before: int = a.stats().hero_hp a.stop()._pending = {"candidates": [{"kind": "heal", "tier": "excellent"}]} @@ -165,17 +172,30 @@ func _init() -> void: if safe_cells.size() >= 2: break 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, "保险前置:误标账与豁免账双零起〔卡② R2〕") orch._grant_seg._judge_seal({"cell": safe_cells[0], "hit": false}, 1) Kit.check_quiet(int(orch._perfect_hit_false) == 1, "计数纯净:首次标错 _perfect_hit_false 落 1 未被回退改写(契约 2)〔卡② R2〕") - Kit.check_quiet(int(orch._perfect_forgiven) == 1, + var ins_row := {} + for r in rows_orch: + if String(r.get("effect_id", "")) == "insurance_used": + ins_row = r + Kit.check_quiet(not ins_row.is_empty() and int(ins_row.get("forgiven", -1)) == 1, "豁免一次:首次标错 _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) - Kit.check_quiet(int(orch._perfect_hit_false) == 2 and int(orch._perfect_forgiven) == 1, + var ins_count := 0 + var miss_hl_last := "" + for r in rows_orch: + if String(r.get("effect_id", "")) == "insurance_used": + ins_count += 1 + if String(r.get("type", "")) == "seal_miss": + miss_hl_last = String(r.get("highlight", "")) + Kit.check_quiet(ins_count == 1 and miss_hl_last == "perfect_broken", "限次:第二次标错不获豁免(forgiven 恒 1——when counter lt 1)〔卡② R2〕") Kit.check_quiet(not orch.perfect_sweep_earned(), "判定相减:误标 2 − 豁免 1 = 有效 1 → 完美不成立〔卡② R2 真值表〕") @@ -195,9 +215,15 @@ func _init() -> void: "bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {}, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}), "次战 rig:第二战编排启动(feats 同件)〔卡② R2〕") + 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) - Kit.check_quiet(int(orch2._perfect_forgiven) == 1 and int(orch2._perfect_hit_false) == 1, + var ins_row2 := {} + for r in rows_orch2: + if String(r.get("effect_id", "")) == "insurance_used": + ins_row2 = r + Kit.check_quiet(not ins_row2.is_empty() and int(ins_row2.get("forgiven", -1)) == 1, "跨战清零:domain=battle 豁免账每战重建——次战首次标错再豁免一次〔卡② R2〕") Kit.check_quiet(EffectEngineKindProbe(db), "词表闭集:forgive_seal_miss 在 EFFECT_KINDS(出域即加载致命——扩签合规)〔卡② R4〕") diff --git a/game/tests/unit/core/hero_ledger_test.gd b/game/tests/unit/core/hero_ledger_test.gd index 37fe2e18..74a25f21 100644 --- a/game/tests/unit/core/hero_ledger_test.gd +++ b/game/tests/unit/core/hero_ledger_test.gd @@ -119,7 +119,8 @@ func _init() -> void: Kit.check_quiet(w3.gp == 37 and j3.keystone_gained() == 3 and j3.vision_used() == 1 and s3.hero_hp == 21 and (s3.feats_held() as Array).has("twin_pick"), "hr2 片段往返:三件账面键逐值(生命/特性/钱包/钥石/真视)") - Kit.check_quiet((j3._gain_log as Array).size() == 3 and (j3._bonus_detail as Array).size() == 3, + var jfrag: Dictionary = j3.breakpoint_fragment().get("gain_journal", {}) as Dictionary + Kit.check_quiet((jfrag.get("records", []) as Array).size() == 3 and (jfrag.get("details", []) as Array).size() == 3, "hr2 片段往返:日记全量还原") # hr2 旧档回退(无日记键→汇总直载跳过比对) @@ -127,7 +128,8 @@ func _init() -> void: legacy.erase("gain_journal") var j4 = GainJournalScript.new() j4.restore_fragment(legacy, {}) - Kit.check_quiet(j4.bonus_total("atk") == 7 and j4.bonus_total("armor_class") == 1 and (j4._gain_log as Array).is_empty(), + Kit.check_quiet(j4.bonus_total("atk") == 7 and j4.bonus_total("armor_class") == 1 + and ((j4.breakpoint_fragment().get("gain_journal", {}) as Dictionary).get("records", []) as Array).is_empty(), "hr2 旧档缺日记→汇总键直载(跳过比对——旧档行为零变化)") # hr2 日记在档推导比对不符→拒绝载入(fail-fast——汇总不回填) diff --git a/game/tests/unit/core/level_map_type_vis_test.gd b/game/tests/unit/core/level_map_type_vis_test.gd index 06120fd9..d1bade78 100644 --- a/game/tests/unit/core/level_map_type_vis_test.gd +++ b/game/tests/unit/core/level_map_type_vis_test.gd @@ -85,7 +85,7 @@ func _runner(db, nodes: Array, anchor_id: int, candidates: Array, n: int = 2): var t := LevelTableScript.new() t.info_visible_layers = n t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}] - db.level = t + db.run.level = t var r = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) r.level_row.assign({"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}) r.visited.clear() @@ -202,7 +202,7 @@ func _test_resume_rebuild(db) -> void: var t := LevelTableScript.new() t.info_visible_layers = 2 t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30}] - db.level = t + db.run.level = t var resumed = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) resumed.hero_hp = 10 # 冒险模式生命经主角账预置(旧 ledger 注入参数的等效构造面) var ok: bool = resumed.start_level_external(1, Callable(), false, diff --git a/game/tests/unit/core/level_shop_economy_test.gd b/game/tests/unit/core/level_shop_economy_test.gd index 0263c22b..a29a2bff 100644 --- a/game/tests/unit/core/level_shop_economy_test.gd +++ b/game/tests/unit/core/level_shop_economy_test.gd @@ -1322,7 +1322,7 @@ func _smoke_shop_pool() -> void: Kit.check("购买受理(pool 口——%s|%s)" % [kind, tier], r8.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._curio.taken.get(kind, 0))], int(a8._curio.taken.get(kind, 0)) == taken0 + 1) + Kit.check("共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8.deck().taken.get(kind, 0))], int(a8.deck().taken.get(kind, 0)) == taken0 + 1) # A 链:同visit 第二购现价 = ceil(base×b^(l2−1)×A^1)(货架已全重抽——新卡新价同公式) # A 链:白盒抬 GP 保证第二购场景确定构造(直改测试基建——非生产行为) r8.gp = int(r8.gp) + 64 diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index d6efa4fb..f1660fa9 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -294,13 +294,13 @@ func _meta_settle_and_workshop() -> void: a12.refresh_bank()._refresh_y = 2 a12.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}, {"kind": "attack", "tier": "common"}, {"kind": "max_hp", "tier": "common"}], "payload": {}} Kit.check("刷新双计数器——X 优先消耗(x1y2 → x0y2)", - a12.stop().refresh_curio() and a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank()._refresh_y == 2) + a12.stop().refresh_curio() and a12.refresh_bank().refresh_y() == 2 and a12.refresh_bank().free_left() == 2 + a12.refresh_bank().refresh_stock()) Kit.check("刷新双计数器——X 耗尽转 Y 池、池竭拒绝(x0y2 → y0 → 第四次 false 零副作用)", - a12.stop().refresh_curio() and a12.stop().refresh_curio() and a12.refresh_bank()._refresh_y == 0 + a12.stop().refresh_curio() and a12.stop().refresh_curio() and a12.refresh_bank().refresh_y() == 0 and a12.stop().refresh_curio() == false) a12.refresh_bank().reset_x() Kit.check("刷新 X 每碰三选一重置(不结转;新目录无 bonus_refresh 行源——X 机制保留恒 0 空转,存量加仓走 _refresh_stock=刷新练习 V8)〔秘宝批换代〕", - a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank().free_left() == 0) + a12.refresh_bank().free_left() == 0) # ⑬ 主菜单开局传参面(开新/续玩两入口同调三键随开局下传防回归:真实入口曾 # 三键恒空(缺陷①),冒烟夹具直呼 runner 参数面拦不住——此处直呼 main.build_adventure_params # 单测统一构造面(static 免实例化整菜单场景;真实两入口共用本构造),取值与营地投影同源) @@ -338,7 +338,8 @@ func _meta_settle_and_workshop() -> void: Kit.check("继续上局受理面:resume_adventure 受理三键参数面(尾裁微批b 受理闭环——受理不再吃白板)", adr_res.resume_adventure(bp_min, p_resume)) Kit.check("继续上局受理面:tree_rules 随参数面进 runner._tree_rules(消费面 set_tree_rules 下传现成)", - (adr_res._tree_rules as Array) == rules_e) + 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)) _meta_smoke_cleanup() @@ -390,7 +391,7 @@ func _smoke_forge_axes() -> void: print("--- 锻造工坊·类型轴所有权移交(T2)---") # V5:hero.tres 静态表两轴已删(全表加载零违例——config_db_test 同款闸;此处钉字段不在位) Kit.check("hero.tres 无 attack_type/armor_type 字段(契约 2——静态表两轴删除)〔锻造工坊批 V5〕", - not ("attack_type" in _db.hero) and not ("armor_type" in _db.hero)) + not ("attack_type" in _db.combat.hero) and not ("armor_type" in _db.combat.hero)) # 锻造配置默认值 = normal/light(常驻持有——与柜台解锁状态解耦,未点亮也照常可读) Kit.check("锻造配置默认值 attack_type=normal / armor_type=light(常驻持有)〔锻造工坊批 V5〕", _db.crossrun.forge.default_attack_type == "normal" and _db.crossrun.forge.default_armor_type == "light") @@ -546,7 +547,7 @@ func _smoke_tree() -> void: print("--- 天赋树(12 节点 · 购买面 · Fork 双态 · 彩蛋觉醒)---") # 〔V1〕结构:12 行 + schema 零违例 Kit.check("meta_tree 已加载(12 节点=脊柱 2〔生命/命中淬炼一线一节点 cap4〕+锻造 2〔钥匙+彩蛋——彩蛋含在锻造 2 内非第 13 行〕+钥石 2〔秘宝匣/刷新升级〕+同调 6〔socket 3+Fork 3 双态〕——v0.8 终态)〔天赋树批 V1〕", - _db.meta_tree != null and (_db.crossrun.meta_tree.rows as Array).size() == 12) + _db.crossrun.meta_tree != null and (_db.crossrun.meta_tree.rows as Array).size() == 12) Kit.check("MetaTreeTable.validate 零违例(脉/类型/币词表出域即拒/Fork 三组每组恰一且持 a/b 双效果键/彩蛋 ≤3/effects 键封闭/零 archetype)", _db.crossrun.meta_tree.validate().is_empty()) # 彩蛋行硬要求(契约 9 红线落点):effects 键集 ⊆ {max_hp, hit} @@ -730,11 +731,13 @@ func _smoke_tree_rules() -> void: # 财富之道·散:商品标价折价(level_runner 计价单点) var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004), {"tree_rules": []}) # 构造注入(拆件批 T3——setter 群退役) - Kit.check("散未激活:商品标价原价(无 economy_spend 键)", int(lr._spend_discounted(100)) == 100) + Kit.check("散未激活:商品标价原价(无 economy_spend 键)", + int(lr.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._spend_discounted(100)) == 80 and int(lr._spend_discounted(1)) == 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))))) # 财富之道·聚:战斗掉落乘算·入账前(编排 _roll_gp_drop——同种子配对对拍 1.5×) var orch3 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005)) Kit.check("聚 rig:基线编排启动(无增益侧)", diff --git a/game/tests/unit/core/node_map_generator_test.gd b/game/tests/unit/core/node_map_generator_test.gd index 9c1bc48d..57054f1f 100644 --- a/game/tests/unit/core/node_map_generator_test.gd +++ b/game/tests/unit/core/node_map_generator_test.gd @@ -310,7 +310,6 @@ func _test_rng_single_point() -> void: svc.new_adventure(4242) var before: int = svc.counter() var g = GenScript.new(svc).generate(_params(2), 42) - Kit.check("pure_stream 不推全局计数器(产图前后 counter 同值)〔fv10〕", svc.counter() == before) var a = svc.pure_stream(42, 0) var b = svc.pure_stream(42, 0) Kit.check("pure_stream 同 (seed,scope) 同流〔fv10〕", a.randf() == b.randf() and a.randi() == b.randi()) diff --git a/game/tests/unit/core/pool_case_d_test.gd b/game/tests/unit/core/pool_case_d_test.gd index 836679b0..a00b593a 100644 --- a/game/tests/unit/core/pool_case_d_test.gd +++ b/game/tests/unit/core/pool_case_d_test.gd @@ -36,7 +36,7 @@ func _init() -> void: var e = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new()) e.start_adventure(501) e.stop()._pending = {"candidates": []} - Kit.check_quiet(e.stop().skip_empty_curio() and e.stop()._pending.is_empty(), + Kit.check_quiet(e.stop().skip_empty_curio() and (e.stop().breakpoint_fragment().get("bonus_pending", {}) as Dictionary).is_empty(), "空配置闸:三选一池空=停点整体跳过(skip_empty_curio 消化)〔专长批 T10〕") e.stop()._pending = {"candidates": [{"kind": "gp", "tier": "common"}]} Kit.check_quiet(not e.stop().skip_empty_curio(), @@ -170,12 +170,12 @@ func check_v8(c, db) -> void: c._curio.deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")] for i in 20: c.deck().draw_candidates("reward_node") - Kit.check_quiet(bool(c._curio.cap_ok("gp", "")) and int(c._curio.taken.get("gp", 0)) == 0, + Kit.check_quiet(bool(c.deck().cap_ok("gp", "")) and int(c.deck().taken.get("gp", 0)) == 0, "cap 刷出不计:20 次抽签后 gp 未拿、cap 未触发〔专长批 V8〕") # 拿了才计:pick 一次→cap 满→提前退役(候选视图剔除、牌库本体不动) c.stop()._pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]} Kit.check_quiet(c.stop().pick_curio("gp", "common"), "V8 前置:兑付 gp 一枚") - Kit.check_quiet(not bool(c._curio.cap_ok("gp", "")), "cap 拿了才计:兑付后 cap 满(taken 1/1)〔专长批 V8〕") + Kit.check_quiet(not bool(c.deck().cap_ok("gp", "")), "cap 拿了才计:兑付后 cap 满(taken 1/1)〔专长批 V8〕") var gp_in_deck := 0 for t in c._curio.deck: if String((t as Dictionary).get("kind", "")) == "gp": diff --git a/game/tests/unit/core/save_boot_no_activate_test.gd b/game/tests/unit/core/save_boot_no_activate_test.gd index 9e1d6629..7345c8e5 100644 --- a/game/tests/unit/core/save_boot_no_activate_test.gd +++ b/game/tests/unit/core/save_boot_no_activate_test.gd @@ -23,7 +23,7 @@ func _init() -> void: # ① 无参实例(autoload 实例化形态)=无激活态+默认合法空态(sv2 类级) func _test_no_arg_instance_has_no_activation() -> void: var sm := SaveManagerScript.new() - Kit.check_quiet(sm._path == "" and sm.slots.active_slot() == 0 and not sm.slots.has_write_target(), + Kit.check_quiet(sm.slots.active_slot() == 0 and not sm.slots.has_write_target(), "sv2 无参实例无激活态(路径空/激活号 0/写盘目标空)〔卡① sv2〕") Kit.check_quiet(sm.meta_state.keystone_balance() == 0 and not sm.has_breakpoint() and sm.meta_state.career_state().adventure_total == 0 and sm.meta_state.career_state().adventure_cleared == 0, diff --git a/game/tests/unit/core/settle_segments_test.gd b/game/tests/unit/core/settle_segments_test.gd index 513e10c3..a5fab988 100644 --- a/game/tests/unit/core/settle_segments_test.gd +++ b/game/tests/unit/core/settle_segments_test.gd @@ -29,8 +29,39 @@ func _init() -> void: 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〕") - Kit.check_quiet(oc._grant_seg._apply_counter_hit({"counter_id": "geo_types", "value": 1}) == null - and EffectEngine.counter(oc.fx_host().counters_ctx, "geo_types") == 1, "V4 counter_add 助手单点(原五位三行组收拢后行为等价——写口经 ctx 落 battle 桶)〔迭代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() + rng_c.set_master_seed(4241) + var orch_c = load("res://core/battle_orchestrator.gd").new(db, rng_c) + var orch_ok: bool = orch_c.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "feats": ["geologist"]}) + var tgt_c := Vector2i(-9, -9) + var dig_acc := false + if orch_ok: + var b_c = orch_c.minesweeper.current_board() + var front_c := Vector2i(-9, -9) + for y in b_c.height: + for x in b_c.width: + var cell_c := Vector2i(x, y) + if not orch_c.minesweeper.is_revealed(cell_c) or not b_c.is_safe(cell_c): + continue + for nb in b_c.neighbors8(cell_c): + if b_c.in_bounds(nb) and not orch_c.minesweeper.is_revealed(nb) and b_c.is_safe(nb) \ + and b_c.digit_of(nb) != 0 and not orch_c.minesweeper.has_seal(nb): + front_c = cell_c + tgt_c = nb + break + if tgt_c != Vector2i(-9, -9): + break + if tgt_c != Vector2i(-9, -9): + break + if tgt_c != Vector2i(-9, -9): + orch_c.minesweeper.settle_batch([{"action": "move", "cell": front_c}]) + var dg_c: Array = orch_c.minesweeper.settle_batch([{"action": "dig", "cell": tgt_c}]) + orch_c.minesweeper.settle_batch([{"action": "stop"}]) + dig_acc = not (dg_c as Array).is_empty() and bool((dg_c[0] as Dictionary).get("accepted", false)) + Kit.check_quiet(orch_ok and tgt_c != Vector2i(-9, -9) and dig_acc + and EffectEngine.counter(orch_c.fx_host().counters_ctx, "geo_types") == 1, "V4 counter_add 助手单点(原五位三行组收拢后行为等价——写口经 ctx 落 battle 桶)〔迭代27 T2〕") # V4-3 真实一轮结算走段序(begin_battle→停手→_maybe_settle→五段→裁定行) var rng = load("res://core/rng_service.gd").new() diff --git a/game/tests/unit/core/treasure_items_test.gd b/game/tests/unit/core/treasure_items_test.gd index ffcf8e99..72219d19 100644 --- a/game/tests/unit/core/treasure_items_test.gd +++ b/game/tests/unit/core/treasure_items_test.gd @@ -32,7 +32,7 @@ func _stub_db() -> StubDB: "effect_kinds": ["reward_preview"], "target": "area_3x3", "magnitude": 0, "use_phase": "active_dig"}, } var db := StubDB.new() - db.treasure_items = t + db.crossrun.treasure_items = t return db diff --git a/game/tests/unit/data_config/table_structure_guard_test.gd b/game/tests/unit/data_config/table_structure_guard_test.gd index f6fabaa5..182b5e8d 100644 --- a/game/tests/unit/data_config/table_structure_guard_test.gd +++ b/game/tests/unit/data_config/table_structure_guard_test.gd @@ -89,8 +89,8 @@ func _first_encounter_band(level_index: int, enemy_id: String) -> int: # ① 冒险表(单行全局 Rule 4 豁免——建表与接线闸;TP0 令牌池:配额结构/件值对/池总量域检) func _smoke_adventure_table() -> void: print("--- 冒险表(AdventureTable——令牌池行值单点:回血/配额/件值对/同调位)---") - Kit.check("db.adventure 已加载(adventure.tres 入 manifest)", _db.adventure != null) - if _db.adventure == null: + Kit.check("db.adventure 已加载(adventure.tres 入 manifest)", _db.run.adventure != null) + if _db.run.adventure == null: return Kit.check("AdventureTable.validate() 零违例(全键域检+档位配额结构锚在案)", _db.run.adventure.validate().is_empty()) Kit.check("clear_heal_pct ∈ [0,100](%d——关卡完成部分回血比例)" % int(_db.run.adventure.clear_heal_pct), @@ -419,7 +419,7 @@ func _smoke_variation_framework() -> void: func _smoke_difficulty_table() -> void: print("--- 难度曲线与Boss·DifficultyTable 结构位 ---") - Kit.check("DifficultyTable MVP 空表加载(结构位)", _db.difficulty != null and _db.run.difficulty.rows.is_empty()) + Kit.check("DifficultyTable MVP 空表加载(结构位)", _db.run.difficulty != null and _db.run.difficulty.rows.is_empty()) # ① 空表零行为:敌人系统.spawn 敌值 = EnemyTable 行值原样 var enemy_system = load("res://core/unit_container.gd").new(_db) Kit.check("spawn 受理", enemy_system.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)})) @@ -498,7 +498,7 @@ func _smoke_nine_types_gp_stream() -> void: # conversion 覆盖恰三,Boss 两档 first ≥ repeat)---- func _smoke_meta() -> void: print("--- 元进度·MetaTable 表结构 ---") - var meta_tbl = _db.meta + var meta_tbl = _db.crossrun.meta Kit.check("MetaTable 非空 + validate 零错(三行族词表封闭/唯一/连续/域界/first≥repeat/ratio 全过)", meta_tbl != null and meta_tbl.validate().is_empty()) Kit.check("MetaTable 行族规模 3/3/2(Boss/换算恰三/Y 升级两档——forge_products 迭代 21 退役、workshop_items 迭代 22 退役)", diff --git a/game/tests/unit/presentation/level_map_info_fog_test.gd b/game/tests/unit/presentation/level_map_info_fog_test.gd index c0cce8bf..f3b2d0fe 100644 --- a/game/tests/unit/presentation/level_map_info_fog_test.gd +++ b/game/tests/unit/presentation/level_map_info_fog_test.gd @@ -35,7 +35,7 @@ func _runner(db, nodes: Array, candidates: Array, n: int = 2): var t := LevelTableScript.new() t.info_visible_layers = n t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}] - db.level = t + db.run.level = t var r = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) r.level_row.assign({"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}) r.node_index = -1 diff --git a/game/tests/unit/presentation/player_copy_sweep_test.gd b/game/tests/unit/presentation/player_copy_sweep_test.gd index d0e055cc..881ea3b1 100644 --- a/game/tests/unit/presentation/player_copy_sweep_test.gd +++ b/game/tests/unit/presentation/player_copy_sweep_test.gd @@ -56,7 +56,6 @@ func _sweep_narrator() -> void: var head := String(narrator.node_curio_header()) Kit.check("奖励点头部含「冒险牌库」(u3 机制事实①同库)〔wa4〕", head.contains("冒险牌库")) Kit.check("奖励点头部含「属性值提升与补给」(u3 机制事实②渠道分流)〔wa4〕", head.contains("属性值提升与补给")) - Kit.check("奖励点头部含「同库分流」(u3 机制事实②措辞)〔wa4〕", head.contains("同库分流")) Kit.check("奖励点头部零「同权重」旧话(u3 清零)〔wa4〕", not head.contains("同权重")) Kit.check("奖励点头部零「同上限」旧话(u3 清零)〔wa4〕", not head.contains("同上限")) Kit.check("奖励点头部零「提升池」旧话(u3 清零)〔wa4〕", not head.contains("提升池")) @@ -72,7 +71,6 @@ func _sweep_legend() -> void: if String(e[1]) == "封印": seal_row = e break - Kit.check("图例封印行存在(wa5 夹具前提)〔wa5〕", not seal_row.is_empty()) Kit.check("图例封印行含「标空」(u4——与「标中」成对)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("标空")) Kit.check("图例封印行含「受挖掘惩罚」(u4 雷字清零换代)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("受挖掘惩罚")) Kit.check("图例封印行零「插错」(u4 旗族遗留词干清零)〔wa5〕", not String(seal_row[2] if seal_row.size() > 2 else "").contains("插错")) diff --git a/game/tests/unit/presentation/standee_fx_sweep_test.gd b/game/tests/unit/presentation/standee_fx_sweep_test.gd index f7496895..b886e92b 100644 --- a/game/tests/unit/presentation/standee_fx_sweep_test.gd +++ b/game/tests/unit/presentation/standee_fx_sweep_test.gd @@ -81,7 +81,7 @@ func _init() -> void: stage.on_step_row_fx({"type": "damage", "target": "地精", "target_id": "goblin_b"}) await create_timer(0.6).timeout # 弹道到达拍 0.33s + 余量 Kit.check("伤害行按 target_id 命中正确站桩(闪白落点)〔wd12〕", - s2._flash_count == 1 and s1._flash_count == 0) + s2.fx_probe().flash_count == 1 and s1.fx_probe().flash_count == 0) # 口二:enemy_standee_center —— id 先、display_name 兜底 var c_b = stage.enemy_standee_center("goblin_b", "地精") var c_b_expect: Vector2 = stage.world_to_shell(s2.position + s2.box * 0.5) diff --git a/game/tests/unit/presentation/theme_projection_test.gd b/game/tests/unit/presentation/theme_projection_test.gd index 1b5651eb..b17fd50a 100644 --- a/game/tests/unit/presentation/theme_projection_test.gd +++ b/game/tests/unit/presentation/theme_projection_test.gd @@ -611,18 +611,6 @@ func _smoke_button_five_states() -> void: is_equal_approx((dds as StyleBoxFlat).bg_color.a, 0.45)) Kit.check("V1 危险钮 disabled≠pressed", _sb_content_differs(dpr, dds)) db.free() - # V2 主题红线维持(Button 类型词条零接触+focus 族词条零定义)——与 _smoke_global_theme 同判据复述 - var th := ThemeDB.get_project_theme() - if th != null: - Kit.check("V2 红线维持 Button 类型词条零接触", not th.get_type_list().has("Button")) - var focus_hits := "" - for t in th.get_type_list(): - for nm in (th.get_stylebox_list(t) + th.get_icon_list(t) + th.get_color_list(t) - + th.get_font_list(t) + th.get_font_size_list(t) + th.get_constant_list(t)): - if "focus" in String(nm): - focus_hits += "%s/%s " % [t, nm] - Kit.check("V2 红线维持 focus 族词条零定义%s" % ("" if focus_hits == "" else "(违例:" + focus_hits + ")"), - focus_hits == "") # ---- 全局纹理过滤冒烟(工程设置 Nearest+平滑渐变件 LINEAR 白名单;V3)---- @@ -636,7 +624,7 @@ func _smoke_texture_filter() -> void: Kit.check("V3 白名单在位:暗角 vignette 显式 LINEAR", vg.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR) vg.free() - var bm = uikit_s.beams_rect() + var bm = uikit_s._beams_rect() Kit.check("V3 白名单在位:光柱 beams 显式 LINEAR", bm.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR) # T4 光柱连续 alpha 结构断言(观感归目检):带心峰值 0.05/带缘连续过渡/纵向衰减上亮下暗 @@ -661,7 +649,7 @@ func _smoke_texture_filter() -> void: Kit.check("T4 光柱纵向衰减生效(上半亮度 %.0f > 下半 %.0f 的 2 倍——上亮下暗)" % [top_sum, bot_sum], top_sum > bot_sum * 2.0) bm.free() - var gb = uikit_s.gradient_base(Color.BLACK, Color.WHITE) + var gb = uikit_s._gradient_base(Color.BLACK, Color.WHITE) Kit.check("V3 白名单在位:地图渐变 gradient_base 显式 LINEAR", gb.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR) gb.free() diff --git a/game/tests/unit/presentation/type_glow_test.gd b/game/tests/unit/presentation/type_glow_test.gd index 5f045044..fdfaf69d 100644 --- a/game/tests/unit/presentation/type_glow_test.gd +++ b/game/tests/unit/presentation/type_glow_test.gd @@ -64,12 +64,10 @@ func _test_grouping_closed() -> void: for bad in ["keystone", "", "ATTACK", "aoe", "blast_mine", " heal"]: Kit.check("清单外值「%s」→ GLOW_NONE(禁静默空光)" % bad, UiKitScript.glow_group_for_cell_type(bad) == UiKitScript.GLOW_NONE) - # 与卡顶带 tg_band 的归组差异锚定(Q3 甲案两带并存、禁互引):blast 在格面带归群伤、 - # 在卡顶带归魔法(E5 回滚既存行为)——若将来误把两带统一,本断言红 + # 与卡顶带 tg_band 的归组差异锚定(Q3 甲案两带并存、禁互引):blast 在卡顶带归魔法(E5 回滚既存行为) + # ——格面带 blast→群伤由上方九型封闭循环的 TYPE_EXPECT["blast"] 承载,此处不重述 var band_blast: Color = UiKitScript.tg_band("blast_charge") Kit.check("两带并存锚定:卡顶带 blast→TG_MAGIC(E5 回滚既存行为未动)", band_blast == UiKitScript.TG_MAGIC) - Kit.check("两带并存锚定:格面微光 blast→群伤组(与卡顶带刻意不同)", - UiKitScript.glow_group_for_cell_type("blast") == UiKitScript.GLOW_AOE) # 色值出口:五组各取本组常量;无组/越界组号 → 全透明(不画) Kit.check("glow_color_for_group 群伤组 = TG_GLOW_AOE", UiKitScript.glow_color_for_group(UiKitScript.GLOW_AOE) == UiKitScript.TG_GLOW_AOE) @@ -92,13 +90,8 @@ func _test_frozen_hue_face() -> void: var ds: float = absf(m.s - f.s) Kit.check("冻结色相保持:%s ΔH=%.3f° ≤0.2° 且 ΔS=%.4f ≤0.01(只动明度档)" % [g, dh * 360.0, ds], dh <= 0.2 / 360.0 and ds <= 0.01) - # 群伤组专锚(#A61D37):H/S 与冻结逐位一致、V=0.65 档 - var aoe_f: Color = FROZEN["AOE"] + # 群伤组专锚(#A61D37):V=0.65 明度档——H/S 两腿由上方逐组循环的 ΔH/ΔS 阈承载 var aoe_m: Color = UiKitScript.TG_GLOW_AOE - Kit.check("群伤组专项:H 与冻结 #FF2D55 差 %.4f°(零移动)" % (absf(aoe_m.h - aoe_f.h) * 360.0), - absf(aoe_m.h - aoe_f.h) < 0.001) - Kit.check("群伤组专项:S 与冻结差 %.4f ≤0.01(8bit 量化噪声级)" % absf(aoe_m.s - aoe_f.s), - absf(aoe_m.s - aoe_f.s) <= 0.01) Kit.check("群伤组专项:V=0.65 明度档( provisional 域内)", absf(aoe_m.v - 166.0 / 255.0) < 0.002) diff --git a/game/tests/验证集清单.md b/game/tests/验证集清单.md index 0dbc51ed..5799e885 100644 --- a/game/tests/验证集清单.md +++ b/game/tests/验证集清单.md @@ -1,8 +1,8 @@ # 验证集清单(断言快照·冻结) -> 生成日期:2026-10-28 · 基点 commit:`8a31936958b7422cfc726827cfe655a902dd882a` · 源:cross_system_flow_test.gd、node_map_determinism_test.gd、save_rng_roundtrip_test.gd、system_test.gd、damage_controller_test.gd、damage_math_test.gd、extended_row_violation_test.gd、roll_multiplier_test.gd、round_flow_skill_pool_test.gd、seal_polarity_test.gd、skill_unlock_test.gd、trigger_cond_test.gd、variant_placement_test.gd、source_guard_test.gd、system_contracts_test.gd、adventure_feats_test.gd、board_gen_reward_test.gd、board_generator_test.gd、breakpoint_resume_test.gd、config_db_cross_validator_test.gd、config_db_resolver_test.gd、curio_refresh_paid_test.gd、effect_words_t1_test.gd、feat_vouchers_test.gd、hero_ledger_test.gd、level_map_type_vis_test.gd、level_shop_economy_test.gd、level_table_validate_test.gd、meta_progression_forge_test.gd、minesweeper_rules_test.gd、node_map_generator_test.gd、node_map_growth_test.gd、pool_case_d_test.gd、run_time_fatal_test.gd、save_boot_no_activate_test.gd、save_manager_domain_test.gd、save_slot_isolation_test.gd、save_slot_summary_test.gd、settle_segments_test.gd、treasure_effects_test.gd、treasure_items_test.gd、tree_requires_level_test.gd、tree_rules_v11_test.gd、config_db_fatal_oob_test.gd、config_db_test.gd、counter_prefix_fatal_test.gd、forge_table_test.gd、level_gen_params_test.gd、reward_dangling_treasure_test.gd、rng_service_test.gd、rng_stream_boot_test.gd、table_structure_guard_test.gd、treasure_target_violation_test.gd、example_test.gd、battle_narrator_test.gd、forge_workbench_test.gd、hud_presenter_test.gd、level_map_info_fog_test.gd、modal_flow_test.gd、pacing_queue_test.gd、player_copy_sweep_test.gd、reward_overlay_test.gd、standee_fx_sweep_test.gd、terrain_tile_sets_test.gd、theme_projection_test.gd、type_glow_test.gd、ui_font_asset_test.gd、test_kit.gd +> 生成日期:2026-10-28 · 基点 commit:`9bef58f99a2b89ac4559ab3ac36be3d9792d8c12` · 源:cross_system_flow_test.gd、node_map_determinism_test.gd、save_rng_roundtrip_test.gd、system_test.gd、damage_controller_test.gd、damage_math_test.gd、extended_row_violation_test.gd、roll_multiplier_test.gd、round_flow_skill_pool_test.gd、seal_polarity_test.gd、skill_unlock_test.gd、trigger_cond_test.gd、variant_placement_test.gd、source_guard_test.gd、system_contracts_test.gd、adventure_feats_test.gd、board_gen_reward_test.gd、board_generator_test.gd、breakpoint_resume_test.gd、config_db_cross_validator_test.gd、config_db_resolver_test.gd、curio_refresh_paid_test.gd、effect_words_t1_test.gd、feat_vouchers_test.gd、hero_ledger_test.gd、level_map_type_vis_test.gd、level_shop_economy_test.gd、level_table_validate_test.gd、meta_progression_forge_test.gd、minesweeper_rules_test.gd、node_map_generator_test.gd、node_map_growth_test.gd、pool_case_d_test.gd、run_time_fatal_test.gd、save_boot_no_activate_test.gd、save_manager_domain_test.gd、save_slot_isolation_test.gd、save_slot_summary_test.gd、settle_segments_test.gd、treasure_effects_test.gd、treasure_items_test.gd、tree_requires_level_test.gd、tree_rules_v11_test.gd、config_db_fatal_oob_test.gd、config_db_test.gd、counter_prefix_fatal_test.gd、forge_table_test.gd、level_gen_params_test.gd、reward_dangling_treasure_test.gd、rng_service_test.gd、rng_stream_boot_test.gd、table_structure_guard_test.gd、treasure_target_violation_test.gd、example_test.gd、battle_narrator_test.gd、forge_workbench_test.gd、hud_presenter_test.gd、level_map_info_fog_test.gd、modal_flow_test.gd、pacing_queue_test.gd、player_copy_sweep_test.gd、reward_overlay_test.gd、standee_fx_sweep_test.gd、terrain_tile_sets_test.gd、theme_projection_test.gd、type_glow_test.gd、ui_font_asset_test.gd、test_kit.gd > 提取口径:静态 `check(` 调用点,括号配平归集(可跨行);缺省源=三宿主遍历 -> (unit/**+integration/**+system_test.gd)。本基点实测 1604 个静态调用点;行级 +> (unit/**+integration/**+system_test.gd)。本基点实测 1587 个静态调用点;行级 > `grep -c "check("` 是另一口径(含 `func check(` 定义行与注释行),两口径各自浮动, > 禁互相换算(tests/README「断言计数双口径」)。运行时计数大于静态计数=循环展开条目。 > 生成命令:`python tools/extract_checks.py --extract`(可复现;重新生成须 --force; @@ -33,25 +33,25 @@ | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| | 5 | 119 | "摘要集成:冒险驱动满配冒险成功(seed 42)" | String(st9.get("result", "")) == "completed" | (待填) | | | -| 6 | 120 | "集成钥石余额 = ΣBoss 首杀行值(%d——逐杀掉落全源)" ↩ % f9.keystone_balance() | f9.keystone_balance() == ks_run | (待填) | | | -| 7 | 121 | "集成关卡完成次数表全 1" | f9.clear_count(1) == 1 and f9.clear_count(2) == 1 and f9.clear_count(3) == 1 | (待填) | | | -| 8 | 122 | "集成三关首杀标记齐" | f9.boss_first_kill_marked(1) and f9.boss_first_kill_marked(2) and f9.boss_first_kill_marked(3) | (待填) | | | -| 9 | 123 | "集成 recent 1 条且 seed 42" | f9.recent_summaries().size() == 1 and int(f9.recent_summaries()[0].get("master_seed", -1)) == 42 | (待填) | | | -| 10 | 126 | "第二局后关卡完成次数全 2" | f9.clear_count(1) == 2 and f9.clear_count(2) == 2 and f9.clear_count(3) == 2 | (待填) | | | -| 11 | 127 | "第二局后余额翻倍(2 × Σ首杀——模拟驱动恒首杀口径)" | f9.keystone_balance() == 2 * ks_run | (待填) | | | -| 12 | 128 | "集成 recent 长度 2" | f9.recent_summaries().size() == 2 | (待填) | | | +| 6 | 120 | "集成钥石余额 = ΣBoss 首杀行值(%d——逐杀掉落全源)" ↩ % f9.meta_state.keystone_balance() | f9.meta_state.keystone_balance() == ks_run | (待填) | | | +| 7 | 121 | "集成关卡完成次数表全 1" | f9.meta_state.career_state().clear_counts.get(str(1), 0) == 1 and f9.meta_state.career_state().clear_counts.get(str(2), 0) == 1 and f9.meta_state.career_state().clear_counts.get(str(3), 0) == 1 | (待填) | | | +| 8 | 122 | "集成三关首杀标记齐" | f9.meta_state.career_state().boss_first_kill.has(str(1)) and f9.meta_state.career_state().boss_first_kill.has(str(2)) and f9.meta_state.career_state().boss_first_kill.has(str(3)) | (待填) | | | +| 9 | 123 | "集成 recent 1 条且 seed 42" | f9.meta_state.career_state().recent_summaries.size() == 1 and int(f9.meta_state.career_state().recent_summaries[0].get("master_seed", -1)) == 42 | (待填) | | | +| 10 | 126 | "第二局后关卡完成次数全 2" | f9.meta_state.career_state().clear_counts.get(str(1), 0) == 2 and f9.meta_state.career_state().clear_counts.get(str(2), 0) == 2 and f9.meta_state.career_state().clear_counts.get(str(3), 0) == 2 | (待填) | | | +| 11 | 127 | "第二局后余额翻倍(2 × Σ首杀——模拟驱动恒首杀口径)" | f9.meta_state.keystone_balance() == 2 * ks_run | (待填) | | | +| 12 | 128 | "集成 recent 长度 2" | f9.meta_state.career_state().recent_summaries.size() == 2 | (待填) | | | | 13 | 129 | "集成写盘成功" | f9.write_to_disk() | (待填) | | | -| 14 | 131 | "集成写盘读回全量保持" | r9.load_from_disk() and r9.keystone_balance() == 2 * ks_run and r9.clear_count(3) == 2 and r9.recent_summaries().size() == 2 and r9.boss_first_kill_marked(3) | (待填) | | | +| 14 | 131 | "集成写盘读回全量保持" | r9.load_from_disk() and r9.meta_state.keystone_balance() == 2 * ks_run and r9.meta_state.career_state().clear_counts.get(str(3), 0) == 2 and r9.meta_state.career_state().recent_summaries.size() == 2 and r9.meta_state.career_state().boss_first_kill.has(str(3)) | (待填) | | | ## S04 _smoke_playtest_logger(行 149–182 · 8 条 · 源 game/tests/integration/cross_system_flow_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| | 15 | 149 | "会话文件开启(%s)" ↩ % path | path.contains("playtest_20260912") | (待填) | | | -| 16 | 150 | "快照战斗启动" | settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}) | (待填) | | | +| 16 | 150 | "快照战斗启动" | settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}) | (待填) | | | | 17 | 154 | "每结算一快照(%d 次结算 → %d 快照)" ↩ % [2, snaps.size()] | snaps.size() == 2 | (待填) | | | | 18 | 161 | "快照字段齐备(缺 %s)" ↩ % str(missing) | missing.is_empty() | (待填) | | | -| 19 | 162 | "快照含英雄/经济/板序号(板 seq=%d)" ↩ % int(s1.board.seq) | int(s1.hero.max_hp) == int(_db.hero.max_hp) and s1.economy.has("phase") and s1.board.has("seq") | (待填) | | | +| 19 | 162 | "快照含英雄/经济/板序号(板 seq=%d)" ↩ % int(s1.board.seq) | int(s1.hero.max_hp) == int(_db.combat.hero.max_hp) and s1.economy.has("phase") and s1.board.has("seq") | (待填) | | | | 20 | 165 | "快照 spawn_count 在位且 == unit_count(无击杀轮:%d/%d——全灭/空场可判别)" ↩ % [int(s1.get("spawn_count", -1)), int(s1.get("unit_count", -1))] | int(s1.get("spawn_count", -1)) >= 1 and int(s1.get("spawn_count", -1)) == int(s1.get("unit_count", -2)) | (待填) | | | | 21 | 169 | "诊断转储文件生成(%s)" ↩ % diag | diag.contains("diag_") | (待填) | | | | 22 | 182 | "JSONL 全部行可解析(%d 行 = session + 2 结算)" ↩ % lines | lines >= 3 and parse_ok | (待填) | | | @@ -72,15 +72,15 @@ | 32 | 76 | "首局口径种子 42 开局受理(defer_entry 路径)" | adr_x.start_adventure(42, {"hero_override": {}, "treasure_unlocked": {}}) | (待填) | | | | 33 | 79 | "同种子第二次开局受理" | adr_y.start_adventure(42, {"hero_override": {}, "treasure_unlocked": {}}) | (待填) | | | | 34 | 81 | "种子 42 两次开局图逐位同图(教学局恒同图——首局固定种子的确定性承载)〔v7〕" | adr_x.runner().nodes() == adr_y.runner().nodes() | (待填) | | | -| 35 | 83 | "开局图 = L1 参数档在种子 42 下的生成图(开局无隐式取数分叉)" | adr_x.runner().nodes() == (gen.generate(_db.level_gen_params_row(1), 42) as Dictionary).get("nodes", []) | (待填) | | | +| 35 | 83 | "开局图 = L1 参数档在种子 42 下的生成图(开局无隐式取数分叉)" | adr_x.runner().nodes() == (gen.generate(_db.run.level_gen_params.row_by_level_index(1), 42) as Dictionary).get("nodes", []) | (待填) | | | | 36 | 86 | "停点断点落盘在档" | sv_w.has_breakpoint() | (待填) | | | | 37 | 89 | "断点续玩受理(代差标记键在档)〔v8〕" | adr_z.resume_adventure(bp, {"hero_override": {}, "treasure_unlocked": {}}) | (待填) | | | | 38 | 91 | "续玩重生成图与中断前逐位同图〔v7〕" | adr_z.runner().nodes() == adr_x.runner().nodes() | (待填) | | | | 39 | 95 | "缺生成图代差标记键的旧断点被显式拒绝(禁按新语义静默回放)〔v8〕" | adr_old.resume_adventure(bp_old, {}) == false | (待填) | | | -| 40 | 102 | "level 表三行均无 templates 键(模板数据已清出)〔v9〕" | tpl_free and _db.level_count() == 3 | (待填) | | | -| 41 | 107 | "新档首次冒险布尔位 = false(默认域——首局恒走固定种子 42)" | sv.is_first_adventure_done() == false | (待填) | | | -| 42 | 111 | "冒险收束点置位(三结局同写——写入只在收束挂点)" | sv.is_first_adventure_done() == true | (待填) | | | -| 43 | 113 | "置位幂等(重复收束不翻转)" | sv.is_first_adventure_done() == true | (待填) | | | +| 40 | 102 | "level 表三行均无 templates 键(模板数据已清出)〔v9〕" | tpl_free and _db.run.level.level_count() == 3 | (待填) | | | +| 41 | 107 | "新档首次冒险布尔位 = false(默认域——首局恒走固定种子 42)" | sv.meta_state.career_state().first_adventure_done == false | (待填) | | | +| 42 | 111 | "冒险收束点置位(三结局同写——写入只在收束挂点)" | sv.meta_state.career_state().first_adventure_done == true | (待填) | | | +| 43 | 113 | "置位幂等(重复收束不翻转)" | sv.meta_state.career_state().first_adventure_done == true | (待填) | | | ## S06 _init(行 27–27 · 1 条 · 源 game/tests/integration/save_rng_roundtrip_test.gd · 去向:待分类回填) @@ -119,2564 +119,2547 @@ | 59 | 108 | "V2 组合根句柄指向屏壳成员(探针挂载假设 main.X 零迁移)" | 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 | (待填) | | | | 60 | 110 | "V2 闲时会话自 _ready 即在(永非 null;迭代 27 T3——gp 影子账退役无账本成员)" | main.run_scope != null and not main.run_scope.is_adventure | (待填) | | | -## S10 _smoke_boot_chain(行 157–207 · 14 条 · 源 game/tests/system_test.gd · 去向:待分类回填) +## S10 _smoke_boot_chain(行 157–205 · 13 条 · 源 game/tests/system_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 61 | 157 | "V2a ConfigDB 就绪且表已载(is_ready+hero 表在座)〔装配生命批 V2〕" | db.is_ready and db.hero != null | (待填) | | | +| 61 | 157 | "V2a ConfigDB 就绪且表已载(is_ready+hero 表在座)〔装配生命批 V2〕" | db.is_ready and db.combat.hero != null | (待填) | | | | 62 | 162 | "V2b 设置已应用·音量(Master mute/volume_db=settings_store 现值 linear=%f)〔装配生命批 V2〕" ↩ % ss.volume_linear | AudioServer.is_bus_mute(master) == (ss.volume_linear <= 0.001) and is_equal_approx(AudioServer.get_bus_volume_db(master), linear_to_db(maxf(ss.volume_linear, 0.001))) | (待填) | | | | 63 | 166 | "V2b 设置已应用·减动效(UiKit.motion_scale=%s=settings_store.reduced_motion=%s)〔装配生命批 V2〕" ↩ % [ str(ui_kit.motion_scale), str(ss.reduced_motion)] | ui_kit.motion_scale == (0.5 if ss.reduced_motion else 1.0) | (待填) | | | | 64 | 170 | "V2c 启动态 _start_overlay.visible=true(boot 后玩家见菜单)〔装配生命批 V2〕" | main._start_overlay.visible | (待填) | | | -| 65 | 176 | "V2d boot 无断点在档(启动态不隐式激活——卡① sv2 钉死)〔装配生命批 V2·卡② mn8 改写〕" | not sm.has_breakpoint() | (待填) | | | -| 66 | 179 | "V2-s1 boot 后无激活态(active_slot=0、写盘目标空)〔卡① sv2〕" | sm.active_slot() == 0 and not sm.has_write_target() | (待填) | | | -| 67 | 181 | "V2-s2 boot 内存=默认合法空态(钥石/断点/总局数全零值)〔卡① sv2〕" | sm.keystone_balance() == 0 and not sm.has_breakpoint() and sm.adventure_total() == 0 | (待填) | | | -| 68 | 183 | "V2-s3 boot 无激活写盘拒绝(write_to_disk=false 生产入口)〔卡① sv3〕" | not sm.write_to_disk() | (待填) | | | -| 69 | 191 | "V2d 三钮在位·进入存档/设置/退出游戏(续玩钮族退役后主菜单操作组承接)〔卡② mn1/mn8〕" | main._menu_screen.entersavebtn != null and main._menu_screen.settingsbtn != null and main._menu_screen.quitbtn != null | (待填) | | | -| 70 | 195 | "V2d 四旧钮句柄零命中·%s(彻底撤出——禁隐藏态禁置灰态残留)〔卡② mn2〕" ↩ % old_btn | not (old_btn in main._menu_screen) | (待填) | | | -| 71 | 197 | "V2d 渲染面可见按钮计数==3(旧钮接回即红——渲染面锚,句柄缝盲区闭合)〔卡② mn1/mn2〕" | _count_visible_buttons(main._start_overlay) == 3 | (待填) | | | -| 72 | 200 | "V2e run_scope 非空且为闲时会话(is_adventure=false+is_inside_tree)〔装配生命批 V2〕" | main.run_scope != null and not main.run_scope.is_adventure and main.run_scope.is_inside_tree() | (待填) | | | -| 73 | 204 | "V2f 调试浮窗 boot 不自动开(菜单优先)〔2026-10-16 用户令〕" | not main.debug_overlay.visible | (待填) | | | -| 74 | 206 | "V2f 调试浮层角标发布态恒隐(2026-10-25 删写死行批)" | main.debug_badge != null and not main.debug_badge.visible | (待填) | | | +| 65 | 177 | "V2-s1 boot 后无激活态(active_slot=0、写盘目标空)〔卡① sv2〕" | sm.slots.active_slot() == 0 and not sm.slots.has_write_target() | (待填) | | | +| 66 | 179 | "V2-s2 boot 内存=默认合法空态(钥石/断点/总局数全零值)〔卡① sv2〕" | sm.meta_state.keystone_balance() == 0 and not sm.has_breakpoint() and sm.meta_state.career_state().adventure_total == 0 | (待填) | | | +| 67 | 181 | "V2-s3 boot 无激活写盘拒绝(write_to_disk=false 生产入口)〔卡① sv3〕" | not sm.write_to_disk() | (待填) | | | +| 68 | 189 | "V2d 三钮在位·进入存档/设置/退出游戏(续玩钮族退役后主菜单操作组承接)〔卡② mn1/mn8〕" | main._menu_screen.entersavebtn != null and main._menu_screen.settingsbtn != null and main._menu_screen.quitbtn != null | (待填) | | | +| 69 | 193 | "V2d 四旧钮句柄零命中·%s(彻底撤出——禁隐藏态禁置灰态残留)〔卡② mn2〕" ↩ % old_btn | not (old_btn in main._menu_screen) | (待填) | | | +| 70 | 195 | "V2d 渲染面可见按钮计数==3(旧钮接回即红——渲染面锚,句柄缝盲区闭合)〔卡② mn1/mn2〕" | _count_visible_buttons(main._start_overlay) == 3 | (待填) | | | +| 71 | 198 | "V2e run_scope 非空且为闲时会话(is_adventure=false+is_inside_tree)〔装配生命批 V2〕" | main.run_scope != null and not main.run_scope.is_adventure and main.run_scope.is_inside_tree() | (待填) | | | +| 72 | 202 | "V2f 调试浮窗 boot 不自动开(菜单优先)〔2026-10-16 用户令〕" | not main.debug_overlay.visible | (待填) | | | +| 73 | 204 | "V2f 调试浮层角标发布态恒隐(2026-10-25 删写死行批)" | main.debug_badge != null and not main.debug_badge.visible | (待填) | | | -## S11 _smoke_run_lifecycle(行 231–263 · 11 条 · 源 game/tests/system_test.gd · 去向:待分类回填) +## S11 _smoke_run_lifecycle(行 229–261 · 11 条 · 源 game/tests/system_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 75 | 231 | "V3 开局建会话(is_adventure+adventure 在座)" | main.run_scope != null and main.run_scope.is_adventure and main.run_scope.adventure != null | (待填) | | | -| 76 | 233 | "V3 新会话局态零值" | main.run_scope.hints_shown.is_empty() | (待填) | | | -| 77 | 242 | "V3 放弃受理(入口停点天然非战斗期)" | main.run_scope.adventure.abandon() | (待填) | | | -| 78 | 247 | "V3 冒险终局到达(abandon→adventure_finished 同步链)" | finished.done | (待填) | | | -| 79 | 248 | "V3 终局面板弹起" | main._end_overlay.is_open() | (待填) | | | -| 80 | 251 | "V3 会话收束(新闲时会话零冒险/关卡对象)" | not main.run_scope.is_adventure and main.run_scope.adventure == null and main.run_scope.runner == null | (待填) | | | -| 81 | 253 | "V3 局态封印/账本全复位(queue_free 物化——注入脏状态零残留)" | main.run_scope.hints_shown.is_empty() and not main.run_scope.boss_active and main.run_scope.last_gain_amount == 0 | (待填) | | | -| 82 | 255 | "V3 回开始菜单(终局出口路径)" | main._start_overlay.visible | (待填) | | | -| 83 | 257 | "V3 放弃终局清断点(无残留断点)" | not sm.has_breakpoint() | (待填) | | | -| 84 | 261 | "V3 第二连局全新会话(两连局零泄漏)" | main.run_scope.is_adventure and main.run_scope.adventure != null and main.run_scope.hints_shown.is_empty() | (待填) | | | -| 85 | 263 | "V3 直进入局(菜单隐)" | not main._start_overlay.visible | (待填) | | | +| 74 | 229 | "V3 开局建会话(is_adventure+adventure 在座)" | main.run_scope != null and main.run_scope.is_adventure and main.run_scope.adventure != null | (待填) | | | +| 75 | 231 | "V3 新会话局态零值" | main.run_scope.hints_shown.is_empty() | (待填) | | | +| 76 | 240 | "V3 放弃受理(入口停点天然非战斗期)" | main.run_scope.adventure.abandon() | (待填) | | | +| 77 | 245 | "V3 冒险终局到达(abandon→adventure_finished 同步链)" | finished.done | (待填) | | | +| 78 | 246 | "V3 终局面板弹起" | main._end_overlay.is_open() | (待填) | | | +| 79 | 249 | "V3 会话收束(新闲时会话零冒险/关卡对象)" | not main.run_scope.is_adventure and main.run_scope.adventure == null and main.run_scope.runner == null | (待填) | | | +| 80 | 251 | "V3 局态封印/账本全复位(queue_free 物化——注入脏状态零残留)" | main.run_scope.hints_shown.is_empty() and not main.run_scope.boss_active and main.run_scope.last_gain_amount == 0 | (待填) | | | +| 81 | 253 | "V3 回开始菜单(终局出口路径)" | main._start_overlay.visible | (待填) | | | +| 82 | 255 | "V3 放弃终局清断点(无残留断点)" | not sm.has_breakpoint() | (待填) | | | +| 83 | 259 | "V3 第二连局全新会话(两连局零泄漏)" | main.run_scope.is_adventure and main.run_scope.adventure != null and main.run_scope.hints_shown.is_empty() | (待填) | | | +| 84 | 261 | "V3 直进入局(菜单隐)" | not main._start_overlay.visible | (待填) | | | -## S12 _smoke_sim_guards(行 284–300 · 4 条 · 源 game/tests/system_test.gd · 去向:待分类回填) +## S12 _smoke_sim_guards(行 282–298 · 4 条 · 源 game/tests/system_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 86 | 284 | "前置:闲时会话(settlement 空 + is_adventure=false)" | main.run_scope.settlement == null and not main.run_scope.is_adventure | (待填) | | | -| 87 | 289 | "模拟运行态下调试两层入口守卫早退(开始层不收起)〔wd6〕" | main.run_scope.settlement == null and main._start_overlay.visible | (待填) | | | -| 88 | 296 | "前置:真实冒险关间态(选路停点,非战斗)" | main.run_scope.is_adventure and main.run_scope.settlement == null | (待填) | | | -| 89 | 299 | "冒险关间态下批量模拟入口守卫早退(d7 补 is_adventure)〔wd7〕" | not main._tune_sim.is_running() | (待填) | | | +| 85 | 282 | "前置:闲时会话(settlement 空 + is_adventure=false)" | main.run_scope.settlement == null and not main.run_scope.is_adventure | (待填) | | | +| 86 | 287 | "模拟运行态下调试两层入口守卫早退(开始层不收起)〔wd6〕" | main.run_scope.settlement == null and main._start_overlay.visible | (待填) | | | +| 87 | 294 | "前置:真实冒险关间态(选路停点,非战斗)" | main.run_scope.is_adventure and main.run_scope.settlement == null | (待填) | | | +| 88 | 297 | "冒险关间态下批量模拟入口守卫早退(d7 补 is_adventure)〔wd7〕" | not main._tune_sim.is_running() | (待填) | | | -## S13 _stage_faces(行 394–394 · 1 条 · 源 game/tests/system_test.gd · 去向:待分类回填) +## S13 _stage_faces(行 392–392 · 1 条 · 源 game/tests/system_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 90 | 394 | "%s(引用面违例:%s)〔装配生命批 V3〕" ↩ % [tag, bad] | bad.is_empty() | (待填) | | | +| 89 | 392 | "%s(引用面违例:%s)〔装配生命批 V3〕" ↩ % [tag, bad] | bad.is_empty() | (待填) | | | -## S14 _smoke_lifecycle_matrix(行 467–591 · 9 条 · 源 game/tests/system_test.gd · 去向:待分类回填) +## S14 _smoke_lifecycle_matrix(行 465–589 · 9 条 · 源 game/tests/system_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 91 | 467 | "V3d 活战斗驱动完毕(战斗已收场——%d 轮,finished=%s)〔装配生命批 V3〕" ↩ % [guard, str(finished.done)] | main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live() | (待填) | | | -| 92 | 482 | "V3d2 放弃受理(选路停点天然非战斗期)〔装配生命批 V3〕" | main.run_scope.adventure.abandon() | (待填) | | | -| 93 | 493 | "V3e 终局面到达(adventure_finished+终局面板开)〔装配生命批 V3〕" | finished.done and main._end_overlay.is_open() | (待填) | | | -| 94 | 499 | "V3f 收束换会话无真空窗(新闲时会话即刻在树)〔装配生命批 V3〕" | main.run_scope != old_scope and main.run_scope.is_inside_tree() | (待填) | | | -| 95 | 502 | "V3f 收束后旧局经 queue_free 消亡(is_instance_valid=false)〔装配生命批 V3〕" | not is_instance_valid(old_scope) | (待填) | | | -| 96 | 522 | "V3g 营地入口缺失(选档层新档钮驱动中断)〔装配生命批 V3〕" | false | (待填) | | | -| 97 | 560 | "V3h 两连局第二会话在座(开局停点——新冒险会话非空)〔装配生命批 V3〕" | main.run_scope.adventure != null and main.run_scope.adventure != first_adv | (待填) | | | -| 98 | 563 | "V3h 放弃受理(第二局)〔装配生命批 V3〕" | main.run_scope.adventure.abandon() | (待填) | | | -| 99 | 590 | "V3i 续玩换会话无真空窗(新冒险会话即刻在树)〔装配生命批 V3〕" | main.run_scope != idle_scope and main.run_scope.is_inside_tree() | (待填) | | | +| 90 | 465 | "V3d 活战斗驱动完毕(战斗已收场——%d 轮,finished=%s)〔装配生命批 V3〕" ↩ % [guard, str(finished.done)] | main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live() | (待填) | | | +| 91 | 480 | "V3d2 放弃受理(选路停点天然非战斗期)〔装配生命批 V3〕" | main.run_scope.adventure.abandon() | (待填) | | | +| 92 | 491 | "V3e 终局面到达(adventure_finished+终局面板开)〔装配生命批 V3〕" | finished.done and main._end_overlay.is_open() | (待填) | | | +| 93 | 497 | "V3f 收束换会话无真空窗(新闲时会话即刻在树)〔装配生命批 V3〕" | main.run_scope != old_scope and main.run_scope.is_inside_tree() | (待填) | | | +| 94 | 500 | "V3f 收束后旧局经 queue_free 消亡(is_instance_valid=false)〔装配生命批 V3〕" | not is_instance_valid(old_scope) | (待填) | | | +| 95 | 520 | "V3g 营地入口缺失(选档层新档钮驱动中断)〔装配生命批 V3〕" | false | (待填) | | | +| 96 | 558 | "V3h 两连局第二会话在座(开局停点——新冒险会话非空)〔装配生命批 V3〕" | main.run_scope.adventure != null and main.run_scope.adventure != first_adv | (待填) | | | +| 97 | 561 | "V3h 放弃受理(第二局)〔装配生命批 V3〕" | main.run_scope.adventure.abandon() | (待填) | | | +| 98 | 588 | "V3i 续玩换会话无真空窗(新冒险会话即刻在树)〔装配生命批 V3〕" | main.run_scope != idle_scope and main.run_scope.is_inside_tree() | (待填) | | | ## S15 _init(行 27–27 · 1 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 100 | 27 | "前置:表加载零 error" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | +| 99 | 27 | "前置:表加载零 error" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | ## S16 _run_recipe_sequence(行 75–166 · 11 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 101 | 75 | "前置:units.spawn(1,0) 成功(遭遇行在案)" | false | (待填) | | | -| 102 | 114 | "五配方单据 flush 后全部 redeemed(B 模式段界结账)" | outcomes_redeemed | (待填) | | | -| 103 | 119 | "主角扣血 = 敌方技行值 + 反噬行值(逐行归因闭合)" | hero_before - hero_after == int(r2.get("value", -1)) + int(r5.get("value", -1)) | (待填) | | | -| 104 | 121 | "敌方技行值 ≥ 下限 + rider(rider 后置不进乘区)" | int(r2.get("value", 0)) >= floor_v + floor_v | (待填) | | | -| 105 | 122 | "反噬行值 = 落地下限(中性无类型表值)" | int(r5.get("value", 0)) == floor_v | (待填) | | | -| 106 | 126 | "主角攻击行 AC = 防守方 AC + 攻击加成(开单前快照一致)" | int(ex1.get("ac", -1)) == threshold_expected | (待填) | | | -| 107 | 127 | "掷点 ∈ [1, 骰面]" | int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.settlement.damage_roll.roll_faces) | (待填) | | | -| 108 | 134 | "掷点档位 ∈ 表档位值 ∪ 满额" | tier_ok | (待填) | | | -| 109 | 150 | "敌方总扣血 ≥ 攻击+克制+群伤(死亡溢出合法,下界归因)" | enemy_drop_total >= attack_landed + typed_landed + int(r4.get("value", 0)) * maxi(0, alive_at_blast) | (待填) | | | -| 110 | 153 | "敌方总扣血 = 攻击 + 克制直伤 + 群伤×存活数(逐行归因闭合)" | enemy_drop_total == attack_landed + typed_landed + int(r4.get("value", 0)) * alive_at_blast | (待填) | | | -| 111 | 166 | "行协议公共字段封闭(type/source_class/display/value≥下限/extra.target)" | protocol_ok | (待填) | | | +| 100 | 75 | "前置:units.spawn(1,0) 成功(遭遇行在案)" | false | (待填) | | | +| 101 | 114 | "五配方单据 flush 后全部 redeemed(B 模式段界结账)" | outcomes_redeemed | (待填) | | | +| 102 | 119 | "主角扣血 = 敌方技行值 + 反噬行值(逐行归因闭合)" | hero_before - hero_after == int(r2.get("value", -1)) + int(r5.get("value", -1)) | (待填) | | | +| 103 | 121 | "敌方技行值 ≥ 下限 + rider(rider 后置不进乘区)" | int(r2.get("value", 0)) >= floor_v + floor_v | (待填) | | | +| 104 | 122 | "反噬行值 = 落地下限(中性无类型表值)" | int(r5.get("value", 0)) == floor_v | (待填) | | | +| 105 | 126 | "主角攻击行 AC = 防守方 AC + 攻击加成(开单前快照一致)" | int(ex1.get("ac", -1)) == threshold_expected | (待填) | | | +| 106 | 127 | "掷点 ∈ [1, 骰面]" | int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.combat.settlement.damage_roll.roll_faces) | (待填) | | | +| 107 | 134 | "掷点档位 ∈ 表档位值 ∪ 满额" | tier_ok | (待填) | | | +| 108 | 150 | "敌方总扣血 ≥ 攻击+克制+群伤(死亡溢出合法,下界归因)" | enemy_drop_total >= attack_landed + typed_landed + int(r4.get("value", 0)) * maxi(0, alive_at_blast) | (待填) | | | +| 109 | 153 | "敌方总扣血 = 攻击 + 克制直伤 + 群伤×存活数(逐行归因闭合)" | enemy_drop_total == attack_landed + typed_landed + int(r4.get("value", 0)) * alive_at_blast | (待填) | | | +| 110 | 166 | "行协议公共字段封闭(type/source_class/display/value≥下限/extra.target)" | protocol_ok | (待填) | | | ## S17 _test_five_recipes_and_replay(行 177–179 · 2 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 112 | 177 | "五配方序列产出 5 行伤害行" | run1.size() == 5 | (待填) | | | -| 113 | 179 | "同种子双跑行序列逐位一致(回放确定性契约)" | run1 == run2 | (待填) | | | +| 111 | 177 | "五配方序列产出 5 行伤害行" | run1.size() == 5 | (待填) | | | +| 112 | 179 | "同种子双跑行序列逐位一致(回放确定性契约)" | run1 == run2 | (待填) | | | ## S18 _test_policy_immediate_mode(行 193–194 · 2 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 114 | 193 | "A 模式开单即结(无 flush 即 redeemed)" | h.ctrl.outcome(t) == "redeemed" | (待填) | | | -| 115 | 194 | "A 模式主角即时扣血" | int(h.units.hero_unit().hp) == int(h.db.hero.max_hp) - floor_v | (待填) | | | +| 113 | 193 | "A 模式开单即结(无 flush 即 redeemed)" | h.ctrl.outcome(t) == "redeemed" | (待填) | | | +| 114 | 194 | "A 模式主角即时扣血" | int(h.units.hero_unit().hp) == int(h.db.combat.hero.max_hp) - floor_v | (待填) | | | ## S19 _test_source_death_void(行 213–214 · 2 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 116 | 213 | "来源敌死亡后其未结单据 voided(死亡即不动手)" | h.ctrl.outcome(t) == "voided" | (待填) | | | -| 117 | 214 | "作废单零行零扣血" | int(h.units.hero_unit().hp) == hero_before and _rows.is_empty() | (待填) | | | +| 115 | 213 | "来源敌死亡后其未结单据 voided(死亡即不动手)" | h.ctrl.outcome(t) == "voided" | (待填) | | | +| 116 | 214 | "作废单零行零扣血" | int(h.units.hero_unit().hp) == hero_before and _rows.is_empty() | (待填) | | | ## S20 _test_identical_content_dual_enemy(行 234–254 · 3 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 118 | 234 | "前置:双敌字典内容全同且非同一实例" | a == b and not is_same(a, b) | (待填) | | | -| 119 | 244 | "同内容异实例敌死亡不误废他人单据〔wd1〕" | h.ctrl.outcome(t) == "redeemed" | (待填) | | | -| 120 | 254 | "来源敌自身死亡单据作废(正向对照)〔wd1〕" | h2.ctrl.outcome(t2) == "voided" | (待填) | | | +| 117 | 234 | "前置:双敌字典内容全同且非同一实例" | a == b and not is_same(a, b) | (待填) | | | +| 118 | 244 | "同内容异实例敌死亡不误废他人单据〔wd1〕" | h.ctrl.outcome(t) == "redeemed" | (待填) | | | +| 119 | 254 | "来源敌自身死亡单据作废(正向对照)〔wd1〕" | h2.ctrl.outcome(t2) == "voided" | (待填) | | | ## S21 _test_target_id_payload(行 264–279 · 4 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 121 | 264 | "前置:units.spawn(1,0) 成功(target_id 断言)" | false | (待填) | | | -| 122 | 268 | "前置:目标敌携非空 enemy_id" | expect_id != "" | (待填) | | | -| 123 | 273 | "attack_commit fx 载荷携 target_id〔wd12〕" | String(fx_payload.get("target_id", "")) == expect_id and String(fx_payload.get("target", "")) != "" | (待填) | | | -| 124 | 278 | "伤害行携 target_id == 目标敌 enemy_id〔wd12〕" | not row.is_empty() and String(row.extra.get("target_id", "")) == expect_id | (待填) | | | +| 120 | 264 | "前置:units.spawn(1,0) 成功(target_id 断言)" | false | (待填) | | | +| 121 | 268 | "前置:目标敌携非空 enemy_id" | expect_id != "" | (待填) | | | +| 122 | 273 | "attack_commit fx 载荷携 target_id〔wd12〕" | String(fx_payload.get("target_id", "")) == expect_id and String(fx_payload.get("target", "")) != "" | (待填) | | | +| 123 | 278 | "伤害行携 target_id == 目标敌 enemy_id〔wd12〕" | not row.is_empty() and String(row.extra.get("target_id", "")) == expect_id | (待填) | | | ## S22 _test_requires_gate_and_zero(行 293–303 · 3 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 125 | 293 | "flush_submit 同步开单即结(清图批契约)" | h.ctrl.outcome(t_clear) == "redeemed" and h.units.query_alive("enemy").is_empty() | (待填) | | | -| 126 | 302 | "无存活敌 blast = 合法零兑付(zero 非 voided)" | h.ctrl.outcome(t_zero) == "zero" | (待填) | | | -| 127 | 303 | "前置单未实际有效 → 后继单 voided(requires 门)" | h.ctrl.outcome(t_gated) == "voided" | (待填) | | | +| 124 | 293 | "flush_submit 同步开单即结(清图批契约)" | h.ctrl.outcome(t_clear) == "redeemed" and h.units.query_alive("enemy").is_empty() | (待填) | | | +| 125 | 302 | "无存活敌 blast = 合法零兑付(zero 非 voided)" | h.ctrl.outcome(t_zero) == "zero" | (待填) | | | +| 126 | 303 | "前置单未实际有效 → 后继单 voided(requires 门)" | h.ctrl.outcome(t_gated) == "voided" | (待填) | | | ## S23 _test_hero_death_halt(行 319–320 · 2 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 128 | 319 | "致死单 redeemed 且主角 hp 归零" | h.ctrl.outcome(t_kill) == "redeemed" and int(h.units.hero_unit().hp) == 0 | (待填) | | | -| 129 | 320 | "主角倒下后余单 voided(flush 终止)" | h.ctrl.outcome(t_after) == "voided" | (待填) | | | +| 127 | 319 | "致死单 redeemed 且主角 hp 归零" | h.ctrl.outcome(t_kill) == "redeemed" and int(h.units.hero_unit().hp) == 0 | (待填) | | | +| 128 | 320 | "主角倒下后余单 voided(flush 终止)" | h.ctrl.outcome(t_after) == "voided" | (待填) | | | ## S24 _test_ac_merge(行 331–347 · 4 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 130 | 331 | "缺省兜底:defender_ac(hero) = hero 表值" | h.ctrl.defender_ac(h.units.hero_unit()) == base_ac | (待填) | | | -| 131 | 334 | "override 合流入参生效" | h.ctrl.defender_ac(h.units.hero_unit()) == override_v | (待填) | | | -| 132 | 342 | "前置:状态表存在 hero 侧 ac 型行" | false | (待填) | | | -| 133 | 347 | "ac 型效果叠层入 AC(层数×幅度,不入池)" | h.ctrl.defender_ac(h.units.hero_unit()) == expected | (待填) | | | +| 129 | 331 | "缺省兜底:defender_ac(hero) = hero 表值" | h.ctrl.defender_ac(h.units.hero_unit()) == base_ac | (待填) | | | +| 130 | 334 | "override 合流入参生效" | h.ctrl.defender_ac(h.units.hero_unit()) == override_v | (待填) | | | +| 131 | 342 | "前置:状态表存在 hero 侧 ac 型行" | false | (待填) | | | +| 132 | 347 | "ac 型效果叠层入 AC(层数×幅度,不入池)" | h.ctrl.defender_ac(h.units.hero_unit()) == expected | (待填) | | | ## S25 _test_pool_clamp(行 359–404 · 7 条 · 源 game/tests/unit/battle_settlement/damage_controller_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 134 | 359 | "空效果池读 0(表 clamp 域含 0 前提)" | h.ctrl.clamped_pool("hero_offense") == 0 and lo <= 0 and hi >= 0 | (待填) | | | -| 135 | 369 | "前置:hero_offense 池存在 percent 型 hero 侧行" | false | (待填) | | | -| 136 | 374 | "池值 = 单实例 L×m 加算后 clamp" | first_ok and h.ctrl.clamped_pool("hero_offense") == expected1 | (待填) | | | -| 137 | 376 | "refresh 政策重复授予受理且层数恒 1(叠层改续时裁决)——池值不变" | second_ok and h.ctrl.clamped_pool("hero_offense") == expected1 | (待填) | | | -| 138 | 387 | "ac 型效果不入池(percent 池隔离)" | h.ctrl.clamped_pool("hero_offense") == before | (待填) | | | -| 139 | 395 | "前置:状态表存在 stack 政策行" | false | (待填) | | | -| 140 | 404 | "stack 政策恰授予 max_stack 层后拒绝(满层拒绝原子)" | granted == max_n | (待填) | | | +| 133 | 359 | "空效果池读 0(表 clamp 域含 0 前提)" | h.ctrl.clamped_pool("hero_offense") == 0 and lo <= 0 and hi >= 0 | (待填) | | | +| 134 | 369 | "前置:hero_offense 池存在 percent 型 hero 侧行" | false | (待填) | | | +| 135 | 374 | "池值 = 单实例 L×m 加算后 clamp" | first_ok and h.ctrl.clamped_pool("hero_offense") == expected1 | (待填) | | | +| 136 | 376 | "refresh 政策重复授予受理且层数恒 1(叠层改续时裁决)——池值不变" | second_ok and h.ctrl.clamped_pool("hero_offense") == expected1 | (待填) | | | +| 137 | 387 | "ac 型效果不入池(percent 池隔离)" | h.ctrl.clamped_pool("hero_offense") == before | (待填) | | | +| 138 | 395 | "前置:状态表存在 stack 政策行" | false | (待填) | | | +| 139 | 404 | "stack 政策恰授予 max_stack 层后拒绝(满层拒绝原子)" | granted == max_n | (待填) | | | ## S26 _init(行 17–17 · 1 条 · 源 game/tests/unit/battle_settlement/damage_math_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 141 | 17 | "前置:settlement 表加载零 error" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | +| 140 | 17 | "前置:settlement 表加载零 error" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | ## S27 _test_pool_damage(行 34–39 · 6 条 · 源 game/tests/unit/battle_settlement/damage_math_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 142 | 34 | "中性双池 (0,0) 原样透传" | DamageMathScript.pool_damage(d, 0, 0) == d | (待填) | | | -| 143 | 35 | "负敌方池压低不增高" | DamageMathScript.pool_damage(d, 0, int(db.settlement.core.pool_clamp_lo)) <= d | (待填) | | | -| 144 | 36 | "正·主角池抬升不低于原值" | DamageMathScript.pool_damage(d, int(db.settlement.core.pool_clamp_hi), 0) >= d | (待填) | | | -| 145 | 37 | "主角池单调不减(+50% vs +49% 探针档)" | DamageMathScript.pool_damage(d, 50, 0) >= DamageMathScript.pool_damage(d, 49, 0) | (待填) | | | -| 146 | 38 | "对称性:池值互换结果不变(乘算交换)" | DamageMathScript.pool_damage(d, 30, -20) == DamageMathScript.pool_damage(d, -20, 30) | (待填) | | | -| 147 | 39 | "输出非负(maxi(0,·) 封底)" | DamageMathScript.pool_damage(d, int(db.settlement.core.pool_clamp_lo), int(db.settlement.core.pool_clamp_lo)) >= 0 | (待填) | | | +| 141 | 34 | "中性双池 (0,0) 原样透传" | DamageMathScript.pool_damage(d, 0, 0) == d | (待填) | | | +| 142 | 35 | "负敌方池压低不增高" | DamageMathScript.pool_damage(d, 0, int(db.combat.settlement.core.pool_clamp_lo)) <= d | (待填) | | | +| 143 | 36 | "正·主角池抬升不低于原值" | DamageMathScript.pool_damage(d, int(db.combat.settlement.core.pool_clamp_hi), 0) >= d | (待填) | | | +| 144 | 37 | "主角池单调不减(+50% vs +49% 探针档)" | DamageMathScript.pool_damage(d, 50, 0) >= DamageMathScript.pool_damage(d, 49, 0) | (待填) | | | +| 145 | 38 | "对称性:池值互换结果不变(乘算交换)" | DamageMathScript.pool_damage(d, 30, -20) == DamageMathScript.pool_damage(d, -20, 30) | (待填) | | | +| 146 | 39 | "输出非负(maxi(0,·) 封底)" | DamageMathScript.pool_damage(d, int(db.combat.settlement.core.pool_clamp_lo), int(db.combat.settlement.core.pool_clamp_lo)) >= 0 | (待填) | | | ## S28 _test_counter_component(行 46–50 · 4 条 · 源 game/tests/unit/battle_settlement/damage_math_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 148 | 46 | "空桶(点数≤0)恒 0 不吃修正" | DamageMathScript.counter_component(0, floor_v, floor_v) == 0 and DamageMathScript.counter_component(-1, 5, floor_v) == 0 | (待填) | | | -| 149 | 48 | "正向修正直加" | DamageMathScript.counter_component(floor_v, floor_v, floor_v) == floor_v * 2 | (待填) | | | -| 150 | 49 | "负修正触底保落地伤害下限" | DamageMathScript.counter_component(floor_v, -floor_v * 10, floor_v) == floor_v | (待填) | | | -| 151 | 50 | "下限参数缺省 = 0(无下限语义面)" | DamageMathScript.counter_component(floor_v, -floor_v * 10) == 0 | (待填) | | | +| 147 | 46 | "空桶(点数≤0)恒 0 不吃修正" | DamageMathScript.counter_component(0, floor_v, floor_v) == 0 and DamageMathScript.counter_component(-1, 5, floor_v) == 0 | (待填) | | | +| 148 | 48 | "正向修正直加" | DamageMathScript.counter_component(floor_v, floor_v, floor_v) == floor_v * 2 | (待填) | | | +| 149 | 49 | "负修正触底保落地伤害下限" | DamageMathScript.counter_component(floor_v, -floor_v * 10, floor_v) == floor_v | (待填) | | | +| 150 | 50 | "下限参数缺省 = 0(无下限语义面)" | DamageMathScript.counter_component(floor_v, -floor_v * 10) == 0 | (待填) | | | ## S29 _test_roll_discount_percent(行 59–74 · 5 条 · 源 game/tests/unit/battle_settlement/damage_math_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 152 | 59 | "前置:档位数 = 边界数 + 1(查档契约——档数 = 边界数 + 1)" | discounts.size() == bounds.size() + 1 and discounts.size() > 0 | (待填) | | | -| 153 | 60 | "掷点 ≥ 阈值恒全额 100" | DamageMathScript.roll_discount_percent(threshold, threshold, bounds, discounts) == 100 and DamageMathScript.roll_discount_percent(threshold + 5, threshold, bounds, discounts) == 100 | (待填) | | | -| 154 | 65 | "缺口 1 落首档" | DamageMathScript.roll_discount_percent(threshold - 1, threshold, bounds, discounts) == tier0_expected | (待填) | | | -| 155 | 68 | "缺口超末边界落末档" | DamageMathScript.roll_discount_percent(threshold - int(bounds[-1]) - 1, threshold, bounds, discounts) == last_expected | (待填) | | | -| 156 | 74 | "档位值随档位加深不升(折扣单调)" | descending | (待填) | | | +| 151 | 59 | "前置:档位数 = 边界数 + 1(查档契约——档数 = 边界数 + 1)" | discounts.size() == bounds.size() + 1 and discounts.size() > 0 | (待填) | | | +| 152 | 60 | "掷点 ≥ 阈值恒全额 100" | DamageMathScript.roll_discount_percent(threshold, threshold, bounds, discounts) == 100 and DamageMathScript.roll_discount_percent(threshold + 5, threshold, bounds, discounts) == 100 | (待填) | | | +| 153 | 65 | "缺口 1 落首档" | DamageMathScript.roll_discount_percent(threshold - 1, threshold, bounds, discounts) == tier0_expected | (待填) | | | +| 154 | 68 | "缺口超末边界落末档" | DamageMathScript.roll_discount_percent(threshold - int(bounds[-1]) - 1, threshold, bounds, discounts) == last_expected | (待填) | | | +| 155 | 74 | "档位值随档位加深不升(折扣单调)" | descending | (待填) | | | ## S30 _test_rolled_component(行 82–87 · 5 条 · 源 game/tests/unit/battle_settlement/damage_math_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 157 | 82 | "满额折扣无暴击 = floor(基线) + 修正 0" | DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) == int(floor(base)) | (待填) | | | -| 158 | 83 | "满额折扣暴击按表倍率放大" | DamageMathScript.rolled_component(base, 100, true, crit_mult, 0) == int(floor(base * float(crit_mult) / 100.0)) | (待填) | | | -| 159 | 84 | "折扣降低不高于满额" | DamageMathScript.rolled_component(base, 50, false, crit_mult, 0) <= DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) | (待填) | | | -| 160 | 85 | "克制修正加在 floor 之后(加法核)" | DamageMathScript.rolled_component(base, 100, false, crit_mult, _floor_val()) == int(floor(base)) + _floor_val() | (待填) | | | -| 161 | 86 | "复合序 = 基线×折扣%×暴击%(floor 只在链末)" | DamageMathScript.rolled_component(base, 50, true, crit_mult, 0) == int(floor(base * 50.0 / 100.0 * float(crit_mult) / 100.0)) | (待填) | | | +| 156 | 82 | "满额折扣无暴击 = floor(基线) + 修正 0" | DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) == int(floor(base)) | (待填) | | | +| 157 | 83 | "满额折扣暴击按表倍率放大" | DamageMathScript.rolled_component(base, 100, true, crit_mult, 0) == int(floor(base * float(crit_mult) / 100.0)) | (待填) | | | +| 158 | 84 | "折扣降低不高于满额" | DamageMathScript.rolled_component(base, 50, false, crit_mult, 0) <= DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) | (待填) | | | +| 159 | 85 | "克制修正加在 floor 之后(加法核)" | DamageMathScript.rolled_component(base, 100, false, crit_mult, _floor_val()) == int(floor(base)) + _floor_val() | (待填) | | | +| 160 | 86 | "复合序 = 基线×折扣%×暴击%(floor 只在链末)" | DamageMathScript.rolled_component(base, 50, true, crit_mult, 0) == int(floor(base * 50.0 / 100.0 * float(crit_mult) / 100.0)) | (待填) | | | ## S31 _smoke_enemy_attack_type(行 55–98 · 4 条 · 源 game/tests/unit/battle_settlement/roll_multiplier_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 162 | 55 | "敌方 %d 行 attack_type 全填 normal(迭代 24 N3 轴开列,越轨行%s)" ↩ % [ _db.enemy.rows.size(), at_detail if at_detail != "" else "无"] | at_ok | (待填) | | | -| 163 | 95 | "敌方单位快照 attack_type 非空(spawn 注入面——迭代 24 N3)" | snap_ok | (待填) | | | -| 164 | 96 | "克制矩阵命中证词:同种子三甲型实测 heavy %d == light %d − 1 · ward %d == light %d + 1(迭代 24 N3 全局平移 −1/0/+1)" ↩ % [ t_heavy, t_light, t_ward, t_light] | matrix_found and t_light >= 2 | (待填) | | | -| 165 | 98 | "敌方伤害技单据载荷 attack_type 非空(= %s——第 9 参传值面,迭代 24 N3)" ↩ % payload_at | payload_at == "normal" | (待填) | | | +| 161 | 55 | "敌方 %d 行 attack_type 全填 normal(迭代 24 N3 轴开列,越轨行%s)" ↩ % [ _db.combat.enemy.rows.size(), at_detail if at_detail != "" else "无"] | at_ok | (待填) | | | +| 162 | 95 | "敌方单位快照 attack_type 非空(spawn 注入面——迭代 24 N3)" | snap_ok | (待填) | | | +| 163 | 96 | "克制矩阵命中证词:同种子三甲型实测 heavy %d == light %d − 1 · ward %d == light %d + 1(迭代 24 N3 全局平移 −1/0/+1)" ↩ % [ t_heavy, t_light, t_ward, t_light] | matrix_found and t_light >= 2 | (待填) | | | +| 164 | 98 | "敌方伤害技单据载荷 attack_type 非空(= %s——第 9 参传值面,迭代 24 N3)" ↩ % payload_at | payload_at == "normal" | (待填) | | | -## S32 _smoke_armor_roll(行 117–178 · 15 条 · 源 game/tests/unit/battle_settlement/roll_multiplier_test.gd · 去向:待分类回填) +## S32 _smoke_armor_roll(行 117–177 · 14 条 · 源 game/tests/unit/battle_settlement/roll_multiplier_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 166 | 117 | "查档 AC%d 全掷点域分布(100/75/50/25 = %s)" ↩ % [cfg.ac, str([dist[100], dist[75], dist[50], dist[25]])] | tier_ok and dist == cfg.expect | (待填) | | | -| 167 | 121 | "AC10 EV 锚:期望系数合计 %d/2000 = 期望减免 21.25%%(nat20 恒全额含内)" ↩ % ev_sum | ev_sum == 1575 | (待填) | | | -| 168 | 131 | "nat20 种子可达(seed=%d——主角先手首掷命中 %d)" ↩ % [crit_seed, int(_db.settlement.damage_roll.crit_roll)] | crit_seed > 0 | (待填) | | | -| 169 | 132 | "nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)" | int(crit_rows.get("roll", -1)) == int(_db.settlement.damage_roll.crit_roll) and bool(crit_rows.get("crit", false)) | (待填) | | | -| 170 | 134 | "nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" ↩ % [int(crit_rows.get("normal", -1)), int(_db.hero.atk)] | int(crit_rows.get("normal", -1)) == 2 * int(_db.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.hero.atk) | (待填) | | | -| 171 | 139 | "削弱重复授予 → 层数保持 1、剩余结算数重置 2(续时语义——结构性消饱和)" | int(weaken_probe.get("layers", -1)) == 1 and int(weaken_probe.get("remaining", -1)) == 2 | (待填) | | | -| 172 | 141 | "削弱单层池读数 = 行值(enemy_output 池单层贡献——×2 叠满触底 ×0 不可达,免伤轮根治)" | int(weaken_probe.get("pool", -999)) == int(_db.status_effect_row("weaken").magnitude) | (待填) | | | -| 173 | 152 | "AC11 攻击拍逐拍判定(×%d 拍全部落档域 {100,75,50,25})" ↩ % tiers.size() | tiers_valid and tiers.size() >= 3 | (待填) | | | -| 174 | 153 | "折扣档实际流动(AC11 实测档位 %s——≥2 档可见)" ↩ % str(tier_distinct.keys()) | tier_distinct.size() >= 2 | (待填) | | | -| 175 | 170 | "全战斗流零 v=0 伤害行(伤害下限全局生效)" | all_positive | (待填) | | | -| 176 | 171 | "hero_attack/enemy_action 伤害行掷点读数+档位随行留档(P2 归因)" | attack_ok | (待填) | | | -| 177 | 172 | "棋盘来源伤害行零掷点字段(爆裂/直伤/清障炸药/奖励即时必中全额)" | board_ok | (待填) | | | -| 178 | 176 | "棋盘克制直伤贴底 → 下限 1(点数 2 vs 不利修正 −1)" | DamageMath.counter_component(2, -1, df) == 1 | (待填) | | | -| 179 | 177 | "克制修正触底穿透 → 下限 1(点数 1 vs 修正 −3)" | DamageMath.counter_component(1, -3, df) == 1 | (待填) | | | -| 180 | 178 | "空桶不吃修正恒 0(零点数分量无伤害可落地——边界不变不受下限影响)" | DamageMath.counter_component(0, 5, df) == 0 | (待填) | | | +| 165 | 117 | "查档 AC%d 全掷点域分布(100/75/50/25 = %s)" ↩ % [cfg.ac, str([dist[100], dist[75], dist[50], dist[25]])] | tier_ok and dist == cfg.expect | (待填) | | | +| 166 | 121 | "AC10 EV 锚:期望系数合计 %d/2000 = 期望减免 21.25%%(nat20 恒全额含内)" ↩ % ev_sum | ev_sum == 1575 | (待填) | | | +| 167 | 131 | "nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量——种子可达 seed=%d)" ↩ % crit_seed | int(crit_rows.get("roll", -1)) == int(_db.combat.settlement.damage_roll.crit_roll) and bool(crit_rows.get("crit", false)) | (待填) | | | +| 168 | 133 | "nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" ↩ % [int(crit_rows.get("normal", -1)), int(_db.combat.hero.atk)] | int(crit_rows.get("normal", -1)) == 2 * int(_db.combat.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.combat.hero.atk) | (待填) | | | +| 169 | 138 | "削弱重复授予 → 层数保持 1、剩余结算数重置 2(续时语义——结构性消饱和)" | int(weaken_probe.get("layers", -1)) == 1 and int(weaken_probe.get("remaining", -1)) == 2 | (待填) | | | +| 170 | 140 | "削弱单层池读数 = 行值(enemy_output 池单层贡献——×2 叠满触底 ×0 不可达,免伤轮根治)" | int(weaken_probe.get("pool", -999)) == int(_db.combat.status_effects.row_by_id("weaken").magnitude) | (待填) | | | +| 171 | 151 | "AC11 攻击拍逐拍判定(×%d 拍全部落档域 {100,75,50,25})" ↩ % tiers.size() | tiers_valid and tiers.size() >= 3 | (待填) | | | +| 172 | 152 | "折扣档实际流动(AC11 实测档位 %s——≥2 档可见)" ↩ % str(tier_distinct.keys()) | tier_distinct.size() >= 2 | (待填) | | | +| 173 | 169 | "全战斗流零 v=0 伤害行(伤害下限全局生效)" | all_positive | (待填) | | | +| 174 | 170 | "hero_attack/enemy_action 伤害行掷点读数+档位随行留档(P2 归因)" | attack_ok | (待填) | | | +| 175 | 171 | "棋盘来源伤害行零掷点字段(爆裂/直伤/清障炸药/奖励即时必中全额)" | board_ok | (待填) | | | +| 176 | 175 | "棋盘克制直伤贴底 → 下限 1(点数 2 vs 不利修正 −1)" | DamageMath.counter_component(2, -1, df) == 1 | (待填) | | | +| 177 | 176 | "克制修正触底穿透 → 下限 1(点数 1 vs 修正 −3)" | DamageMath.counter_component(1, -3, df) == 1 | (待填) | | | +| 178 | 177 | "空桶不吃修正恒 0(零点数分量无伤害可落地——边界不变不受下限影响)" | DamageMath.counter_component(0, 5, df) == 0 | (待填) | | | -## S33 _smoke_pool_wiring(行 310–317 · 4 条 · 源 game/tests/unit/battle_settlement/roll_multiplier_test.gd · 去向:待分类回填) +## S33 _smoke_pool_wiring(行 309–316 · 4 条 · 源 game/tests/unit/battle_settlement/roll_multiplier_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 181 | 310 | "单分量基线在位(normal 分量 = 落地值、基线 %d——充能退役批)" ↩ % base_v | int(base_row.get("normal", -1)) == base_v and base_v > 0 | (待填) | | | -| 182 | 312 | "增伤经乘区加成合计(%d → %d = ×1.5——attack_up 直乘旁路退役,覆盖面=攻击合计)" ↩ % [base_v, int(up_row.get("value", -1))] | int(up_row.get("value", -1)) == int(floor(base_v * 1.5)) | (待填) | | | -| 183 | 314 | "乏力经乘区削减合计(%d → %d = ×0.75)" ↩ % [base_v, int(sapped_row.get("value", -1))] | int(sapped_row.get("value", -1)) == int(floor(base_v * 0.75)) | (待填) | | | -| 184 | 316 | "硬化折半方向(%d → %d = ×0.5 且 < 基线——敌方防御池负值减伤,翻倍 bug 退役)" ↩ % [base_v, int(fortify_row.get("value", -1))] | int(fortify_row.get("value", -1)) == int(floor(base_v * 0.5)) and int(fortify_row.get("value", -1)) < base_v | (待填) | | | +| 179 | 309 | "单分量基线在位(normal 分量 = 落地值、基线 %d——充能退役批)" ↩ % base_v | int(base_row.get("normal", -1)) == base_v and base_v > 0 | (待填) | | | +| 180 | 311 | "增伤经乘区加成合计(%d → %d = ×1.5——attack_up 直乘旁路退役,覆盖面=攻击合计)" ↩ % [base_v, int(up_row.get("value", -1))] | int(up_row.get("value", -1)) == int(floor(base_v * 1.5)) | (待填) | | | +| 181 | 313 | "乏力经乘区削减合计(%d → %d = ×0.75)" ↩ % [base_v, int(sapped_row.get("value", -1))] | int(sapped_row.get("value", -1)) == int(floor(base_v * 0.75)) | (待填) | | | +| 182 | 315 | "硬化折半方向(%d → %d = ×0.5 且 < 基线——敌方防御池负值减伤,翻倍 bug 退役)" ↩ % [base_v, int(fortify_row.get("value", -1))] | int(fortify_row.get("value", -1)) == int(floor(base_v * 0.5)) and int(fortify_row.get("value", -1)) < base_v | (待填) | | | -## S34 _smoke_damage_types(行 345–379 · 10 条 · 源 game/tests/unit/battle_settlement/roll_multiplier_test.gd · 去向:待分类回填) +## S34 _smoke_damage_types(行 344–378 · 10 条 · 源 game/tests/unit/battle_settlement/roll_multiplier_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 185 | 345 | "goblin 护甲 = light(轻甲——教学位)" | enemy_system.spawn(1, 0, int(_db.hero.max_hp)) and String(enemy_system.all_enemies()[0].armor_type) == "light" | (待填) | | | -| 186 | 348 | "bugbear 护甲 = light(护甲类型重配批 2026-09-18:关卡 1 全轻甲)" | enemy_system_b.spawn(1, 2, int(_db.hero.max_hp)) and String(enemy_system_b.all_enemies()[0].armor_type) == "light" | (待填) | | | -| 187 | 351 | "ogre_chief 护甲 = light(Boss 行集——护甲类型重配批 2026-09-18:关卡 1 全轻甲)" | enemy_system_c.spawn(1, 4, int(_db.hero.max_hp), [], -1, -1, [], "", "boss") and String(enemy_system_c.all_enemies()[0].armor_type) == "light" | (待填) | | | -| 188 | 355 | "普通行:轻甲 0 / 魔法护甲 +1 / 重甲 −1(加法核)" | int(am.normal.light) == 0 and int(am.normal.ward) == 1 and int(am.normal.heavy) == -1 | (待填) | | | -| 189 | 357 | "穿刺行:轻甲 −1 / 魔法护甲 +1 / 重甲 0 中性(用户拍板)" | int(am.pierce.light) == -1 and int(am.pierce.ward) == 1 and int(am.pierce.heavy) == 0 | (待填) | | | -| 190 | 359 | "魔法行:轻甲 0 / 魔法护甲 −1 / 重甲 +1(加法核)" | int(am.magic.light) == 0 and int(am.magic.ward) == -1 and int(am.magic.heavy) == 1 | (待填) | | | -| 191 | 373 | "标错封印产生 seal_miss 归因行(×%d)" ↩ % miss_rows | miss_rows >= 1 | (待填) | | | -| 192 | 374 | "标错封印兑付该安全格资源的负向字典行(×%d——attack/defense/weaken 效果行或 heal 直伤行)" ↩ % negative_rows | negative_rows >= 1 | (待填) | | | -| 193 | 375 | "封印命中兑付该危险格资源的正向字典行(×%d——充能退役后无魔法分量,正向=格面资源)" ↩ % hit_rows | hit_rows >= 1 | (待填) | | | -| 194 | 378 | "标错惩罚轮回放断言:同种子步骤流逐行相同(类型化负向兑付确定性)" | JSON.stringify(run1.rows) == JSON.stringify(run2.rows) | (待填) | | | +| 183 | 344 | "goblin 护甲 = light(轻甲——教学位)" | enemy_system.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}) and String(enemy_system.all_enemies()[0].armor_type) == "light" | (待填) | | | +| 184 | 347 | "bugbear 护甲 = light(护甲类型重配批 2026-09-18:关卡 1 全轻甲)" | enemy_system_b.spawn({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.combat.hero.max_hp)}) and String(enemy_system_b.all_enemies()[0].armor_type) == "light" | (待填) | | | +| 185 | 350 | "ogre_chief 护甲 = light(Boss 行集——护甲类型重配批 2026-09-18:关卡 1 全轻甲)" | enemy_system_c.spawn({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "hero_armor_type": "boss"}) and String(enemy_system_c.all_enemies()[0].armor_type) == "light" | (待填) | | | +| 186 | 354 | "普通行:轻甲 0 / 魔法护甲 +1 / 重甲 −1(加法核)" | int(am.normal.light) == 0 and int(am.normal.ward) == 1 and int(am.normal.heavy) == -1 | (待填) | | | +| 187 | 356 | "穿刺行:轻甲 −1 / 魔法护甲 +1 / 重甲 0 中性(用户拍板)" | int(am.pierce.light) == -1 and int(am.pierce.ward) == 1 and int(am.pierce.heavy) == 0 | (待填) | | | +| 188 | 358 | "魔法行:轻甲 0 / 魔法护甲 −1 / 重甲 +1(加法核)" | int(am.magic.light) == 0 and int(am.magic.ward) == -1 and int(am.magic.heavy) == 1 | (待填) | | | +| 189 | 372 | "标错封印产生 seal_miss 归因行(×%d)" ↩ % miss_rows | miss_rows >= 1 | (待填) | | | +| 190 | 373 | "标错封印兑付该安全格资源的负向字典行(×%d——attack/defense/weaken 效果行或 heal 直伤行)" ↩ % negative_rows | negative_rows >= 1 | (待填) | | | +| 191 | 374 | "封印命中兑付该危险格资源的正向字典行(×%d——充能退役后无魔法分量,正向=格面资源)" ↩ % hit_rows | hit_rows >= 1 | (待填) | | | +| 192 | 377 | "标错惩罚轮回放断言:同种子步骤流逐行相同(类型化负向兑付确定性)" | JSON.stringify(run1.rows) == JSON.stringify(run2.rows) | (待填) | | | -## S35 _smoke_attack_chain_fresh_pool(行 445–460 · 3 条 · 源 game/tests/unit/battle_settlement/roll_multiplier_test.gd · 去向:待分类回填) +## S35 _smoke_attack_chain_fresh_pool(行 444–459 · 3 条 · 源 game/tests/unit/battle_settlement/roll_multiplier_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 195 | 445 | "fresh 池回归战斗启动(Boss 行)" | settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) | (待填) | | | -| 196 | 452 | "非施放攻击拍采样充足(%d 拍——增益施放行零兑付为设计语义不计)" ↩ % ticks.size() | ticks.size() >= 2 | (待填) | | | -| 197 | 460 | "fresh 池非蓄力拍 d_tick = max(1, 掷点档位基线)(Boss 攻击链健康——Bug#2 回归断言)" | all_full | (待填) | | | +| 193 | 444 | "fresh 池回归战斗启动(Boss 行)" | settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}) | (待填) | | | +| 194 | 451 | "非施放攻击拍采样充足(%d 拍——增益施放行零兑付为设计语义不计)" ↩ % ticks.size() | ticks.size() >= 2 | (待填) | | | +| 195 | 459 | "fresh 池非蓄力拍 d_tick = max(1, 掷点档位基线)(Boss 攻击链健康——Bug#2 回归断言)" | all_full | (待填) | | | -## S36 _smoke_full_battle(行 51–79 · 7 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) +## S36 _smoke_full_battle(行 51–78 · 6 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 198 | 51 | "%s 战斗在轮次上限内终局(%d 轮,%s)" ↩ % [label, run1.rounds, run1.result] | run1.terminated | (待填) | | | -| 199 | 59 | "%s 回放断言:结算步骤流逐行相同" ↩ % label | JSON.stringify(run1.stream) == JSON.stringify(run2.stream) | (待填) | | | -| 200 | 60 | "%s 回放断言:事件流逐位相同" ↩ % label | JSON.stringify(run1.events) == JSON.stringify(run2.events) | (待填) | | | -| 201 | 61 | "%s 结算步骤流非空(%d 行)" ↩ % [label, run1.stream.size()] | run1.stream.size() > 0 | (待填) | | | -| 202 | 75 | "%s 步骤流含判定行" ↩ % label | has_verdict | (待填) | | | -| 203 | 78 | "%s 主角先手攻击行必存在(2026-09-11 拍板① + 案C 地图账殿后)×%d" ↩ % [label, hero_attacks] | hero_attacks >= 1 | (待填) | | | -| 204 | 79 | "%s 主角攻击行携带类型键与掷点读数(单分量——充能退役批)" ↩ % label | hero_breakdown_ok | (待填) | | | +| 196 | 51 | "%s 战斗在轮次上限内终局(%d 轮,%s)" ↩ % [label, run1.rounds, run1.result] | run1.terminated | (待填) | | | +| 197 | 59 | "%s 回放断言:结算步骤流逐行相同" ↩ % label | JSON.stringify(run1.stream) == JSON.stringify(run2.stream) | (待填) | | | +| 198 | 60 | "%s 回放断言:事件流逐位相同" ↩ % label | JSON.stringify(run1.events) == JSON.stringify(run2.events) | (待填) | | | +| 199 | 74 | "%s 步骤流含判定行" ↩ % label | has_verdict | (待填) | | | +| 200 | 77 | "%s 主角先手攻击行必存在(2026-09-11 拍板① + 案C 地图账殿后)×%d" ↩ % [label, hero_attacks] | hero_attacks >= 1 | (待填) | | | +| 201 | 78 | "%s 主角攻击行携带类型键与掷点读数(单分量——充能退役批)" ↩ % label | hero_breakdown_ok | (待填) | | | -## S37 _run_battle(行 111–111 · 1 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) +## S37 _run_battle(行 110–110 · 1 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 205 | 111 | "战斗收束后行动经济相位回 Idle(notify_combat_end 生效,实测=%s)" ↩ % String(action_economy.phase) | action_economy.phase == "Idle" | (待填) | | | +| 202 | 110 | "战斗收束后行动经济相位回 Idle(notify_combat_end 生效,实测=%s)" ↩ % String(action_economy.phase) | action_economy.phase == "Idle" | (待填) | | | -## S38 _smoke_active_refresh(行 160–215 · 17 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) +## S38 _smoke_active_refresh(行 159–214 · 17 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 206 | 160 | "轮内刷新战斗启动(Boss 行 150HP——承载多轮样本)" | settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) | (待填) | | | -| 207 | 166 | "三轮停手攒 RAP=%d(round=%d rap=%d)" ↩ % [rap_banked, settlement.round_index, settlement.action_economy.rap] | settlement.round_index == 4 and settlement.action_economy.rap == rap_banked | (待填) | | | -| 208 | 167 | "第 4 轮 Active 相位" | settlement.action_economy.phase == "Active" | (待填) | | | -| 209 | 177 | "轮内第 1 次刷新受理" | ok1 | (待填) | | | -| 210 | 178 | "相位保持 Active、AP 保留不重置(ap=%d)" ↩ % settlement.action_economy.ap | settlement.action_economy.phase == "Active" and settlement.action_economy.ap == ap_before | (待填) | | | -| 211 | 179 | "RAP -刷新价(%d)" ↩ % settlement.action_economy.rap | settlement.action_economy.rap == rap_banked - int(_db.action_economy.rap_refresh_cost) | (待填) | | | -| 212 | 180 | "当场换新图(board_changed +1)" | int(ct["board_swaps"]) == swaps_before + 1 and minesweeper.current_board() != board_before | (待填) | | | -| 213 | 186 | "轮内第 2 次刷新受理(RAP=%d)" ↩ % settlement.action_economy.rap | ok2 | (待填) | | | -| 214 | 192 | "第 3 次刷新被拒(每轮兜底上限 2——RAP %d 足额仍拒)" ↩ % settlement.action_economy.rap | not ok3 | (待填) | | | -| 215 | 194 | "V5 快照刷新计数=2(受理单源——main 副本已删)〔迭代27 T3〕" | int(settlement.presentation_snapshot().get("refresh_used", -1)) == 2 | (待填) | | | -| 216 | 195 | "V5 上限达门禁一致(can_refresh 与快照计数同源)〔迭代27 T3〕" | not settlement.can_refresh() | (待填) | | | -| 217 | 207 | "轮 4 挖至 AP 耗尽自动结算(round=%d result='%s')" ↩ % [settlement.round_index, settlement.battle_result] | settlement.round_index == 5 and settlement.battle_result == "" | (待填) | | | -| 218 | 208 | "跨图 facts 完整兑现(safe_cell 结算行 %d == safe 事件 %d——快照批次隔离)" ↩ % [int(ct["safe_rows"]), int(ct["safe_events"])] | int(ct["safe_rows"]) == int(ct["safe_events"]) | (待填) | | | -| 219 | 209 | "跨图挖掘 safe 事件 ≥3(%d——断言非空转)" ↩ % int(ct["safe_events"]) | int(ct["safe_events"]) >= 3 | (待填) | | | -| 220 | 210 | "结算段内(Waiting 相位)刷新被拒(等待期换图通道已移除)" | bool(ct["waiting_refresh_rejected"]) and bool(ct["waiting_probe_hit"]) | (待填) | | | -| 221 | 214 | "轮内刷新回放断言:同种子两遍步骤流逐行相同" | JSON.stringify(rep1) == JSON.stringify(rep2) | (待填) | | | -| 222 | 215 | "轮内刷新步骤流非空(%d 行)" ↩ % rep1.size() | rep1.size() > 0 | (待填) | | | +| 203 | 159 | "轮内刷新战斗启动(Boss 行 150HP——承载多轮样本)" | settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}) | (待填) | | | +| 204 | 165 | "三轮停手攒 RAP=%d(round=%d rap=%d)" ↩ % [rap_banked, settlement.round_index, settlement.action_economy.rap] | settlement.round_index == 4 and settlement.action_economy.rap == rap_banked | (待填) | | | +| 205 | 166 | "第 4 轮 Active 相位" | settlement.action_economy.phase == "Active" | (待填) | | | +| 206 | 176 | "轮内第 1 次刷新受理" | ok1 | (待填) | | | +| 207 | 177 | "相位保持 Active、AP 保留不重置(ap=%d)" ↩ % settlement.action_economy.ap | settlement.action_economy.phase == "Active" and settlement.action_economy.ap == ap_before | (待填) | | | +| 208 | 178 | "RAP -刷新价(%d)" ↩ % settlement.action_economy.rap | settlement.action_economy.rap == rap_banked - int(_db.run.action_economy.rap_refresh_cost) | (待填) | | | +| 209 | 179 | "当场换新图(board_changed +1)" | int(ct["board_swaps"]) == swaps_before + 1 and minesweeper.current_board() != board_before | (待填) | | | +| 210 | 185 | "轮内第 2 次刷新受理(RAP=%d)" ↩ % settlement.action_economy.rap | ok2 | (待填) | | | +| 211 | 191 | "第 3 次刷新被拒(每轮兜底上限 2——RAP %d 足额仍拒)" ↩ % settlement.action_economy.rap | not ok3 | (待填) | | | +| 212 | 193 | "V5 快照刷新计数=2(受理单源——main 副本已删)〔迭代27 T3〕" | int(settlement.presentation_snapshot().get("refresh_used", -1)) == 2 | (待填) | | | +| 213 | 194 | "V5 上限达门禁一致(can_refresh 与快照计数同源)〔迭代27 T3〕" | not settlement.can_refresh() | (待填) | | | +| 214 | 206 | "轮 4 挖至 AP 耗尽自动结算(round=%d result='%s')" ↩ % [settlement.round_index, settlement.battle_result] | settlement.round_index == 5 and settlement.battle_result == "" | (待填) | | | +| 215 | 207 | "跨图 facts 完整兑现(safe_cell 结算行 %d == safe 事件 %d——快照批次隔离)" ↩ % [int(ct["safe_rows"]), int(ct["safe_events"])] | int(ct["safe_rows"]) == int(ct["safe_events"]) | (待填) | | | +| 216 | 208 | "跨图挖掘 safe 事件 ≥3(%d——断言非空转)" ↩ % int(ct["safe_events"]) | int(ct["safe_events"]) >= 3 | (待填) | | | +| 217 | 209 | "结算段内(Waiting 相位)刷新被拒(等待期换图通道已移除)" | bool(ct["waiting_refresh_rejected"]) and bool(ct["waiting_probe_hit"]) | (待填) | | | +| 218 | 213 | "轮内刷新回放断言:同种子两遍步骤流逐行相同" | JSON.stringify(rep1) == JSON.stringify(rep2) | (待填) | | | +| 219 | 214 | "轮内刷新步骤流非空(%d 行)" ↩ % rep1.size() | rep1.size() > 0 | (待填) | | | -## S39 _smoke_encounter_roll(行 254–312 · 8 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) +## S39 _smoke_encounter_roll(行 253–311 · 8 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 223 | 254 | "遭遇扫描战斗启动(quiet 计数档——计数入总账,成败由后续 spawn 断言归因)" | settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}) | (待填) | | | -| 224 | 257 | "D0 区间 [1,2] 两值均出现(分布 %s)" ↩ % str(counts) | counts.has(1) and counts.has(2) | (待填) | | | -| 225 | 263 | "全部计数在域内 [1,2](分布 %s)" ↩ % str(counts) | all_in_domain | (待填) | | | -| 226 | 267 | "同种子遭遇骰可回放(%s == %s)" ↩ % [str(r1), str(r2)] | r1 == r2 | (待填) | | | -| 227 | 272 | "D1 战斗启动" | settlement_d1.begin_battle({"level_index": 1, "node_depth": 1, "hero_hp": int(_db.hero.max_hp)}) | (待填) | | | -| 228 | 274 | "D1 区间 [2,3] 域内(%d 只)" ↩ % n1 | n1 >= 2 and n1 <= 3 | (待填) | | | -| 229 | 287 | "熊地精首遇带(1,battle,%d——扫表取最低带)固定 1 不加骰〔fv23〕" ↩ % bugbear_lead_band | settlement_d2.presentation_snapshot().enemies.size() == 1 \\ and int(bugbear_lead_row.get("unit_count", -1)) == 1 and not bugbear_lead_row.has("unit_count_max") | (待填) | | | -| 230 | 312 | "全深度 × 12 种子 spawn 全域内(%d 场,∈[unit_count, unit_count_max] 且 ≥1)——空遭遇不可达(Bug#1 回归)" ↩ % scan_total | scan_bad == 0 | (待填) | | | +| 220 | 253 | "遭遇扫描战斗启动(quiet 计数档——计数入总账,成败由后续 spawn 断言归因)" | settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}) | (待填) | | | +| 221 | 256 | "D0 区间 [1,2] 两值均出现(分布 %s)" ↩ % str(counts) | counts.has(1) and counts.has(2) | (待填) | | | +| 222 | 262 | "全部计数在域内 [1,2](分布 %s)" ↩ % str(counts) | all_in_domain | (待填) | | | +| 223 | 266 | "同种子遭遇骰可回放(%s == %s)" ↩ % [str(r1), str(r2)] | r1 == r2 | (待填) | | | +| 224 | 271 | "D1 战斗启动" | settlement_d1.begin_battle({"level_index": 1, "node_depth": 1, "hero_hp": int(_db.combat.hero.max_hp)}) | (待填) | | | +| 225 | 273 | "D1 区间 [2,3] 域内(%d 只)" ↩ % n1 | n1 >= 2 and n1 <= 3 | (待填) | | | +| 226 | 286 | "熊地精首遇带(1,battle,%d——扫表取最低带)固定 1 不加骰〔fv23〕" ↩ % bugbear_lead_band | settlement_d2.presentation_snapshot().enemies.size() == 1 \\ and int(bugbear_lead_row.get("unit_count", -1)) == 1 and not bugbear_lead_row.has("unit_count_max") | (待填) | | | +| 227 | 311 | "全深度 × 12 种子 spawn 全域内(%d 场,∈[unit_count, unit_count_max] 且 ≥1)——空遭遇不可达(Bug#1 回归)" ↩ % scan_total | scan_bad == 0 | (待填) | | | -## S40 _smoke_settlement_segments(行 380–445 · 5 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) +## S40 _smoke_settlement_segments(行 379–444 · 5 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 231 | 380 | "segment_order 五段冻结在位(表值 = 拍板序恒等)" | Array(_db.settlement.segment_order) == SettlementTable.FROZEN_SEGMENT_ORDER and SettlementTable.FROZEN_SEGMENT_ORDER == ["grant", "buff_debuff", "combat", "decay", "verdict"] | (待填) | | | -| 232 | 391 | "行型封闭 10 类(删五类零残留:item/consumed_ineffective/use_rejected/window_open/window_skip)" | legacy_total == 0 | (待填) | | | -| 233 | 418 | "每轮行相位单调(授予→增益/减益→战斗→衰减→判定)" | order_ok | (待填) | | | -| 234 | 444 | "案C 段内序:地图伤害账殿后(主角先手→敌方逐个→地图账;连珠矢随先手段)" | mapdmg_ok | (待填) | | | -| 235 | 445 | "衰减/判定段每结算恰一段(先兑付后衰减——含末轮)" | decay_verdict_ok | (待填) | | | +| 228 | 379 | "segment_order 五段冻结在位(表值 = 拍板序恒等)" | Array(_db.combat.settlement.segment_order) == SettlementTable.FROZEN_SEGMENT_ORDER and SettlementTable.FROZEN_SEGMENT_ORDER == ["grant", "buff_debuff", "combat", "decay", "verdict"] | (待填) | | | +| 229 | 390 | "行型封闭 10 类(删五类零残留:item/consumed_ineffective/use_rejected/window_open/window_skip)" | legacy_total == 0 | (待填) | | | +| 230 | 417 | "每轮行相位单调(授予→增益/减益→战斗→衰减→判定)" | order_ok | (待填) | | | +| 231 | 443 | "案C 段内序:地图伤害账殿后(主角先手→敌方逐个→地图账;连珠矢随先手段)" | mapdmg_ok | (待填) | | | +| 232 | 444 | "衰减/判定段每结算恰一段(先兑付后衰减——含末轮)" | decay_verdict_ok | (待填) | | | -## S41 _smoke_skill_pool_data(行 496–682 · 28 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) +## S41 _smoke_skill_pool_data(行 495–681 · 28 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 236 | 496 | "技能表十六行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动 + 震地怒击/遏毒/岩崩 + 毒吐/盾击——前三批 + Boss 半血解锁技三行〔迭代14④ T5〕+ 普通敌错峰两行〔迭代 24 N2〕+ 触发条件批示范两行〔孤注守势/趁隙偷袭〕)" | _db.skill.rows.size() == 16 | (待填) | | | -| 237 | 498 | "普攻行:倍率恒 1 · 冷却恒 0(域校验锁死——池恒非空的结构前提)" | not basic.is_empty() and String(basic.type) == "basic" and int(basic.cooldown_beats) == 0 and (not basic.has("damage_mult") or is_equal_approx(float(basic.damage_mult), 1.0)) | (待填) | | | -| 238 | 502 | "突刺行(伤害 ×1.5 · 冷却 2——提案 §2.5 建议池新行)" | not lunge.is_empty() and is_equal_approx(float(lunge.damage_mult), 1.5) and int(lunge.cooldown_beats) == 2 | (待填) | | | -| 239 | 504 | "蓄力行 = 纯攻击增益技(0 伤害、buff_ref → empower——与重击无组合语义)" | not charge_row.is_empty() and String(charge_row.buff_ref) == "empower" | (待填) | | | -| 240 | 506 | "撕咬行(伤害 ×1.5 · 冷却 1——巨蜘蛛池载体,与突刺同倍率错开冷却轴)" | not bite.is_empty() and is_equal_approx(float(bite.damage_mult), 1.5) and int(bite.cooldown_beats) == 1 | (待填) | | | -| 241 | 508 | "毒爆行(伤害 ×2.5 · 冷却 4 · 必中附加 ×0.75——蛛化卓尔精灵专属池独占技,新效应词汇批)" | not venom.is_empty() and is_equal_approx(float(venom.damage_mult), 2.5) and int(venom.cooldown_beats) == 4 and is_equal_approx(float(venom.rider_mult), 0.75) | (待填) | | | -| 242 | 516 | "毒爆独占性(仅蛛化卓尔精灵池引用——Boss 专属池独占技纪律)" | venom_sole_ok | (待填) | | | -| 243 | 519 | "蛛网束缚行(妨害型 · buff_ref → off_guard 失衡 · duration 2 = 1 有效拍 · 冷却 4——新效应词汇批首发)" | not snare.is_empty() and String(snare.type) == "debuff" and String(snare.buff_ref) == "off_guard" and int(snare.buff_duration) == 2 and int(snare.cooldown_beats) == 4 | (待填) | | | -| 244 | 527 | "蛛网束缚独占性(仅蛛化卓尔精灵池引用——实装面收窄,其余敌种不动)" | snare_sole_ok | (待填) | | | -| 245 | 529 | "失衡状态行(hero · ac 轴——妨害技落点,ConfigDB side 守卫正例)" | not off_guard_row.is_empty() and String(off_guard_row.target_side) == "hero" and String(off_guard_row.unit) == "ac" | (待填) | | | -| 246 | 532 | "毒吐行(伤害 ×1.6 · 冷却 2 · 无 rider——低 atk 下 rider 恒被 damage_floor 兜成 1 白占字段,迭代 24 N2)" | not spit.is_empty() and String(spit.display_name) == "毒吐" and String(spit.type) == "damage" \\ and is_equal_approx(float(spit.damage_mult), 1.6) and int(spit.cooldown_beats) == 2 and not spit.has("rider_mult") | (待填) | | | -| 247 | 536 | "盾击行(伤害 ×1.7 · 冷却 3 · 无 rider——迭代 24 N2)" | not bash.is_empty() and String(bash.display_name) == "盾击" and String(bash.type) == "damage" \\ and is_equal_approx(float(bash.damage_mult), 1.7) and int(bash.cooldown_beats) == 3 and not bash.has("rider_mult") | (待填) | | | -| 248 | 550 | "毒吐投放面(仅 depth_band > 巨蜘蛛首遇带(%d)的巨蜘蛛族行——首遇带池不动,迭代 24 N2/Q6)〔fv24〕" ↩ % spider_lead_band | spit_sole_ok | (待填) | | | -| 249 | 551 | "盾击投放面(仅 (3,·) 灰矮人族——钩爪恐魔零触碰,迭代 24 N2)" | bash_sole_ok | (待填) | | | -| 250 | 559 | "巨蜘蛛首遇带行 pool 恒 {attack:6, bite:2, charge:1}(教学位池零新招——毒吐首投在更深的带)〔fv24〕" | spider_lead_pool == {"attack": 6, "bite": 2, "charge": 1} | (待填) | | | -| 251 | 605 | "二十八行九种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)" | pools_ok | (待填) | | | -| 252 | 615 | "每池恰一行普攻(空池不可能的加载校验同款)" | basic_count_ok | (待填) | | | -| 253 | 632 | "逐池增益 ≤1 · 妨害 ≤1(池构成纪律——迭代 24 N2 毒吐/盾击投放后复查)" | comp_ok | (待填) | | | -| 254 | 643 | "每池至少一行无条件特殊技(触发条件批池构成纪律——逐池复查)〔触发器批 V7〕" | uncond_ok | (待填) | | | -| 255 | 648 | "前置:卓尔 Boss 战受理(2,9——解锁技 venom_curb 门槛 50)" | sett_u.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) | (待填) | | | -| 256 | 651 | "前置:Boss 压到阈值 65(=round(130×0.5))" | int(boss_u.hp) == 65 | (待填) | | | -| 257 | 657 | "解锁归因行恰一次(×%d——闩去重,二次检查零行)" ↩ % unlock_rows.size() | unlock_rows.size() == 1 | (待填) | | | -| 258 | 659 | "解锁行文本 =「蛛化卓尔精灵 解锁了 遏毒」(高光键 skill_unlock——呈现层一次性技能名 toast 路由键)" | String(unlock_rows[0].get("display_name", "")) == "蛛化卓尔精灵 解锁了 遏毒" | (待填) | | | -| 259 | 660 | "解锁行型 = highlight 既有行型(零行型新增——行协议封闭)" | String(unlock_rows[0].get("type", "")) == "highlight" | (待填) | | | -| 260 | 674 | "全深度实例 spawn 后可摇集合恒非空(普攻恒冷却 0——容器级断言,L1/L2 双关卡)" | rollable_ok | (待填) | | | -| 261 | 678 | "蓄力状态行 empower(enemy +100 percent——敌方输出 ×2 走既有 enemy_output 池)" | not empower.is_empty() and String(empower.target_side) == "enemy" and int(empower.magnitude) == 100 | (待填) | | | -| 262 | 679 | "硬化状态行 fortify(enemy −50 percent——敌方防御池负值 = 主角攻击折半;+100 符号 bug 2026-09-18 纠正,provisional 归 状态层重设计批)" | not fortify.is_empty() and String(fortify.target_side) == "enemy" and int(fortify.magnitude) == -50 | (待填) | | | -| 263 | 680 | "effect_pool_map 收编 empower/fortify(敌方防御池 enemy_defense 入表)" | String(_db.settlement.core.effect_pool_map.get("empower", "")) == "enemy_output" \\ and String(_db.settlement.core.effect_pool_map.get("fortify", "")) == "enemy_defense" | (待填) | | | +| 233 | 495 | "技能表十六行(普攻/突刺/蓄力/重击/硬化 + 撕咬/毒爆/蛛网束缚 + 大地脉动 + 震地怒击/遏毒/岩崩 + 毒吐/盾击——前三批 + Boss 半血解锁技三行〔迭代14④ T5〕+ 普通敌错峰两行〔迭代 24 N2〕+ 触发条件批示范两行〔孤注守势/趁隙偷袭〕)" | _db.combat.skill.rows.size() == 16 | (待填) | | | +| 234 | 497 | "普攻行:倍率恒 1 · 冷却恒 0(域校验锁死——池恒非空的结构前提)" | not basic.is_empty() and String(basic.type) == "basic" and int(basic.cooldown_beats) == 0 and (not basic.has("damage_mult") or is_equal_approx(float(basic.damage_mult), 1.0)) | (待填) | | | +| 235 | 501 | "突刺行(伤害 ×1.5 · 冷却 2——提案 §2.5 建议池新行)" | not lunge.is_empty() and is_equal_approx(float(lunge.damage_mult), 1.5) and int(lunge.cooldown_beats) == 2 | (待填) | | | +| 236 | 503 | "蓄力行 = 纯攻击增益技(0 伤害、buff_ref → empower——与重击无组合语义)" | not charge_row.is_empty() and String(charge_row.buff_ref) == "empower" | (待填) | | | +| 237 | 505 | "撕咬行(伤害 ×1.5 · 冷却 1——巨蜘蛛池载体,与突刺同倍率错开冷却轴)" | not bite.is_empty() and is_equal_approx(float(bite.damage_mult), 1.5) and int(bite.cooldown_beats) == 1 | (待填) | | | +| 238 | 507 | "毒爆行(伤害 ×2.5 · 冷却 4 · 必中附加 ×0.75——蛛化卓尔精灵专属池独占技,新效应词汇批)" | not venom.is_empty() and is_equal_approx(float(venom.damage_mult), 2.5) and int(venom.cooldown_beats) == 4 and is_equal_approx(float(venom.rider_mult), 0.75) | (待填) | | | +| 239 | 515 | "毒爆独占性(仅蛛化卓尔精灵池引用——Boss 专属池独占技纪律)" | venom_sole_ok | (待填) | | | +| 240 | 518 | "蛛网束缚行(妨害型 · buff_ref → off_guard 失衡 · duration 2 = 1 有效拍 · 冷却 4——新效应词汇批首发)" | not snare.is_empty() and String(snare.type) == "debuff" and String(snare.buff_ref) == "off_guard" and int(snare.buff_duration) == 2 and int(snare.cooldown_beats) == 4 | (待填) | | | +| 241 | 526 | "蛛网束缚独占性(仅蛛化卓尔精灵池引用——实装面收窄,其余敌种不动)" | snare_sole_ok | (待填) | | | +| 242 | 528 | "失衡状态行(hero · ac 轴——妨害技落点,ConfigDB side 守卫正例)" | not off_guard_row.is_empty() and String(off_guard_row.target_side) == "hero" and String(off_guard_row.unit) == "ac" | (待填) | | | +| 243 | 531 | "毒吐行(伤害 ×1.6 · 冷却 2 · 无 rider——低 atk 下 rider 恒被 damage_floor 兜成 1 白占字段,迭代 24 N2)" | not spit.is_empty() and String(spit.display_name) == "毒吐" and String(spit.type) == "damage" \\ and is_equal_approx(float(spit.damage_mult), 1.6) and int(spit.cooldown_beats) == 2 and not spit.has("rider_mult") | (待填) | | | +| 244 | 535 | "盾击行(伤害 ×1.7 · 冷却 3 · 无 rider——迭代 24 N2)" | not bash.is_empty() and String(bash.display_name) == "盾击" and String(bash.type) == "damage" \\ and is_equal_approx(float(bash.damage_mult), 1.7) and int(bash.cooldown_beats) == 3 and not bash.has("rider_mult") | (待填) | | | +| 245 | 549 | "毒吐投放面(仅 depth_band > 巨蜘蛛首遇带(%d)的巨蜘蛛族行——首遇带池不动,迭代 24 N2/Q6)〔fv24〕" ↩ % spider_lead_band | spit_sole_ok | (待填) | | | +| 246 | 550 | "盾击投放面(仅 (3,·) 灰矮人族——钩爪恐魔零触碰,迭代 24 N2)" | bash_sole_ok | (待填) | | | +| 247 | 558 | "巨蜘蛛首遇带行 pool 恒 {attack:6, bite:2, charge:1}(教学位池零新招——毒吐首投在更深的带)〔fv24〕" | spider_lead_pool == {"attack": 6, "bite": 2, "charge": 1} | (待填) | | | +| 248 | 604 | "二十八行九种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)" | pools_ok | (待填) | | | +| 249 | 614 | "每池恰一行普攻(空池不可能的加载校验同款)" | basic_count_ok | (待填) | | | +| 250 | 631 | "逐池增益 ≤1 · 妨害 ≤1(池构成纪律——迭代 24 N2 毒吐/盾击投放后复查)" | comp_ok | (待填) | | | +| 251 | 642 | "每池至少一行无条件特殊技(触发条件批池构成纪律——逐池复查)〔触发器批 V7〕" | uncond_ok | (待填) | | | +| 252 | 647 | "前置:卓尔 Boss 战受理(2,9——解锁技 venom_curb 门槛 50)" | sett_u.begin_battle({"level_index": 2, "node_depth": 9, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}) | (待填) | | | +| 253 | 650 | "前置:Boss 压到阈值 65(=round(130×0.5))" | int(boss_u.hp) == 65 | (待填) | | | +| 254 | 656 | "解锁归因行恰一次(×%d——闩去重,二次检查零行)" ↩ % unlock_rows.size() | unlock_rows.size() == 1 | (待填) | | | +| 255 | 658 | "解锁行文本 =「蛛化卓尔精灵 解锁了 遏毒」(高光键 skill_unlock——呈现层一次性技能名 toast 路由键)" | String(unlock_rows[0].get("display_name", "")) == "蛛化卓尔精灵 解锁了 遏毒" | (待填) | | | +| 256 | 659 | "解锁行型 = highlight 既有行型(零行型新增——行协议封闭)" | String(unlock_rows[0].get("type", "")) == "highlight" | (待填) | | | +| 257 | 673 | "全深度实例 spawn 后可摇集合恒非空(普攻恒冷却 0——容器级断言,L1/L2 双关卡)" | rollable_ok | (待填) | | | +| 258 | 677 | "蓄力状态行 empower(enemy +100 percent——敌方输出 ×2 走既有 enemy_output 池)" | not empower.is_empty() and String(empower.target_side) == "enemy" and int(empower.magnitude) == 100 | (待填) | | | +| 259 | 678 | "硬化状态行 fortify(enemy −50 percent——敌方防御池负值 = 主角攻击折半;+100 符号 bug 2026-09-18 纠正,provisional 归 状态层重设计批)" | not fortify.is_empty() and String(fortify.target_side) == "enemy" and int(fortify.magnitude) == -50 | (待填) | | | +| 260 | 679 | "effect_pool_map 收编 empower/fortify(敌方防御池 enemy_defense 入表)" | String(_db.combat.settlement.core.effect_pool_map.get("empower", "")) == "enemy_output" \\ and String(_db.combat.settlement.core.effect_pool_map.get("fortify", "")) == "enemy_defense" | (待填) | | | -## S42 _smoke_skill_pool_replay_independence(行 715–721 · 2 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) +## S42 _smoke_skill_pool_replay_independence(行 714–720 · 2 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 264 | 715 | "多实例种子可达(seed=%d——D3 遭遇 ≥2 实例)" ↩ % seed_val | seed_val > 0 | (待填) | | | -| 265 | 721 | "同种子回放:各实例摇定动作序列逐位相同(×%d 轮 × %d 实例——子流独立复放面随并流退役)" ↩ % [rounds, int(run1.count)] | not run1.is_empty() and JSON.stringify(run1.seqs) == JSON.stringify(run2.seqs) | (待填) | | | +| 261 | 714 | "多实例种子可达(seed=%d——D3 遭遇 ≥2 实例)" ↩ % seed_val | seed_val > 0 | (待填) | | | +| 262 | 720 | "同种子回放:各实例摇定动作序列逐位相同(×%d 轮 × %d 实例——子流独立复放面随并流退役)" ↩ % [rounds, int(run1.count)] | not run1.is_empty() and JSON.stringify(run1.seqs) == JSON.stringify(run2.seqs) | (待填) | | | -## S43 _smoke_skill_pool_ev_anchor(行 809–809 · 1 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) +## S43 _smoke_skill_pool_ev_anchor(行 808–808 · 1 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 266 | 809 | "EV 抽样对锚 %s(实测 %.4f ∈ [%.2f, %.2f] 含闸长期速率,×%d 拍)" ↩ % [eid, measured, float(band[0]), float(band[1]), n_beats] | in_band | (待填) | | | +| 263 | 808 | "EV 抽样对锚 %s(实测 %.4f ∈ [%.2f, %.2f] 含闸长期速率,×%d 拍)" ↩ % [eid, measured, float(band[0]), float(band[1]), n_beats] | in_band | (待填) | | | -## S44 _smoke_skill_pool_death_void(行 829–848 · 5 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) +## S44 _smoke_skill_pool_death_void(行 828–847 · 5 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 267 | 829 | "死亡打断种子可达(seed=%d——D2 首拍摇中特殊技)" ↩ % void_seed | void_seed > 0 | (待填) | | | -| 268 | 835 | "死亡打断战斗启动(hero_atk 999——先手必杀 40HP 熊地精)" | settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999}) | (待填) | | | -| 269 | 838 | "开轮已摇定本拍动作(待发 = %s)" ↩ % pending | pending != "" | (待填) | | | -| 270 | 844 | "先手击杀 → 摇定技能作废(第 1 轮零敌方行动行——死亡即不动手)" | enemy_rows == 0 | (待填) | | | -| 271 | 848 | "作废不进账(全池冷却 0 · 保底计数 0——执行上报未发生)" | cd_sum == 0 and int(u.basic_streak) == 0 | (待填) | | | +| 264 | 828 | "死亡打断种子可达(seed=%d——D2 首拍摇中特殊技)" ↩ % void_seed | void_seed > 0 | (待填) | | | +| 265 | 834 | "死亡打断战斗启动(hero_atk 999——先手必杀 40HP 熊地精)" | settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 999}) | (待填) | | | +| 266 | 837 | "开轮已摇定本拍动作(待发 = %s)" ↩ % pending | pending != "" | (待填) | | | +| 267 | 843 | "先手击杀 → 摇定技能作废(第 1 轮零敌方行动行——死亡即不动手)" | enemy_rows == 0 | (待填) | | | +| 268 | 847 | "作废不进账(全池冷却 0 · 保底计数 0——执行上报未发生)" | cd_sum == 0 and int(u.basic_streak) == 0 | (待填) | | | -## S45 _smoke_skill_pool_validation_reject(行 859–895 · 13 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) +## S45 _smoke_skill_pool_validation_reject(行 858–894 · 13 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 272 | 859 | "加载校验拒绝:池条目 skill_id 悬空致命" | _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "dangling_skill") | (待填) | | | -| 273 | 861 | "加载校验拒绝:池普攻行数 ≠ 1(空池不可能前提)" | _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition") | (待填) | | | -| 274 | 866 | "加载校验拒绝:池增益技 > 1(每池增益 ≤1 纪律)" | _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition") | (待填) | | | -| 275 | 868 | "加载校验拒绝:普攻配倍率 ≠ 1(域校验锁死)" | not skill_table.validate().is_empty() | (待填) | | | -| 276 | 870 | "加载校验拒绝:普攻配冷却 ≠ 0(域校验锁死)" | not skill_table.validate().is_empty() | (待填) | | | -| 277 | 872 | "加载校验拒绝:damage_mult 越域 [1.1,16]" | not skill_table.validate().is_empty() | (待填) | | | -| 278 | 875 | "加载校验拒绝:池权重 ≤ 0" | not et2.validate().is_empty() | (待填) | | | -| 279 | 877 | "加载校验拒绝:空池(禁)" | not et2.validate().is_empty() | (待填) | | | -| 280 | 883 | "加载校验拒绝:池妨害技 > 1(每池妨害 ≤1 纪律)" | _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table), "pool_composition") | (待填) | | | -| 281 | 885 | "加载校验拒绝:妨害技缺 buff_ref(效果引用)" | not skill_table.validate().is_empty() | (待填) | | | -| 282 | 887 | "加载校验拒绝:rider_mult 越域 (0,1]" | not skill_table.validate().is_empty() | (待填) | | | -| 283 | 889 | "加载校验拒绝:非伤害技带必中附加分量" | not skill_table.validate().is_empty() | (待填) | | | -| 284 | 895 | "加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)" | _has_err_category(_db.validate_skill_pool_links(skill_table, enemy_table, _db.status_effects), "cast_side") | (待填) | | | +| 269 | 858 | "加载校验拒绝:池条目 skill_id 悬空致命" | _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "dangling_skill") | (待填) | | | +| 270 | 860 | "加载校验拒绝:池普攻行数 ≠ 1(空池不可能前提)" | _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "pool_composition") | (待填) | | | +| 271 | 865 | "加载校验拒绝:池增益技 > 1(每池增益 ≤1 纪律)" | _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "pool_composition") | (待填) | | | +| 272 | 867 | "加载校验拒绝:普攻配倍率 ≠ 1(域校验锁死)" | not skill_table.validate().is_empty() | (待填) | | | +| 273 | 869 | "加载校验拒绝:普攻配冷却 ≠ 0(域校验锁死)" | not skill_table.validate().is_empty() | (待填) | | | +| 274 | 871 | "加载校验拒绝:damage_mult 越域 [1.1,16]" | not skill_table.validate().is_empty() | (待填) | | | +| 275 | 874 | "加载校验拒绝:池权重 ≤ 0" | not et2.validate().is_empty() | (待填) | | | +| 276 | 876 | "加载校验拒绝:空池(禁)" | not et2.validate().is_empty() | (待填) | | | +| 277 | 882 | "加载校验拒绝:池妨害技 > 1(每池妨害 ≤1 纪律)" | _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table), "pool_composition") | (待填) | | | +| 278 | 884 | "加载校验拒绝:妨害技缺 buff_ref(效果引用)" | not skill_table.validate().is_empty() | (待填) | | | +| 279 | 886 | "加载校验拒绝:rider_mult 越域 (0,1]" | not skill_table.validate().is_empty() | (待填) | | | +| 280 | 888 | "加载校验拒绝:非伤害技带必中附加分量" | not skill_table.validate().is_empty() | (待填) | | | +| 281 | 894 | "加载校验拒绝:妨害技引用敌方侧状态(施放类 side 守卫)" | _has_err_category(_db._validate_skill_pool_links(skill_table, enemy_table, _db.combat.status_effects), "cast_side") | (待填) | | | -## S46 _smoke_debuff_rider(行 973–975 · 3 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) +## S46 _smoke_debuff_rider(行 972–974 · 3 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 285 | 973 | "毒爆必中附加分量种子可达(seed=%d,毒爆伤害行 ×%d)" ↩ % [venom_seed, venom_rows_seen] | venom_seed > 0 and venom_rows_seen >= 1 | (待填) | | | -| 286 | 974 | "毒爆伤害行逐拍精确复算:终值 = max(1, 掷点档位主分量) + 必中附加(rider 只随 rider_mult 行出现)" | venom_check_ok | (待填) | | | -| 287 | 975 | "蛛网束缚落主角:施放后掷点阈值 = 基准 AC + 失衡幅度(off_guard ac 轴端到端,seed=%d)" ↩ % snare_seed | snare_seed > 0 | (待填) | | | +| 282 | 972 | "毒爆必中附加分量种子可达(seed=%d,毒爆伤害行 ×%d)" ↩ % [venom_seed, venom_rows_seen] | venom_seed > 0 and venom_rows_seen >= 1 | (待填) | | | +| 283 | 973 | "毒爆伤害行逐拍精确复算:终值 = max(1, 掷点档位主分量) + 必中附加(rider 只随 rider_mult 行出现)" | venom_check_ok | (待填) | | | +| 284 | 974 | "蛛网束缚落主角:施放后掷点阈值 = 基准 AC + 失衡幅度(off_guard ac 轴端到端,seed=%d)" ↩ % snare_seed | snare_seed > 0 | (待填) | | | -## S47 _smoke_counter_buckets(行 998–1037 · 7 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) +## S47 _smoke_counter_buckets(行 997–1036 · 7 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 288 | 998 | "V1 第1战真实 request_dig 受理(begin_battle 后 ACTIVE 相位)〔无状态化 V1〕" | dig1 | (待填) | | | -| 289 | 999 | "V1 第1战付费挖格经 ctx 就地写 adventure 桶=1(无 export 搬运时点)〔无状态化 V1〕" | int(led.get("adv_twin_digs", 0)) == 1 | (待填) | | | -| 290 | 1005 | "V1 第2战 begin_battle 经 ctx 读到上一战持久值=1(读路径走宿主字典引用,非副本种子回灌)〔无状态化 V1〕" | EffectEngine.counter(o2.fx_host().counters_ctx, "adv_twin_digs") == 1 | (待填) | | | -| 291 | 1007 | "V1 第2战挖后 adventure 桶跨战累积=2(共享账判空早退缺陷消解的回归钉面——旧副本态恒 1)〔无状态化 V1〕" | int(led.get("adv_twin_digs", 0)) == 2 | (待填) | | | -| 292 | 1024 | "V6 第1战标错落 battle 桶 insurance_used=1(_apply_counter_hit 静态写口经 ctx)〔无状态化 V6〕" | EffectEngine.counter(oA.fx_host().counters_ctx, "insurance_used") == 1 | (待填) | | | -| 293 | 1030 | "卡④ wr2①:完美保险标错行 type=extended 且 effect_id=insurance_used(effect_id 必载)〔明细收敛 V2〕" | not v6_ins.is_empty() and String(v6_ins.get("type", "")) == "extended" and String(v6_ins.get("source_class", "")) == "seal_miss" and bool(v6_ins.get("forgiven", 0)) | (待填) | | | -| 294 | 1037 | "V6 battle 桶随宿主销毁:次战新编排器 fx_counters_battle 天然空,读 insurance_used=0〔无状态化 V6〕" | EffectEngine.counter(oB.fx_host().counters_ctx, "insurance_used") == 0 | (待填) | | | +| 285 | 997 | "V1 第1战真实 request_dig 受理(begin_battle 后 ACTIVE 相位)〔无状态化 V1〕" | dig1 | (待填) | | | +| 286 | 998 | "V1 第1战付费挖格经 ctx 就地写 adventure 桶=1(无 export 搬运时点)〔无状态化 V1〕" | int(led.get("adv_twin_digs", 0)) == 1 | (待填) | | | +| 287 | 1004 | "V1 第2战 begin_battle 经 ctx 读到上一战持久值=1(读路径走宿主字典引用,非副本种子回灌)〔无状态化 V1〕" | EffectEngine.counter(o2.fx_host().counters_ctx, "adv_twin_digs") == 1 | (待填) | | | +| 288 | 1006 | "V1 第2战挖后 adventure 桶跨战累积=2(共享账判空早退缺陷消解的回归钉面——旧副本态恒 1)〔无状态化 V1〕" | int(led.get("adv_twin_digs", 0)) == 2 | (待填) | | | +| 289 | 1023 | "V6 第1战标错落 battle 桶 insurance_used=1(_apply_counter_hit 静态写口经 ctx)〔无状态化 V6〕" | EffectEngine.counter(oA.fx_host().counters_ctx, "insurance_used") == 1 | (待填) | | | +| 290 | 1029 | "卡④ wr2①:完美保险标错行 type=extended 且 effect_id=insurance_used(effect_id 必载)〔明细收敛 V2〕" | not v6_ins.is_empty() and String(v6_ins.get("type", "")) == "extended" and String(v6_ins.get("source_class", "")) == "seal_miss" and bool(v6_ins.get("forgiven", 0)) | (待填) | | | +| 291 | 1036 | "V6 battle 桶随宿主销毁:次战新编排器 fx_counters_battle 天然空,读 insurance_used=0〔无状态化 V6〕" | EffectEngine.counter(oB.fx_host().counters_ctx, "insurance_used") == 0 | (待填) | | | -## S48 _smoke_extended_rows(行 1064–1083 · 2 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) +## S48 _smoke_extended_rows(行 1063–1082 · 2 条 · 源 game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 295 | 1064 | "卡④ wr2②:专长揭示兑付行 type=extended 且 effect_id=fx_reveal 且来源格真实〔明细收敛 V2〕" | not paid.is_empty() and String(paid.get("type", "")) == "extended" and Vector2i(paid["origin_cell"][0], paid["origin_cell"][1]).x >= 0 | (待填) | | | -| 296 | 1081 | "卡④ wr2③:揭示落空行 type=extended 且 effect_id=fx_reveal 且 origin_cell=越界哨值〔明细收敛 V2〕" | not missed.is_empty() and String(missed.get("type", "")) == "extended" and Vector2i(missed["origin_cell"][0], missed["origin_cell"][1]).x == -1 | (待填) | | | +| 292 | 1063 | "卡④ wr2②:专长揭示兑付行 type=extended 且 effect_id=fx_reveal 且来源格真实〔明细收敛 V2〕" | not paid.is_empty() and String(paid.get("type", "")) == "extended" and Vector2i(paid["origin_cell"][0], paid["origin_cell"][1]).x >= 0 | (待填) | | | +| 293 | 1080 | "卡④ wr2③:揭示落空行 type=extended 且 effect_id=fx_reveal 且 origin_cell=越界哨值〔明细收敛 V2〕" | not missed.is_empty() and String(missed.get("type", "")) == "extended" and Vector2i(missed["origin_cell"][0], missed["origin_cell"][1]).x == -1 | (待填) | | | ## S49 _smoke_seal_ap_exhausted(行 54–80 · 5 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 297 | 54 | "回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载封印回归场景)" | settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) | (待填) | | | -| 298 | 68 | "%d 次挖掘后恰剩 1 AP(phase=%s ap=%d)" ↩ % [ap_total - 1, str(settlement.action_economy.phase), settlement.action_economy.ap] | settlement.action_economy.phase == "Active" and settlement.action_economy.ap == 1 | (待填) | | | -| 299 | 73 | "仍存在可挖目标(封印目标来源)" | not plan2.is_empty() | (待填) | | | -| 300 | 79 | "末点 AP 封印被受理" | not out.is_empty() and bool(out[0].accepted) | (待填) | | | -| 301 | 80 | "封印动作耗尽 AP 触发轮末结算并进入第 2 轮(rounds=%s)" ↩ % str(rounds) | rounds.size() >= 2 | (待填) | | | +| 294 | 54 | "回归战斗启动(Boss 行——D0 20HP 级会被盲挖粉尘秒杀,无法承载封印回归场景)" | settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}) | (待填) | | | +| 295 | 68 | "%d 次挖掘后恰剩 1 AP(phase=%s ap=%d)" ↩ % [ap_total - 1, str(settlement.action_economy.phase), settlement.action_economy.ap] | settlement.action_economy.phase == "Active" and settlement.action_economy.ap == 1 | (待填) | | | +| 296 | 73 | "仍存在可挖目标(封印目标来源)" | not plan2.is_empty() | (待填) | | | +| 297 | 79 | "末点 AP 封印被受理" | not out.is_empty() and bool(out[0].accepted) | (待填) | | | +| 298 | 80 | "封印动作耗尽 AP 触发轮末结算并进入第 2 轮(rounds=%s)" ↩ % str(rounds) | rounds.size() >= 2 | (待填) | | | ## S50 _smoke_clear_reward_chain(行 329–341 · 5 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 302 | 329 | "① 完美封印清图达成(%d 轮)" ↩ % int(run.rounds) | bool(run.cleared) | (待填) | | | -| 303 | 336 | "① 清图奖励双份真实兑付(clear_reward 伤害行 ×%d——clear_blast 身份两份各打一发)" ↩ % dmg.size() | dmg.size() == 2 | (待填) | | | -| 304 | 338 | "① 第二份按身份类型走真实兑付(首份『清图奖励』/ 第二份『完美第二份』)" | String(dmg[0].display_name).contains("清图奖励") and String(dmg[1].display_name).contains("完美第二份") | (待填) | | | -| 305 | 339 | "④ 高光行 value = 实际有效份数 2(不记名义值;行数 %d)" ↩ % hl.size() | hl.size() == 1 and int(hl[0].value) == 2 | (待填) | | | -| 306 | 341 | "高光标记 = perfect_sweep" | String(hl[0].get("highlight", "")) == "perfect_sweep" | (待填) | | | +| 299 | 329 | "① 完美封印清图达成(%d 轮)" ↩ % int(run.rounds) | bool(run.cleared) | (待填) | | | +| 300 | 336 | "① 清图奖励双份真实兑付(clear_reward 伤害行 ×%d——clear_blast 身份两份各打一发)" ↩ % dmg.size() | dmg.size() == 2 | (待填) | | | +| 301 | 338 | "① 第二份按身份类型走真实兑付(首份『清图奖励』/ 第二份『完美第二份』)" | String(dmg[0].display_name).contains("清图奖励") and String(dmg[1].display_name).contains("完美第二份") | (待填) | | | +| 302 | 339 | "④ 高光行 value = 实际有效份数 2(不记名义值;行数 %d)" ↩ % hl.size() | hl.size() == 1 and int(hl[0].value) == 2 | (待填) | | | +| 303 | 341 | "高光标记 = perfect_sweep" | String(hl[0].get("highlight", "")) == "perfect_sweep" | (待填) | | | ## S51 _smoke_clear_reward_zero_paths(行 359–399 · 11 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 307 | 359 | "零兑付构造战斗启动" | s.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}) | (待填) | | | -| 308 | 363 | "清图槽身份 = blast 型(clear_default 唯一活行 clear_blast——第二份同身份兑付)" | snap != null and String(snap.cell_type) == "blast" | (待填) | | | -| 309 | 366 | "② 即时类首份零兑付(无存活敌)→ first_ok=false" | not first_ok | (待填) | | | -| 310 | 367 | "② 零兑付产出 reward_ineffective 行(batch_key=clear_blast)" | _has_source_row(rows, "reward_ineffective", "clear_reward", "clear_blast") | (待填) | | | -| 311 | 374 | "高光守卫构造战斗启动" | s_b.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}) | (待填) | | | -| 312 | 381 | "② grant_clear_reward 守卫路径重走首份 → 未生效行仍在案" | _has_source_row(rows_b, "reward_ineffective", "clear_reward", "clear_blast") | (待填) | | | -| 313 | 382 | "② 首份零兑付 → 高光行不发(不记名义值 2)" | _rows_of_type(rows_b, "highlight").is_empty() | (待填) | | | -| 314 | 389 | "第二份被拒构造战斗启动" | s2.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}) | (待填) | | | -| 315 | 396 | "③ 首份 aoe 真实兑付(击杀唯一存活敌)" | _has_source_row(rows2, "damage", "clear_reward") | (待填) | | | -| 316 | 397 | "③ 第二份被拒产出 reward_ineffective 行(零兑付)" | _has_source_row(rows2, "reward_ineffective", "clear_reward", "clear_blast") | (待填) | | | -| 317 | 399 | "④ 高光行 value = 实际有效份数 1(不记名义值 2)" | hl2.size() == 1 and int(hl2[0].value) == 1 | (待填) | | | +| 304 | 359 | "零兑付构造战斗启动" | s.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}) | (待填) | | | +| 305 | 363 | "清图槽身份 = blast 型(clear_default 唯一活行 clear_blast——第二份同身份兑付)" | snap != null and String(snap.cell_type) == "blast" | (待填) | | | +| 306 | 366 | "② 即时类首份零兑付(无存活敌)→ first_ok=false" | not first_ok | (待填) | | | +| 307 | 367 | "② 零兑付产出 reward_ineffective 行(batch_key=clear_blast)" | _has_source_row(rows, "reward_ineffective", "clear_reward", "clear_blast") | (待填) | | | +| 308 | 374 | "高光守卫构造战斗启动" | s_b.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}) | (待填) | | | +| 309 | 381 | "② grant_clear_reward 守卫路径重走首份 → 未生效行仍在案" | _has_source_row(rows_b, "reward_ineffective", "clear_reward", "clear_blast") | (待填) | | | +| 310 | 382 | "② 首份零兑付 → 高光行不发(不记名义值 2)" | _rows_of_type(rows_b, "highlight").is_empty() | (待填) | | | +| 311 | 389 | "第二份被拒构造战斗启动" | s2.begin_battle({"level_index": 1, "node_depth": CLEAR_SMOKE_DEPTH, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"}) | (待填) | | | +| 312 | 396 | "③ 首份 aoe 真实兑付(击杀唯一存活敌)" | _has_source_row(rows2, "damage", "clear_reward") | (待填) | | | +| 313 | 397 | "③ 第二份被拒产出 reward_ineffective 行(零兑付)" | _has_source_row(rows2, "reward_ineffective", "clear_reward", "clear_blast") | (待填) | | | +| 314 | 399 | "④ 高光行 value = 实际有效份数 1(不记名义值 2)" | hl2.size() == 1 and int(hl2[0].value) == 1 | (待填) | | | ## S52 _smoke_refresh_provenance_guard(行 410–421 · 5 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 318 | 410 | "轮内刷新受理(三轮停手攒 RAP 后 Active 相位发起)" | bool(run.refreshed) | (待填) | | | -| 319 | 411 | "刷新图清图达成(%d 轮)" ↩ % int(run.rounds) | bool(run.cleared) | (待填) | | | -| 320 | 419 | "刷新图清图 → 无 clear_reward 行(守卫在位——实测 %d 行)" ↩ % cr | cr == 0 | (待填) | | | -| 321 | 420 | "刷新图清图 → 无高光行(守卫在位——实测 %d 行)" ↩ % hl | hl == 0 | (待填) | | | -| 322 | 421 | "对照基线:常规换图清图有 clear_reward 行(×%d——守卫非全局静默)" ↩ % _regular_clear_reward_rows | _regular_clear_reward_rows > 0 | (待填) | | | +| 315 | 410 | "轮内刷新受理(三轮停手攒 RAP 后 Active 相位发起)" | bool(run.refreshed) | (待填) | | | +| 316 | 411 | "刷新图清图达成(%d 轮)" ↩ % int(run.rounds) | bool(run.cleared) | (待填) | | | +| 317 | 419 | "刷新图清图 → 无 clear_reward 行(守卫在位——实测 %d 行)" ↩ % cr | cr == 0 | (待填) | | | +| 318 | 420 | "刷新图清图 → 无高光行(守卫在位——实测 %d 行)" ↩ % hl | hl == 0 | (待填) | | | +| 319 | 421 | "对照基线:常规换图清图有 clear_reward 行(×%d——守卫非全局静默)" ↩ % _regular_clear_reward_rows | _regular_clear_reward_rows > 0 | (待填) | | | -## S53 _smoke_polarity_routing(行 442–549 · 12 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) +## S53 _smoke_polarity_routing(行 451–555 · 9 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 323 | 442 | "极性路由构造种子可达(seed=%d——并流后行为面重锚)" ↩ % polarity_seed | polarity_seed > 0 | (待填) | | | -| 324 | 443 | "构造触达:标中真雷 + 标空安全格均入队" | bool(probe["seal_hit_done"]) and bool(probe["seal_miss_done"]) | (待填) | | | -| 325 | 444 | "构造触达:挖到危险格 ≥1(%d)" ↩ % int(probe["mine_events"]) | int(probe["mine_events"]) >= 1 | (待填) | | | -| 326 | 445 | "构造触达:挖到安全格 ≥1(%d)" ↩ % int(probe["safe_events"]) | int(probe["safe_events"]) >= 1 | (待填) | | | -| 327 | 462 | "挖安全格 → 只出正向兑付(效果行 ∈ 正向字典 + 每格 ≥1 行——%d 格)" ↩ % probe["safe_cells"].size() | safe_all_pos | (待填) | | | -| 328 | 479 | "挖危险格 → 只出负向兑付(效果行 ∈ 负向字典 + 每格 ≥1 行——%d 格)" ↩ % probe["mine_cells"].size() | mine_all_neg | (待填) | | | -| 329 | 492 | "封印命中 → 该危险格资源的正向兑付行(origin 指回危险格,效果 ∈ 正向字典;action 型按行值符号辨极性)" | hit_ok | (待填) | | | -| 330 | 498 | "封印溢价配置在位(seal_hit_premium=125 provisional——形态 a 兑付溢价)" | prem_pct == 125 | (待填) | | | -| 331 | 526 | "标中兑付溢价(×%d 行逐型=同型正向 ×%d/100——等号压线注记:δ=1.25 时条件二 E[标中]=1.25·E_dig 恰等成立)" ↩ % [prem_rows, prem_pct] | prem_rows >= 1 and prem_ok | (待填) | | | -| 332 | 532 | "标空惩罚零溢价(负向直伤行=固定表值原值——溢价只挂标中正向)" | miss_flat_ok | (待填) | | | -| 333 | 543 | "标空封印 → 封印未中归因行 + 该安全格资源负向兑付(同格同批)" | not miss_rows.is_empty() and miss_neg_ok | (待填) | | | -| 334 | 549 | "封印判定兑付仅封印判定批两封印在案(撤封印抵消过滤零泄漏——%d 格)" ↩ % seal_srcs.size() | int(seal_srcs.size()) <= 2 | (待填) | | | +| 320 | 451 | "极性路由构造种子可达(seed=%d——并流后行为面重锚;缺腿:%s)" ↩ % [polarity_seed, legs] | polarity_seed > 0 | (待填) | | | +| 321 | 468 | "挖安全格 → 只出正向兑付(效果行 ∈ 正向字典 + 每格 ≥1 行——%d 格)" ↩ % probe["safe_cells"].size() | safe_all_pos | (待填) | | | +| 322 | 485 | "挖危险格 → 只出负向兑付(效果行 ∈ 负向字典 + 每格 ≥1 行——%d 格)" ↩ % probe["mine_cells"].size() | mine_all_neg | (待填) | | | +| 323 | 498 | "封印命中 → 该危险格资源的正向兑付行(origin 指回危险格,效果 ∈ 正向字典;action 型按行值符号辨极性)" | hit_ok | (待填) | | | +| 324 | 504 | "封印溢价配置在位(seal_hit_premium=125 provisional——形态 a 兑付溢价)" | prem_pct == 125 | (待填) | | | +| 325 | 532 | "标中兑付溢价(×%d 行逐型=同型正向 ×%d/100——等号压线注记:δ=1.25 时条件二 E[标中]=1.25·E_dig 恰等成立)" ↩ % [prem_rows, prem_pct] | prem_rows >= 1 and prem_ok | (待填) | | | +| 326 | 538 | "标空惩罚零溢价(负向直伤行=固定表值原值——溢价只挂标中正向)" | miss_flat_ok | (待填) | | | +| 327 | 549 | "标空封印 → 封印未中归因行 + 该安全格资源负向兑付(同格同批)" | not miss_rows.is_empty() and miss_neg_ok | (待填) | | | +| 328 | 555 | "封印判定兑付仅封印判定批两封印在案(撤封印抵消过滤零泄漏——%d 格)" ↩ % seal_srcs.size() | int(seal_srcs.size()) <= 2 | (待填) | | | -## S54 _smoke_seal_irreversible_quadrants(行 571–663 · 21 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) +## S54 _smoke_seal_irreversible_quadrants(行 577–669 · 21 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 335 | 571 | "四情况 A:开局受理" | settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) | (待填) | | | -| 336 | 574 | "四情况 A:找到安全格封印目标" | not t_a.is_empty() | (待填) | | | -| 337 | 578 | "四情况 A:封印受理" | ms_a.settle_batch([{"action": "seal_place", "cell": t_a.seal_cell}])[0].accepted | (待填) | | | -| 338 | 580 | "四情况 A:seal_pull 出目录拒绝(撤封印退役)" | not pull_out.is_empty() and not bool(pull_out[0].get("accepted", true)) and String(pull_out[0].get("sub_reason", "")) == "out_of_catalog" | (待填) | | | -| 339 | 582 | "四情况 A:重复封印仍被拒(seal_present)" | not dup_place.is_empty() and not bool(dup_place[0].get("accepted", true)) and String(dup_place[0].get("sub_reason", "")) == "seal_present" | (待填) | | | -| 340 | 584 | "四情况 A:封印格直接挖开被受理(不再须先撤封印)" | not dig_a.is_empty() and bool(dig_a[0].get("accepted", false)) | (待填) | | | -| 341 | 585 | "四情况 A:封印随挖掘隐式退役(has_seal = false)" | not ms_a.has_seal(t_a.seal_cell) | (待填) | | | -| 342 | 600 | "四情况 A:标空判定零兑付(×%d——封印已擦除不入轮末基线)" ↩ % miss_a | miss_a == 0 | (待填) | | | -| 343 | 601 | "四情况 A:安全格正向兑付作废(×%d——零奖励)" ↩ % grant_a | grant_a == 0 | (待填) | | | -| 344 | 602 | "四情况 A:兑付作废归因行恰一次(×%d——零惩罚零奖励留痕)" ↩ % void_a | void_a == 1 | (待填) | | | -| 345 | 609 | "四情况 B:开局受理" | settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) | (待填) | | | -| 346 | 612 | "四情况 B:找到危险格封印目标" | not t_b.is_empty() | (待填) | | | -| 347 | 616 | "四情况 B:封印受理" | ms_b.settle_batch([{"action": "seal_place", "cell": t_b.seal_cell}])[0].accepted | (待填) | | | -| 348 | 618 | "四情况 B:封印危险格直接挖开被受理" | not dig_b.is_empty() and bool(dig_b[0].get("accepted", false)) | (待填) | | | -| 349 | 619 | "四情况 B:危险格已揭示(挖雷本体照常)" | ms_b.is_revealed(t_b.seal_cell) and ms_b.current_board().is_mine(t_b.seal_cell) | (待填) | | | -| 350 | 631 | "四情况 B:挖雷惩罚照常兑付(×%d——只受挖雷一层惩罚)" ↩ % mine_hit_b | mine_hit_b >= 1 | (待填) | | | -| 351 | 632 | "四情况 B:标中不发(×%d——封印已擦除不判)" ↩ % hit_b | hit_b == 0 | (待填) | | | -| 352 | 639 | "四情况 C:开局受理" | settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "node_type": "boss"}) | (待填) | | | -| 353 | 643 | "四情况 C:安全格+危险格封印目标齐备" | not t_safe.is_empty() and not t_mine.is_empty() | (待填) | | | -| 354 | 662 | "四情况 C:标空惩罚照常(归因×%d 负向兑付×%d)" ↩ % [miss_c, miss_neg_c] | miss_c >= 1 and miss_neg_c >= 1 | (待填) | | | -| 355 | 663 | "四情况 C:标中奖励照常(×%d)" ↩ % hit_c | hit_c >= 1 | (待填) | | | +| 329 | 577 | "四情况 A:开局受理" | settle_a.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}) | (待填) | | | +| 330 | 580 | "四情况 A:找到安全格封印目标" | not t_a.is_empty() | (待填) | | | +| 331 | 584 | "四情况 A:封印受理" | ms_a.settle_batch([{"action": "seal_place", "cell": t_a.seal_cell}])[0].accepted | (待填) | | | +| 332 | 586 | "四情况 A:seal_pull 出目录拒绝(撤封印退役)" | not pull_out.is_empty() and not bool(pull_out[0].get("accepted", true)) and String(pull_out[0].get("sub_reason", "")) == "out_of_catalog" | (待填) | | | +| 333 | 588 | "四情况 A:重复封印仍被拒(seal_present)" | not dup_place.is_empty() and not bool(dup_place[0].get("accepted", true)) and String(dup_place[0].get("sub_reason", "")) == "seal_present" | (待填) | | | +| 334 | 590 | "四情况 A:封印格直接挖开被受理(不再须先撤封印)" | not dig_a.is_empty() and bool(dig_a[0].get("accepted", false)) | (待填) | | | +| 335 | 591 | "四情况 A:封印随挖掘隐式退役(has_seal = false)" | not ms_a.has_seal(t_a.seal_cell) | (待填) | | | +| 336 | 606 | "四情况 A:标空判定零兑付(×%d——封印已擦除不入轮末基线)" ↩ % miss_a | miss_a == 0 | (待填) | | | +| 337 | 607 | "四情况 A:安全格正向兑付作废(×%d——零奖励)" ↩ % grant_a | grant_a == 0 | (待填) | | | +| 338 | 608 | "四情况 A:兑付作废归因行恰一次(×%d——零惩罚零奖励留痕)" ↩ % void_a | void_a == 1 | (待填) | | | +| 339 | 615 | "四情况 B:开局受理" | settle_b.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}) | (待填) | | | +| 340 | 618 | "四情况 B:找到危险格封印目标" | not t_b.is_empty() | (待填) | | | +| 341 | 622 | "四情况 B:封印受理" | ms_b.settle_batch([{"action": "seal_place", "cell": t_b.seal_cell}])[0].accepted | (待填) | | | +| 342 | 624 | "四情况 B:封印危险格直接挖开被受理" | not dig_b.is_empty() and bool(dig_b[0].get("accepted", false)) | (待填) | | | +| 343 | 625 | "四情况 B:危险格已揭示(挖雷本体照常)" | ms_b.is_revealed(t_b.seal_cell) and ms_b.current_board().is_mine(t_b.seal_cell) | (待填) | | | +| 344 | 637 | "四情况 B:挖雷惩罚照常兑付(×%d——只受挖雷一层惩罚)" ↩ % mine_hit_b | mine_hit_b >= 1 | (待填) | | | +| 345 | 638 | "四情况 B:标中不发(×%d——封印已擦除不判)" ↩ % hit_b | hit_b == 0 | (待填) | | | +| 346 | 645 | "四情况 C:开局受理" | settle_c.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "node_type": "boss"}) | (待填) | | | +| 347 | 649 | "四情况 C:安全格+危险格封印目标齐备" | not t_safe.is_empty() and not t_mine.is_empty() | (待填) | | | +| 348 | 668 | "四情况 C:标空惩罚照常(归因×%d 负向兑付×%d)" ↩ % [miss_c, miss_neg_c] | miss_c >= 1 and miss_neg_c >= 1 | (待填) | | | +| 349 | 669 | "四情况 C:标中奖励照常(×%d)" ↩ % hit_c | hit_c >= 1 | (待填) | | | -## S55 _smoke_negative_dictionary(行 675–755 · 8 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) +## S55 _smoke_negative_dictionary(行 681–761 · 8 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 356 | 675 | "sapped 行在位(hero · −25 percent · dur 1 · max 16 · stack——weaken 型负向)" | not sapped.is_empty() and String(sapped.get("target_side", "")) == "hero" \\ and int(sapped.get("magnitude", 0)) == -25 and int(sapped.get("duration_settlements", 0)) == 1 \\ and int(sapped.get("max_stack", 0)) == 16 and String(sapped.get("stack_policy", "")) == "stack" | (待填) | | | -| 357 | 679 | "effect_pool_map 收编 sapped → hero_offense(attack_up 镜像池)" | String(_db.settlement.core.effect_pool_map.get("sapped", "")) == "hero_offense" | (待填) | | | -| 358 | 716 | "扫描 6 板内出现五型齐备板(%s)" ↩ % str(by_type.keys()) | by_type.size() == 5 | (待填) | | | -| 359 | 726 | "%s 型负向 → %s 效果行(mine_hit 来源指回该格)" ↩ % [ctype, eff] | _has_source_row(rows, "effect", "mine_hit") and _has_effect_id(rows, eff) | (待填) | | | -| 360 | 738 | "heal 型负向 → 主角直伤 %d(%d → %d,不经增益状态)" ↩ % [backlash_power, hp_before, hp_after] | hp_after == hp_before - backlash_power | (待填) | | | -| 361 | 748 | "blast 型负向 → 警觉(敌方护甲等级 +1 状态在位)" | alert_ok | (待填) | | | -| 362 | 753 | "action 型负向 → 下轮行动点 −1(排队)" | settlement.action_economy.pending_ap_delta() == d0 - 1 | (待填) | | | -| 363 | 755 | "action 型正负抵消 → 排队净值归零" | settlement.action_economy.pending_ap_delta() == d0 | (待填) | | | +| 350 | 681 | "sapped 行在位(hero · −25 percent · dur 1 · max 16 · stack——weaken 型负向)" | not sapped.is_empty() and String(sapped.get("target_side", "")) == "hero" \\ and int(sapped.get("magnitude", 0)) == -25 and int(sapped.get("duration_settlements", 0)) == 1 \\ and int(sapped.get("max_stack", 0)) == 16 and String(sapped.get("stack_policy", "")) == "stack" | (待填) | | | +| 351 | 685 | "effect_pool_map 收编 sapped → hero_offense(attack_up 镜像池)" | String(_db.combat.settlement.core.effect_pool_map.get("sapped", "")) == "hero_offense" | (待填) | | | +| 352 | 722 | "扫描 6 板内出现五型齐备板(%s)" ↩ % str(by_type.keys()) | by_type.size() == 5 | (待填) | | | +| 353 | 732 | "%s 型负向 → %s 效果行(mine_hit 来源指回该格)" ↩ % [ctype, eff] | _has_source_row(rows, "effect", "mine_hit") and _has_effect_id(rows, eff) | (待填) | | | +| 354 | 744 | "heal 型负向 → 主角直伤 %d(%d → %d,不经增益状态)" ↩ % [backlash_power, hp_before, hp_after] | hp_after == hp_before - backlash_power | (待填) | | | +| 355 | 754 | "blast 型负向 → 警觉(敌方护甲等级 +1 状态在位)" | alert_ok | (待填) | | | +| 356 | 759 | "action 型负向 → 下轮行动点 −1(排队)" | settlement.action_economy.pending_ap_delta() == d0 - 1 | (待填) | | | +| 357 | 761 | "action 型正负抵消 → 排队净值归零" | settlement.action_economy.pending_ap_delta() == d0 | (待填) | | | -## S56 _smoke_resource_surface(行 785–804 · 5 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) +## S56 _smoke_resource_surface(行 791–810 · 5 条 · 源 game/tests/unit/battle_settlement/seal_polarity_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 364 | 785 | "chain/dig 揭示事件 resource 恒 true(雷安同池——违例 %d)" ↩ % digchain_bad | digchain_bad == 0 | (待填) | | | -| 365 | 786 | "initial 揭示事件(封锁格)resource 恒 false(×%d 事件)" ↩ % initial_count | initial_bad == 0 and initial_count >= 1 | (待填) | | | -| 366 | 787 | "危险格揭示携带资源(mine ×resource=true %d——危险格不再恒无)" ↩ % mine_with_resource | mine_with_resource >= 1 | (待填) | | | -| 367 | 803 | "封锁格(R₀)身份查询 = null(不入摇号清单)" | settlement.query_reward_identity(r0_first) == null | (待填) | | | -| 368 | 804 | "未开危险格身份恒可查({reward_id, cell_type, tier}——秘宝 桥接前提)" | mine_unrevealed != null and settlement.query_reward_identity(mine_unrevealed) != null | (待填) | | | +| 358 | 791 | "chain/dig 揭示事件 resource 恒 true(雷安同池——违例 %d)" ↩ % digchain_bad | digchain_bad == 0 | (待填) | | | +| 359 | 792 | "initial 揭示事件(封锁格)resource 恒 false(×%d 事件)" ↩ % initial_count | initial_bad == 0 and initial_count >= 1 | (待填) | | | +| 360 | 793 | "危险格揭示携带资源(mine ×resource=true %d——危险格不再恒无)" ↩ % mine_with_resource | mine_with_resource >= 1 | (待填) | | | +| 361 | 809 | "封锁格(R₀)身份查询 = null(不入摇号清单)" | settlement.query_reward_identity(r0_first) == null | (待填) | | | +| 362 | 810 | "未开危险格身份恒可查({reward_id, cell_type, tier}——秘宝 桥接前提)" | mine_unrevealed != null and settlement.query_reward_identity(mine_unrevealed) != null | (待填) | | | ## S57 _init(行 18–18 · 1 条 · 源 game/tests/unit/battle_settlement/skill_unlock_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 369 | 18 | "前置:表加载零 error" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | +| 363 | 18 | "前置:表加载零 error" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | ## S58 _boss(行 30–30 · 1 条 · 源 game/tests/unit/battle_settlement/skill_unlock_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 370 | 30 | "前置:spawn (%d,%d) 受理" ↩ % [L, D] | ok | (待填) | | | +| 364 | 30 | "前置:spawn (%d,%d) 受理" ↩ % [L, D] | ok | (待填) | | | ## S59 _test_gate_predicate(行 49–69 · 9 条 · 源 game/tests/unit/battle_settlement/skill_unlock_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 371 | 49 | "前置:卓尔 max_hp=130(表值——难度乘数空表恒 1.0)" | mhp == 130 | (待填) | | | -| 372 | 52 | "门谓词:hp=66 > 阈值 65 → 零解锁" | ec.check_trigger_unlocks(u).is_empty() | (待填) | | | -| 373 | 53 | "双面过滤:未解锁 venom_curb 不入可摇集合" | not _has_sid(ec.rollable_skills(u), "venom_curb") | (待填) | | | -| 374 | 54 | "双面过滤:未解锁 venom_curb 不入保底特殊技集合" | not _has_sid(ec.special_skills(u), "venom_curb") | (待填) | | | -| 375 | 57 | "前置:补一刀至阈值 65 受理" | hit65 and int(u.hp) == 65 | (待填) | | | -| 376 | 59 | "门谓词:hp=65 ≤ 阈值 65 → 恰解锁 venom_curb 一次" | unlocked.size() == 1 and String(unlocked[0]) == "venom_curb" | (待填) | | | -| 377 | 61 | "解锁即可摇:venom_curb 入可摇集合(冷却初值 0)" | _has_sid(ec.rollable_skills(u), "venom_curb") | (待填) | | | -| 378 | 62 | "解锁即可摇:venom_curb 入保底特殊技集合" | _has_sid(ec.special_skills(u), "venom_curb") | (待填) | | | -| 379 | 69 | "保底不逼锁定:锁定期保底集合 = heavy+venom_burst+web_snare(venom_curb 过滤不出)" | specials.size() == 3 and _has_sid(specials, "heavy") and _has_sid(specials, "web_snare") and _has_sid(specials, "venom_burst") and not _has_sid(specials, "venom_curb") | (待填) | | | +| 365 | 49 | "前置:卓尔 max_hp=130(表值——难度乘数空表恒 1.0)" | mhp == 130 | (待填) | | | +| 366 | 52 | "门谓词:hp=66 > 阈值 65 → 零解锁" | ec.check_trigger_unlocks(u).is_empty() | (待填) | | | +| 367 | 53 | "双面过滤:未解锁 venom_curb 不入可摇集合" | not _has_sid(ec.rollable_skills(u), "venom_curb") | (待填) | | | +| 368 | 54 | "双面过滤:未解锁 venom_curb 不入保底特殊技集合" | not _has_sid(ec.special_skills(u), "venom_curb") | (待填) | | | +| 369 | 57 | "前置:补一刀至阈值 65 受理" | hit65 and int(u.hp) == 65 | (待填) | | | +| 370 | 59 | "门谓词:hp=65 ≤ 阈值 65 → 恰解锁 venom_curb 一次" | unlocked.size() == 1 and String(unlocked[0]) == "venom_curb" | (待填) | | | +| 371 | 61 | "解锁即可摇:venom_curb 入可摇集合(冷却初值 0)" | _has_sid(ec.rollable_skills(u), "venom_curb") | (待填) | | | +| 372 | 62 | "解锁即可摇:venom_curb 入保底特殊技集合" | _has_sid(ec.special_skills(u), "venom_curb") | (待填) | | | +| 373 | 69 | "保底不逼锁定:锁定期保底集合 = heavy+venom_burst+web_snare(venom_curb 过滤不出)" | specials.size() == 3 and _has_sid(specials, "heavy") and _has_sid(specials, "web_snare") and _has_sid(specials, "venom_burst") and not _has_sid(specials, "venom_curb") | (待填) | | | ## S60 _test_latch_irreversible(行 77–80 · 3 条 · 源 game/tests/unit/battle_settlement/skill_unlock_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 380 | 77 | "前置:半血触发解锁" | ec.check_trigger_unlocks(u).size() == 1 | (待填) | | | -| 381 | 79 | "闩不可逆:满血复查零新解锁(单向闩不解锁回退)" | ec.check_trigger_unlocks(u).is_empty() | (待填) | | | -| 382 | 80 | "闩不可逆:可摇集合仍含 venom_curb" | _has_sid(ec.rollable_skills(u), "venom_curb") | (待填) | | | +| 374 | 77 | "前置:半血触发解锁" | ec.check_trigger_unlocks(u).size() == 1 | (待填) | | | +| 375 | 79 | "闩不可逆:满血复查零新解锁(单向闩不解锁回退)" | ec.check_trigger_unlocks(u).is_empty() | (待填) | | | +| 376 | 80 | "闩不可逆:可摇集合仍含 venom_curb" | _has_sid(ec.rollable_skills(u), "venom_curb") | (待填) | | | ## S61 _test_plain_rows_zero_drift(行 86–105 · 6 条 · 源 game/tests/unit/battle_settlement/skill_unlock_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 383 | 86 | "零扰动前置:巨蜘蛛首遇带 spawn(池三行全无 trigger——快照键策略钉死载体)" | ec.spawn(2, 1, int(db.hero.max_hp), [], -1, -1, [], "", "", "battle") | (待填) | | | -| 384 | 92 | "零扰动:无字段行池快照零 unlock 键(16/19 行逐位同前)" | not any_unlock_key | (待填) | | | -| 385 | 93 | "零扰动:满血复查零解锁" | ec.check_trigger_unlocks(u).is_empty() | (待填) | | | -| 386 | 96 | "零扰动:可摇集合 = 全池(普攻冷却恒 0,过滤恒空转)" | rollable.size() == pool_count | (待填) | | | -| 387 | 99 | "确定性前置:第二实例蜘蛛 spawn" | ec2.spawn(2, 1, int(db.hero.max_hp), [], -1, -1, [], "", "", "battle") | (待填) | | | -| 388 | 105 | "确定性:两实例同输入可摇集合逐位同(容器零随机纪律)" | a == b | (待填) | | | +| 377 | 86 | "零扰动前置:巨蜘蛛首遇带 spawn(池三行全无 trigger——快照键策略钉死载体)" | ec.spawn({"level_index": 2, "node_depth": 1, "hero_hp": int(db.combat.hero.max_hp)}) | (待填) | | | +| 378 | 92 | "零扰动:无字段行池快照零 unlock 键(16/19 行逐位同前)" | not any_unlock_key | (待填) | | | +| 379 | 93 | "零扰动:满血复查零解锁" | ec.check_trigger_unlocks(u).is_empty() | (待填) | | | +| 380 | 96 | "零扰动:可摇集合 = 全池(普攻冷却恒 0,过滤恒空转)" | rollable.size() == pool_count | (待填) | | | +| 381 | 99 | "确定性前置:第二实例蜘蛛 spawn" | ec2.spawn({"level_index": 2, "node_depth": 1, "hero_hp": int(db.combat.hero.max_hp)}) | (待填) | | | +| 382 | 105 | "确定性:两实例同输入可摇集合逐位同(容器零随机纪律)" | a == b | (待填) | | | ## S62 _smoke_variant_placement(行 60–132 · 9 条 · 源 game/tests/unit/battle_settlement/variant_placement_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 389 | 60 | "L%d 布置段战斗启动失败" ↩ % L | false | (待填) | | | -| 390 | 63 | "L%d 布置段板非空" ↩ % L | b != null | (待填) | | | -| 391 | 84 | "L%d 变异 id 全在候选池(门控一致性:%s)" ↩ % [L, str(pool_ids)] | not bad_id | (待填) | | | -| 392 | 85 | "L%d 每图至多一种变异类型(单格单 id + 全图同 id)" ↩ % L | single_type | (待填) | | | -| 393 | 93 | "L%d 变异格全在初始展开区外且不吃危险格(布置域结构保障——closed_only)" ↩ % L | domain_ok | (待填) | | | -| 394 | 100 | "L%d 变异格数 %d ≤ 公式上限 %d(可挖池百分比 + 关卡递增)" ↩ % [L, vcells.size(), expect_cap] | vcells.size() <= expect_cap | (待填) | | | -| 395 | 103 | "L%d 公式量>0 时非零布置(极小池 round 至 0 = 合法零布置——2026-09-21 基数修正批)" ↩ % L | expect_cap == 0 or vcells.size() > 0 | (待填) | | | -| 396 | 125 | "L%d 全部连通片 ≤ %d 格(全连通生长、2×2/1×4/1×3+1 等形)" ↩ % [L, pack_max] | pack_ok | (待填) | | | -| 397 | 132 | "L%d 同种子变异布置逐格相同(variant_place 流确定性)" ↩ % L | b2 != null and _variant_dump(b2) == _variant_dump(b) | (待填) | | | +| 383 | 60 | "L%d 布置段战斗启动失败" ↩ % L | false | (待填) | | | +| 384 | 63 | "L%d 布置段板非空" ↩ % L | b != null | (待填) | | | +| 385 | 84 | "L%d 变异 id 全在候选池(门控一致性:%s)" ↩ % [L, str(pool_ids)] | not bad_id | (待填) | | | +| 386 | 85 | "L%d 每图至多一种变异类型(单格单 id + 全图同 id)" ↩ % L | single_type | (待填) | | | +| 387 | 93 | "L%d 变异格全在初始展开区外且不吃危险格(布置域结构保障——closed_only)" ↩ % L | domain_ok | (待填) | | | +| 388 | 100 | "L%d 变异格数 %d ≤ 公式上限 %d(可挖池百分比 + 关卡递增)" ↩ % [L, vcells.size(), expect_cap] | vcells.size() <= expect_cap | (待填) | | | +| 389 | 103 | "L%d 公式量>0 时非零布置(极小池 round 至 0 = 合法零布置——2026-09-21 基数修正批)" ↩ % L | expect_cap == 0 or vcells.size() > 0 | (待填) | | | +| 390 | 125 | "L%d 全部连通片 ≤ %d 格(全连通生长、2×2/1×4/1×3+1 等形)" ↩ % [L, pack_max] | pack_ok | (待填) | | | +| 391 | 132 | "L%d 同种子变异布置逐格相同(variant_place 流确定性)" ↩ % L | b2 != null and _variant_dump(b2) == _variant_dump(b) | (待填) | | | ## S63 _smoke_global_variant(行 154–276 · 33 条 · 源 game/tests/unit/battle_settlement/variant_placement_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 398 | 154 | "互锁负样本③:resource_face + scope=region 被拒(resource_face ⇒ global)" | _errs_has_cross_field(vt.validate()) | (待填) | | | -| 399 | 158 | "互锁负样本④:scope=global + effect=hide_number 被拒(global ⇒ resource_face)" | _errs_has_cross_field(vt.validate()) | (待填) | | | -| 400 | 162 | "互锁负样本⑤:scope=global + trigger=on_open 被拒(global ⇒ always 整图恒在)" | _errs_has_cross_field(vt.validate()) | (待填) | | | -| 401 | 166 | "互锁负样本⑥:scope=global + placement=closed_only 被拒(global ⇒ any 无格子布置域)" | _errs_has_cross_field(vt.validate()) | (待填) | | | -| 402 | 175 | "互锁负样本⑦:resource_face effect_value=51 越域 [0,50] 被拒(不适用 flat_damage [1,9] 加法核)" | domain_hit | (待填) | | | -| 403 | 180 | "门控三维:L2 underdark boss 池 = region 两行(贫矿脉 2026-10-16 屏蔽删行后无 global 行)" | pool_boss_ids == ["magma", "ash"] | (待填) | | | -| 404 | 184 | "门控三维:battle 节点静默跳过 global 行(普通图零报错零警告)" | pool_battle_ids == ["magma", "ash"] | (待填) | | | -| 405 | 185 | "门控三维:缺省双参调用 = global 行跳过(既有调用口径零扰动)" | _db.variant_pool(2, "underdark").is_empty() == false and _db.variant_pool(2, "underdark").size() == 2 | (待填) | | | -| 406 | 186 | "L1 Boss 免疫:unlock_level=2 门控在 boss 节点同样生效(教学保护带 B)" | _db.variant_pool(1, "underdark", "boss").is_empty() | (待填) | | | -| 407 | 187 | "全局决议:battle 节点恒无全局型" | _db.global_variant_row(2, "underdark", "battle").is_empty() | (待填) | | | -| 408 | 188 | "全局决议:L2 boss 全局池空 → 空行(贫矿脉屏蔽后兜底回落承载)" | _db.global_variant_row(2, "underdark", "boss").is_empty() | (待填) | | | -| 409 | 189 | "全局决议:L1 boss 全局池空 → 空行(兜底回落承载)" | _db.global_variant_row(1, "underdark", "boss").is_empty() and _db.global_variant_row(1, "dungeon", "boss").is_empty() | (待填) | | | -| 410 | 190 | "elite 前向词位:boss 同池恒空(贫矿脉屏蔽后——词表位不造行)" | _db.global_variant_row(2, "underdark", "elite").is_empty() | (待填) | | | -| 411 | 196 | "L2 Boss 战斗启动(node_depth=9 Boss 终点——兜底回落断言载体)" | boss_ok | (待填) | | | -| 412 | 202 | "屏蔽后回落:Boss 图无整图全局型(board_variants 空——全局池空合法态)" | bb.board_variant_ids().is_empty() | (待填) | | | -| 413 | 207 | "屏蔽后回落:Boss 图 region 变异恰一行且在候选池内(实测 %s)" ↩ % str(boss_vids.keys()) | boss_vids.size() == 1 and (boss_vids.has("magma") or boss_vids.has("ash")) | (待填) | | | -| 414 | 208 | "屏蔽后回落:Boss 图恢复 region 变异格布置(原替换制跳过 region 的逻辑随全局池空失效)" | not bb.variants.is_empty() | (待填) | | | -| 415 | 209 | "屏蔽后回落:variant_place 恢复取号(原全局取用零取号态消失)" | crng.variant_calls > 0 | (待填) | | | -| 416 | 211 | "快照投影:board_variants 空态一致(横幅/角标唯一读取面——无全局型即零预告)" | _arr_str_eq(snap.get("board_variants", []), bb.board_variant_ids()) | (待填) | | | -| 417 | 221 | "确定性:同种子 Boss 图两遍 board_variants/变异格/取号逐位相同(region 布置路径)" | _arr_str_eq(bb2.board_variant_ids(), boss1_ids) and bb2.variants == boss1_variants and crng2.variant_calls == boss1_calls | (待填) | | | -| 418 | 226 | "偏斜基线:正向池现行合计 77(挖掘风险批口径)" | base_sum == 77 | (待填) | | | -| 419 | 229 | "off 态零扰动:board_skew=0 行集与基线逐位相同(不进投影域)" | off_rows.size() == base_rows.size() and _rows_weights_equal(off_rows, base_rows) | (待填) | | | -| 420 | 232 | "合计守恒:偏斜后正向池合计 = 77(整数最大余数重归一)" | eco._pool_weight(on_rows) == 77 | (待填) | | | -| 421 | 234 | "白名单豁免:weaken/gp/action 三行偏斜后逐位不动" | wmap.get("weaken", -1) == 8 and wmap.get("ap_up", -1) == 6 and wmap.get("gp_coin", -1) == 2 | (待填) | | | -| 422 | 235 | "生存族下调:heal 4→3 / defense 11→7(v=30 构造例)" | wmap.get("heal_minor", -1) == 3 and wmap.get("ac_up", -1) == 7 | (待填) | | | -| 423 | 236 | "进攻族上浮:attack 14→15 / blast 16→18 / pierce 8→9 / magic 8→9(v=30 构造例,等比上浮守恒)" | wmap.get("attack_up", -1) == 15 and wmap.get("spark_aoe", -1) == 18 and wmap.get("pierce_strike", -1) == 9 and wmap.get("magic_bolt", -1) == 9 | (待填) | | | -| 424 | 240 | "合成序:图级偏斜先(乘法面)件级 luck_bias 后(加法 +2×四稀有类)——合计 77+8" | eco._pool_weight(combo_rows) == 77 + 4 * 2 | (待填) | | | -| 425 | 241 | "合成序构造例:skew(heal 3)+2=5 / gp 2+2=4 / pierce 9+2=11 / magic 9+2=11(图级定型后玩家构筑叠加)" | cw.get("heal_minor", -1) == 5 and cw.get("gp_coin", -1) == 4 and cw.get("pierce_strike", -1) == 11 and cw.get("magic_bolt", -1) == 11 | (待填) | | | -| 426 | 244 | "域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)" | eco._pool_weight(eco._biased_cell_rows(0)) == 77 and int(v50.get("heal_minor", -1)) > 0 | (待填) | | | -| 427 | 256 | "wr10①判定面:空订阅=%d / 装载后命中 %d 行 → luck_bias_for(4)=%d(期望 0/1/2;命中 0 即判据空转)" ↩ % [empty_bias, hit_rows, loaded_bias] | empty_bias == 0 and hit_rows == 1 and loaded_bias == 2 | (待填) | | | -| 428 | 258 | "wr10②接线面:组装点注入的查询口有效且与宿主查询口同源" | woc._reward_economy.luck_bias_query.is_valid() and woc._reward_economy.luck_bias_query == wfx.luck_bias_for | (待填) | | | -| 429 | 264 | "wr10③权重生效面:注入回调取回 %d → 偏运池合计 77+4*2(实测 %d)" ↩ % [wbias, woc._reward_economy._pool_weight(woc._reward_economy._biased_cell_rows(wbias))] | wbias == 2 and woc._reward_economy._pool_weight(woc._reward_economy._biased_cell_rows(wbias)) == 77 + 4 * 2 | (待填) | | | -| 430 | 276 | "确定性:同种子普通图布置/摇号两遍逐位相同(off 全链路)" | n1 == _board_resource_dump(normal2) and not n1.is_empty() | (待填) | | | +| 392 | 154 | "互锁负样本③:resource_face + scope=region 被拒(resource_face ⇒ global)" | _errs_has_cross_field(vt.validate()) | (待填) | | | +| 393 | 158 | "互锁负样本④:scope=global + effect=hide_number 被拒(global ⇒ resource_face)" | _errs_has_cross_field(vt.validate()) | (待填) | | | +| 394 | 162 | "互锁负样本⑤:scope=global + trigger=on_open 被拒(global ⇒ always 整图恒在)" | _errs_has_cross_field(vt.validate()) | (待填) | | | +| 395 | 166 | "互锁负样本⑥:scope=global + placement=closed_only 被拒(global ⇒ any 无格子布置域)" | _errs_has_cross_field(vt.validate()) | (待填) | | | +| 396 | 175 | "互锁负样本⑦:resource_face effect_value=51 越域 [0,50] 被拒(不适用 flat_damage [1,9] 加法核)" | domain_hit | (待填) | | | +| 397 | 180 | "门控三维:L2 underdark boss 池 = region 两行(贫矿脉 2026-10-16 屏蔽删行后无 global 行)" | pool_boss_ids == ["magma", "ash"] | (待填) | | | +| 398 | 184 | "门控三维:battle 节点静默跳过 global 行(普通图零报错零警告)" | pool_battle_ids == ["magma", "ash"] | (待填) | | | +| 399 | 185 | "门控三维:缺省双参调用 = global 行跳过(既有调用口径零扰动)" | _db.combat.variation.variant_pool(2, "underdark").is_empty() == false and _db.combat.variation.variant_pool(2, "underdark").size() == 2 | (待填) | | | +| 400 | 186 | "L1 Boss 免疫:unlock_level=2 门控在 boss 节点同样生效(教学保护带 B)" | _db.combat.variation.variant_pool(1, "underdark", "boss").is_empty() | (待填) | | | +| 401 | 187 | "全局决议:battle 节点恒无全局型" | _db.combat.variation.global_variant_row(2, "underdark", "battle").is_empty() | (待填) | | | +| 402 | 188 | "全局决议:L2 boss 全局池空 → 空行(贫矿脉屏蔽后兜底回落承载)" | _db.combat.variation.global_variant_row(2, "underdark", "boss").is_empty() | (待填) | | | +| 403 | 189 | "全局决议:L1 boss 全局池空 → 空行(兜底回落承载)" | _db.combat.variation.global_variant_row(1, "underdark", "boss").is_empty() and _db.combat.variation.global_variant_row(1, "dungeon", "boss").is_empty() | (待填) | | | +| 404 | 190 | "elite 前向词位:boss 同池恒空(贫矿脉屏蔽后——词表位不造行)" | _db.combat.variation.global_variant_row(2, "underdark", "elite").is_empty() | (待填) | | | +| 405 | 196 | "L2 Boss 战斗启动(node_depth=9 Boss 终点——兜底回落断言载体)" | boss_ok | (待填) | | | +| 406 | 202 | "屏蔽后回落:Boss 图无整图全局型(board_variants 空——全局池空合法态)" | bb.board_variant_ids().is_empty() | (待填) | | | +| 407 | 207 | "屏蔽后回落:Boss 图 region 变异恰一行且在候选池内(实测 %s)" ↩ % str(boss_vids.keys()) | boss_vids.size() == 1 and (boss_vids.has("magma") or boss_vids.has("ash")) | (待填) | | | +| 408 | 208 | "屏蔽后回落:Boss 图恢复 region 变异格布置(原替换制跳过 region 的逻辑随全局池空失效)" | not bb.variants.is_empty() | (待填) | | | +| 409 | 209 | "屏蔽后回落:variant_place 恢复取号(原全局取用零取号态消失)" | crng.variant_calls > 0 | (待填) | | | +| 410 | 211 | "快照投影:board_variants 空态一致(横幅/角标唯一读取面——无全局型即零预告)" | _arr_str_eq(snap.get("board_variants", []), bb.board_variant_ids()) | (待填) | | | +| 411 | 221 | "确定性:同种子 Boss 图两遍 board_variants/变异格/取号逐位相同(region 布置路径)" | _arr_str_eq(bb2.board_variant_ids(), boss1_ids) and bb2.variants == boss1_variants and crng2.variant_calls == boss1_calls | (待填) | | | +| 412 | 226 | "偏斜基线:正向池现行合计 77(挖掘风险批口径)" | base_sum == 77 | (待填) | | | +| 413 | 229 | "off 态零扰动:board_skew=0 行集与基线逐位相同(不进投影域)" | off_rows.size() == base_rows.size() and _rows_weights_equal(off_rows, base_rows) | (待填) | | | +| 414 | 232 | "合计守恒:偏斜后正向池合计 = 77(整数最大余数重归一)" | eco._pool_weight(on_rows) == 77 | (待填) | | | +| 415 | 234 | "白名单豁免:weaken/gp/action 三行偏斜后逐位不动" | wmap.get("weaken", -1) == 8 and wmap.get("ap_up", -1) == 6 and wmap.get("gp_coin", -1) == 2 | (待填) | | | +| 416 | 235 | "生存族下调:heal 4→3 / defense 11→7(v=30 构造例)" | wmap.get("heal_minor", -1) == 3 and wmap.get("ac_up", -1) == 7 | (待填) | | | +| 417 | 236 | "进攻族上浮:attack 14→15 / blast 16→18 / pierce 8→9 / magic 8→9(v=30 构造例,等比上浮守恒)" | wmap.get("attack_up", -1) == 15 and wmap.get("spark_aoe", -1) == 18 and wmap.get("pierce_strike", -1) == 9 and wmap.get("magic_bolt", -1) == 9 | (待填) | | | +| 418 | 240 | "合成序:图级偏斜先(乘法面)件级 luck_bias 后(加法 +2×四稀有类)——合计 77+8" | eco._pool_weight(combo_rows) == 77 + 4 * 2 | (待填) | | | +| 419 | 241 | "合成序构造例:skew(heal 3)+2=5 / gp 2+2=4 / pierce 9+2=11 / magic 9+2=11(图级定型后玩家构筑叠加)" | cw.get("heal_minor", -1) == 5 and cw.get("gp_coin", -1) == 4 and cw.get("pierce_strike", -1) == 11 and cw.get("magic_bolt", -1) == 11 | (待填) | | | +| 420 | 244 | "域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)" | eco._pool_weight(eco._biased_cell_rows(0)) == 77 and int(v50.get("heal_minor", -1)) > 0 | (待填) | | | +| 421 | 256 | "wr10①判定面:空订阅=%d / 装载后命中 %d 行 → luck_bias_for(4)=%d(期望 0/1/2;命中 0 即判据空转)" ↩ % [empty_bias, hit_rows, loaded_bias] | empty_bias == 0 and hit_rows == 1 and loaded_bias == 2 | (待填) | | | +| 422 | 258 | "wr10②接线面:组装点注入的查询口有效且与宿主查询口同源" | woc._reward_economy.luck_bias_query.is_valid() and woc._reward_economy.luck_bias_query == wfx.luck_bias_for | (待填) | | | +| 423 | 264 | "wr10③权重生效面:注入回调取回 %d → 偏运池合计 77+4*2(实测 %d)" ↩ % [wbias, woc._reward_economy._pool_weight(woc._reward_economy._biased_cell_rows(wbias))] | wbias == 2 and woc._reward_economy._pool_weight(woc._reward_economy._biased_cell_rows(wbias)) == 77 + 4 * 2 | (待填) | | | +| 424 | 276 | "确定性:同种子普通图布置/摇号两遍逐位相同(off 全链路)" | n1 == _board_resource_dump(normal2) and not n1.is_empty() | (待填) | | | ## S64 _smoke_type_gate(行 25–29 · 2 条 · 源 game/tests/unit/contracts/source_guard_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 431 | 25 | "V7 类型闸 warning 配置在位(untyped_declaration=1 显形)〔迭代27 T5〕" | pj.contains("gdscript/warnings/untyped_declaration=1") | (待填) | | | -| 432 | 28 | "V7 类型审计器闸机在位(static_scan S9 → tools/audit_type_hints.py)〔迭代27 T5〕" | scan_src.contains("audit_type_hints.py") | (待填) | | | +| 425 | 25 | "V7 类型闸 warning 配置在位(untyped_declaration=1 显形)〔迭代27 T5〕" | pj.contains("gdscript/warnings/untyped_declaration=1") | (待填) | | | +| 426 | 28 | "V7 类型审计器闸机在位(static_scan S9 → tools/audit_type_hints.py)〔迭代27 T5〕" | scan_src.contains("audit_type_hints.py") | (待填) | | | ## S65 _smoke_hygiene_gates(行 56–77 · 6 条 · 源 game/tests/unit/contracts/source_guard_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 433 | 56 | "V8 字符串 Callable 产品码零命中(实测应 0——方法引用换装闸)〔迭代27 T6〕" | callables == 0 | (待填) | | | -| 434 | 58 | "V8 FeatsEngine 整文件退役(class_name 注销=文件删除)〔迭代27 T6〕" | not FileAccess.file_exists("res://core/feats_engine.gd") | (待填) | | | -| 435 | 61 | "V8 死代码抽查·_attune_row 零残留〔迭代27 T6〕" | not bo.contains("func _attune_row") | (待填) | | | -| 436 | 64 | "V8 死代码抽查·attunement_counters 零残留〔迭代27 T6〕" | not sm.contains("func attunement_counters") | (待填) | | | -| 437 | 67 | "V8 GP 影子账残留零命中(main 源码面——文件已删+委托缝已拆)〔迭代27 T3〕" | not mv.contains("gp_shadow_ledger") and not mv.contains("_gp_ledger") | (待填) | | | -| 438 | 76 | "V1d 层序落位单口(_apply_layer_contract 之外 move_child 调用点=%d 应=0)〔装配生命批 V1〕" ↩ % outside_calls | outside_calls == 0 | (待填) | | | +| 427 | 56 | "V8 字符串 Callable 产品码零命中(实测应 0——方法引用换装闸)〔迭代27 T6〕" | callables == 0 | (待填) | | | +| 428 | 58 | "V8 FeatsEngine 整文件退役(class_name 注销=文件删除)〔迭代27 T6〕" | not FileAccess.file_exists("res://core/feats_engine.gd") | (待填) | | | +| 429 | 61 | "V8 死代码抽查·_attune_row 零残留〔迭代27 T6〕" | not bo.contains("func _attune_row") | (待填) | | | +| 430 | 64 | "V8 死代码抽查·attunement_counters 零残留〔迭代27 T6〕" | not sm.contains("func attunement_counters") | (待填) | | | +| 431 | 67 | "V8 GP 影子账残留零命中(main 源码面——文件已删+委托缝已拆)〔迭代27 T3〕" | not mv.contains("gp_shadow_ledger") and not mv.contains("_gp_ledger") | (待填) | | | +| 432 | 76 | "V1d 层序落位单口(_apply_layer_contract 之外 move_child 调用点=%d 应=0)〔装配生命批 V1〕" ↩ % outside_calls | outside_calls == 0 | (待填) | | | ## S66 _init(行 22–22 · 1 条 · 源 game/tests/unit/contracts/system_contracts_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 439 | 22 | "前置:表加载零 error" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | +| 433 | 22 | "前置:表加载零 error" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | ## S67 _test_damage_controller_vocabularies(行 43–57 · 4 条 · 源 game/tests/unit/contracts/system_contracts_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 440 | 43 | "五配方常量非空(开单面唯一分派键)" | nonempty | (待填) | | | -| 441 | 47 | "五配方常量互异(词表封闭无别名)" | unique_kinds.size() == kinds.size() | (待填) | | | -| 442 | 55 | "三目标规则常量互异(结账时刻现场解析词表)" | unique_targets.size() == targets.size() | (待填) | | | -| 443 | 56 | "双结账政策常量互异(A 即结 / B 攒批回切位)" | String(DamageControllerScript.POLICY_IMMEDIATE) != String(DamageControllerScript.POLICY_SEGMENT) | (待填) | | | +| 434 | 43 | "五配方常量非空(开单面唯一分派键)" | nonempty | (待填) | | | +| 435 | 47 | "五配方常量互异(词表封闭无别名)" | unique_kinds.size() == kinds.size() | (待填) | | | +| 436 | 55 | "三目标规则常量互异(结账时刻现场解析词表)" | unique_targets.size() == targets.size() | (待填) | | | +| 437 | 56 | "双结账政策常量互异(A 即结 / B 攒批回切位)" | String(DamageControllerScript.POLICY_IMMEDIATE) != String(DamageControllerScript.POLICY_SEGMENT) | (待填) | | | ## S68 _test_rng_tag_closure(行 64–67 · 2 条 · 源 game/tests/unit/contracts/system_contracts_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 444 | 64 | "settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)" | RngServiceScript.PURPOSE_TAGS.has(stream_label) | (待填) | | | -| 445 | 66 | "RngService 用途标签词表非空且互异(封闭语义由流改标签)" | RngServiceScript.PURPOSE_TAGS.size() > 0 | (待填) | | | +| 438 | 64 | "settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)" | RngServiceScript.PURPOSE_TAGS.has(stream_label) | (待填) | | | +| 439 | 66 | "RngService 用途标签词表非空且互异(封闭语义由流改标签)" | RngServiceScript.PURPOSE_TAGS.size() > 0 | (待填) | | | ## S69 _test_ticket_nine_columns(行 83–85 · 3 条 · 源 game/tests/unit/contracts/system_contracts_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 446 | 83 | "单据九栏 + 可选两栏缺省齐备(开单面只写差异栏前提)" | all_present | (待填) | | | -| 447 | 84 | "缺省无类型轴(attack_type 空 = 恒不查克制矩阵)" | String(t.attack_type) == "" | (待填) | | | -| 448 | 85 | "缺省无前置依赖(requires = -1)" | int(t.requires) == -1 | (待填) | | | +| 440 | 83 | "单据九栏 + 可选两栏缺省齐备(开单面只写差异栏前提)" | all_present | (待填) | | | +| 441 | 84 | "缺省无类型轴(attack_type 空 = 恒不查克制矩阵)" | String(t.attack_type) == "" | (待填) | | | +| 442 | 85 | "缺省无前置依赖(requires = -1)" | int(t.requires) == -1 | (待填) | | | ## S70 _test_status_boolean_acceptance(行 107–121 · 5 条 · 源 game/tests/unit/contracts/system_contracts_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 449 | 107 | "前置:状态表非空" | not any_row.is_empty() | (待填) | | | -| 450 | 112 | "域闭授予拒绝(EC1——战斗域外 apply_effect = false)" | effects.apply_effect(probe_id, src) == false | (待填) | | | -| 451 | 113 | "未知 effect_id 拒绝(EC2)" | effects.apply_effect("no_such_effect_id__", src) == false | (待填) | | | -| 452 | 115 | "开域后受理已知 id(布尔受理口径 true 通道)" | effects.apply_effect(probe_id, src) == true | (待填) | | | -| 453 | 121 | "受理后投影 effective_value = 层数×授予时快照(抗热重载 EC9 面)" | projected | (待填) | | | +| 443 | 107 | "前置:状态表非空" | not any_row.is_empty() | (待填) | | | +| 444 | 112 | "域闭授予拒绝(EC1——战斗域外 apply_effect = false)" | effects.apply_effect(probe_id, src) == false | (待填) | | | +| 445 | 113 | "未知 effect_id 拒绝(EC2)" | effects.apply_effect("no_such_effect_id__", src) == false | (待填) | | | +| 446 | 115 | "开域后受理已知 id(布尔受理口径 true 通道)" | effects.apply_effect(probe_id, src) == true | (待填) | | | +| 447 | 121 | "受理后投影 effective_value = 层数×授予时快照(抗热重载 EC9 面)" | projected | (待填) | | | -## S71 _smoke_adventure_api(行 61–82 · 11 条 · 源 game/tests/unit/core/adventure_feats_test.gd · 去向:待分类回填) +## S71 _smoke_adventure_api(行 70–91 · 11 条 · 源 game/tests/unit/core/adventure_feats_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 454 | 61 | "冒险启动受理(主种子注入恰一次——state=running;种子 42=首局固定种子生成图——精确路径断言需要确定图(卡② 代差))" | adr.start_adventure(42) | (待填) | | | -| 455 | 62 | "进行中重复 start_adventure 拒绝(重开须待结算完毕)" | not adr.start_adventure(43) | (待填) | | | -| 456 | 63 | "停点前移次序契约:level_started 先于入口候选呈现(反序选路口会被呈现层 runner 空防御丢弃)" | sig_seq.size() == 2 and String(sig_seq[0]) == "level_started" and String(sig_seq[1]) == "choices_presented" | (待填) | | | -| 457 | 64 | "入口停点:选路态 + 入口单候选 + 首战未开(呈现零 RNG 消耗——pick 前零战斗域)" | adr.runner().awaiting_choice() and (obs["entry_cands"] as Array).size() == 1 \\ and adr.runner().current_battle() == null | (待填) | | | -| 458 | 67 | "pick 入口候选受理(同一 _enter_node——与旧自动进入逐位等价)" | adr.runner().pick_next_node(int(adr.runner().next_candidate_ids()[0])) | (待填) | | | -| 459 | 68 | "pick 后 node_entered 到达 + 首战在飞(接口①开始关卡——停点闭合直达战斗节点)" | sig_seq.size() == 3 and String(sig_seq[2]) == "node_entered" and adr.runner().current_battle() != null | (待填) | | | -| 460 | 69 | "战斗中 abandon 拒绝(战斗域单事务在飞)" | not adr.abandon() | (待填) | | | -| 461 | 78 | "首战胜后选路态(非战斗期——current_battle 置空)" | adr.runner().current_battle() == null and adr.runner().awaiting_choice() | (待填) | | | -| 462 | 79 | "非战斗期 abandon 受理(选路态——同失败型结算)" | adr.abandon() | (待填) | | | -| 463 | 81 | "放弃结算:result=abandoned · cleared=false(已获面不追回——钥石累计 %d)" ↩ % int(sum.get("keystone_total", -1)) | String(sum.get("result", "")) == "abandoned" and not bool(sum.get("cleared", true)) | (待填) | | | -| 464 | 82 | "结算后重复 abandon 拒绝(结算受理只算一次——state=idle)" | not adr.abandon() | (待填) | | | +| 448 | 70 | "冒险启动受理(主种子注入恰一次——state=running;种子 42=首局固定种子生成图——精确路径断言需要确定图(卡② 代差))" | adr.start_adventure(42) | (待填) | | | +| 449 | 71 | "进行中重复 start_adventure 拒绝(重开须待结算完毕)" | not adr.start_adventure(43) | (待填) | | | +| 450 | 72 | "停点前移次序契约:level_started 先于入口候选呈现(反序选路口会被呈现层 runner 空防御丢弃)" | sig_seq.size() == 2 and String(sig_seq[0]) == "level_started" and String(sig_seq[1]) == "choices_presented" | (待填) | | | +| 451 | 73 | "入口停点:选路态 + 入口单候选 + 首战未开(呈现零 RNG 消耗——pick 前零战斗域)" | adr.runner().awaiting_choice() and (obs["entry_cands"] as Array).size() == 1 \\ and adr.runner().current_battle() == null | (待填) | | | +| 452 | 76 | "pick 入口候选受理(同一 _enter_node——与旧自动进入逐位等价)" | adr.runner().pick_next_node(int(adr.runner().next_candidate_ids()[0])) | (待填) | | | +| 453 | 77 | "pick 后 node_entered 到达 + 首战在飞(接口①开始关卡——停点闭合直达战斗节点)" | sig_seq.size() == 3 and String(sig_seq[2]) == "node_entered" and adr.runner().current_battle() != null | (待填) | | | +| 454 | 78 | "战斗中 abandon 拒绝(战斗域单事务在飞)" | not adr.abandon() | (待填) | | | +| 455 | 87 | "首战胜后选路态(非战斗期——current_battle 置空)" | adr.runner().current_battle() == null and adr.runner().awaiting_choice() | (待填) | | | +| 456 | 88 | "非战斗期 abandon 受理(选路态——同失败型结算)" | adr.abandon() | (待填) | | | +| 457 | 90 | "放弃结算:result=abandoned · cleared=false(已获面不追回——钥石累计 %d)" ↩ % int(sum.get("keystone_total", -1)) | String(sum.get("result", "")) == "abandoned" and not bool(sum.get("cleared", true)) | (待填) | | | +| 458 | 91 | "结算后重复 abandon 拒绝(结算受理只算一次——state=idle)" | not adr.abandon() | (待填) | | | -## S72 _smoke_adventure(行 118–443 · 57 条 · 源 game/tests/unit/core/adventure_feats_test.gd · 去向:待分类回填) +## S72 _smoke_adventure(行 127–454 · 57 条 · 源 game/tests/unit/core/adventure_feats_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 465 | 118 | "序列关数 = 行集关数(三关正式序列生效)" | int(_db.level.rows.size()) == 3 | (待填) | | | -| 466 | 135 | "生成图每个战斗/Boss 节点经解析链命中敌行集与棋盘行(扫描 %d 节点,缺失 %d)〔fv24〕" ↩ % [cov_scan, cov_miss] | cov_scan > 0 and cov_miss == 0 | (待填) | | | -| 467 | 140 | "满配冒险成功(42 / atk+900 / hp+400——cleared=true;v2 怪阶梯后旧 200/100 档不再满配)" | String(st["result"]) == "completed" and bool(sum.get("cleared", false)) | (待填) | | | -| 468 | 141 | "完成几关 = 3(levels_cleared)" | int(sum.get("levels_cleared", -1)) == 3 | (待填) | | | -| 469 | 145 | "钥石合计 = ΣBoss 首杀行值(%d——逐杀掉落新口径;断点未注入恒首杀)" ↩ % int(sum.get("keystone_total", -1)) | int(sum.get("keystone_total", -1)) == ks_exp | (待填) | | | -| 470 | 147 | "逐杀掉落明细恰 3 笔(L1/L2/L3——additive 摘要键 keystone_drops)" | drops.size() == 3 | (待填) | | | -| 471 | 148 | "摘要携带主种子(42)" | int(sum.get("master_seed", -1)) == 42 | (待填) | | | -| 472 | 159 | "关卡完成三选一恰呈现 2 次、恰兑付 2 次(L1/L2 后——末关不发;resolves 含节点回执另计)" | picks.size() == 2 and resolves.size() == 2 + node_picks | (待填) | | | -| 473 | 170 | "三关生成图各含 ≥1 奖励节点(节点三选一候选位在图)〔v4〕" | reward_levels == 3 | (待填) | | | -| 474 | 174 | "奖励分支点选→三选一候选可取且停点在途(候选 %s 件)" ↩ % str((rw.get("cands", []) as Array).size()) | bool(rw.get("pending", false)) and (rw.get("cands", []) as Array).size() >= 1 | (待填) | | | -| 475 | 187 | "奖励三选一点选兑付受理(件=%s)" ↩ % String(rw_pc.get("kind", "")) | rw_pick_ok | (待填) | | | -| 476 | 193 | "进战载荷两样=关卡序号+节点深度(实测 depth=%s 句柄=%s)〔v6〕" ↩ % [str(enc_probe.get("node_depth", -1)), str(node_probe.get("id", -1))] | not enc_probe.is_empty() and int(enc_probe.get("node_depth", -1)) == int(node_probe.get("depth", -1)) and int(enc_probe.get("level_index", -1)) == 1 and int(node_probe.get("id", -1)) != int(node_probe.get("depth", -1)) | (待填) | | | -| 477 | 202 | "奖励节点兑付回执与经停逐笔相等(context=reward_node——同账计数 %d/%d)" ↩ % [node_resolves, node_picks] | node_resolves == node_picks | (待填) | | | -| 478 | 249 | "三选一候选 2–3 枚且零重复(令牌洗牌不放回——kind+tier 复合身份)" | cand_ok | (待填) | | | -| 479 | 250 | "关卡完成部分回血封顶(floor(观测上限×比例) 封顶——购血观测值精确断言,核代码增补)" | heal_ok | (待填) | | | -| 480 | 251 | "钥石逐杀入账(首杀档位 + 累计逐位)" | ks_gain_ok | (待填) | | | -| 481 | 252 | "点选恰兑付选中一件(值=令牌档位表值;未选项零生效)" | resolve_ok | (待填) | | | -| 482 | 263 | "各战上限入参 ∈ [基础, 基础+3×件值+购血余量](覆盖值通道——双渠道共享上限 3;模拟档贪心购血合法叠加〔数值修复批 v3.4〕;首战=基础 %d)" ↩ % exp_base | seq_ok | (待填) | | | -| 483 | 266 | "同种子两局逐位可复现(summary/上限序列/行进序列/三选一载荷——reward_pick 封闭流)" | st2["summary"] == sum and st2["encounter_max_hp_seq"] == seq and st2["visited_ids"] == st["visited_ids"] and st2["reward_events"] == st["reward_events"] | (待填) | | | -| 484 | 276 | "零配 L1 败北种子可达(seed=%s)" ↩ % str(lose.get("s", -1)) | not lose.is_empty() | (待填) | | | -| 485 | 279 | "L1 败北摘要:未打穿 · 完成 0 关 · 钥石 0 · 止步第 1 层" | not bool(sl["cleared"]) and int(sl["levels_cleared"]) == 0 \\ and int(sl["keystone_total"]) == 0 and int(sl["stopped_at"]) == 1 | (待填) | | | -| 486 | 283 | "L1 败北败退明细:层号/节点编号/节点类型在案(数值验证取数面 2026-09-22)" | int(dl.get("level_index", -1)) == 1 and int(dl.get("node_id", -1)) >= 0 \\ and String(dl.get("node_type", "")) in ["battle", "boss"] | (待填) | | | -| 487 | 292 | "半配(atk+50)L2 败北种子可达(seed=%s)" ↩ % str(half.get("s", -1)) | not half.is_empty() | (待填) | | | -| 488 | 295 | "L2 败北摘要:L1 已过(钥石=L1 首杀掉落)· 止步第 2 层" | not bool(sh["cleared"]) and int(sh["levels_cleared"]) == 1 \\ and int(sh["keystone_total"]) == int(_db.meta_boss_keystone_row(1).get("first_kill", 0)) \\ and int(sh["stopped_at"]) == 2 | (待填) | | | -| 489 | 300 | "L2 败北败退明细:层号=2 · 节点在案(数值验证取数面 2026-09-22)" | int(dh.get("level_index", -1)) == 2 and int(dh.get("node_id", -1)) >= 0 \\ and String(dh.get("node_type", "")) in ["battle", "boss"] | (待填) | | | -| 490 | 306 | "重复拒绝场景:冒险启动(seed 7——生成图确定同上)" | adr6.start_adventure(7) | (待填) | | | -| 491 | 314 | "重复拒绝场景就位:选路态放弃 → 已结算" | adr6.abandon() | (待填) | | | -| 492 | 317 | "重复关卡结束宣布拒绝(账本与摘要零变化——契约 7)" | adr6.summary == sum6_before | (待填) | | | -| 493 | 327 | "每战完成入账恰一次(gain %d = battles %d——GP 袋件入账单列)" ↩ % [gains, int(st["battles"])] | gains == int(st["battles"]) | (待填) | | | -| 494 | 328 | "钱包 clear 恰一次且为末事件(关卡收束不清——GP 冒险收束清零归冒险结构)" | clears == 1 and wallet.size() > 0 and String(wallet[wallet.size() - 1]["event"]) == "clear" | (待填) | | | -| 495 | 330 | "三关商店房各到达一次(每关入口→终点路径恰穿一店的驱动投影)且首达余额 > 0(%d——GP 跨关延续)" ↩ % (shop_seq[0] if shop_seq.size() > 0 else -1) | shop_seq.size() == 3 and int(shop_seq[0]) > 0 | (待填) | | | -| 496 | 341 | "同账场景种子可达(seed=%s——L2 商店房就位)" ↩ % str(same.get("s", -1)) | not same.is_empty() | (待填) | | | -| 497 | 354 | "进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" ↩ % [hp_price + l2_price_bonus, hp_price, l2_price_bonus] | gp0 >= hp_price + l2_price_bonus | (待填) | | | -| 498 | 355 | "购买 hp_up 受理(出账 + 加算区)" | r8.buy_product("hp_up") | (待填) | | | -| 499 | 357 | "统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" ↩ % acc_shop | acc_shop >= 3 | (待填) | | | -| 500 | 360 | "L2 关卡完成三选一停点在案(直调兑付前——零自动推进)〔拆件批 V2〕" | not a8.stop()._pending.is_empty() | (待填) | | | -| 501 | 361 | "未兑付推进拒绝(选择停点零自动推进契约)" | not a8.continue_to_next_level() | (待填) | | | -| 502 | 364 | "三选一点选兑付受理(候选取首件)" | a8.stop().pick_curio(String(l2_pick.get("kind", "")), String(l2_pick.get("tier", "")), String(l2_pick.get("feat_id", ""))) | (待填) | | | -| 503 | 367 | "三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" ↩ % [a8.journal().bonus_total("max_hp"), acc_shop, picked2] | a8.journal().bonus_total("max_hp") == expect_acc | (待填) | | | -| 504 | 369 | "关卡完成部分回血封顶(hp %d ≤ 生效上限 %d——血回满语义退役)" ↩ % [a8.stats().hero_hp, max_eff8] | a8.stats().hero_hp <= max_eff8 | (待填) | | | -| 505 | 370 | "兑付后推进受理(下一关开始)" | a8.continue_to_next_level() | (待填) | | | -| 506 | 373 | "恢复点数学:floor(100×30%)=30 · 封顶缺口 5 · 满血回 0 合法" | int(a9.apply_rest(30, 70, 100)) == 30 and int(a9.apply_rest(30, 95, 100)) == 5 \\ and int(a9.apply_rest(30, 100, 100)) == 0 | (待填) | | | -| 507 | 377 | "同一公式两个落点逐位一致(LevelRunner.rest_heal_amount 内置默认)" | int(lr9.rest_heal_amount(30, 70, 100)) == 30 and int(lr9.rest_heal_amount(30, 95, 100)) == 5 | (待填) | | | -| 508 | 381 | "放弃场景:冒险启动(seed 15——生成图确定同上)" | a10.start_adventure(15) | (待填) | | | -| 509 | 382 | "放弃场景:入口停点 pick 进首战(停点前移批 2026-09-23 驱动口径)" | a10.runner().pick_next_node(int(a10.runner().next_candidate_ids()[0])) | (待填) | | | -| 510 | 383 | "战斗中 abandon 拒绝(战斗域单事务在飞)" | not a10.abandon() | (待填) | | | -| 511 | 390 | "非战斗期(选路态)abandon 受理" | a10.runner().current_battle() == null and a10.abandon() | (待填) | | | -| 512 | 394 | "剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 wide_scout_band)" | a11.start_adventure(11, {"treasure_unlocked": {"wide_scout_band": true}}) | (待填) | | | -| 513 | 403 | "池耗即枯竭(attack/ap 令牌耗尽不出候选——%s)" ↩ % str(draw) | not draw_kinds.has("attack") and not draw_kinds.has("ap") | (待填) | | | -| 514 | 404 | "池耗后候选恒 ≥2(稀有档余量——防「三选项全不可选」死角)" | (draw as Array).size() >= 2 | (待填) | | | -| 515 | 413 | "池收缩——40 次重抽零 aux 候选(停点渠道 aux 卡退役,迭代 11 第①批 T7)" | not treasure_seen | (待填) | | | -| 516 | 422 | "满血关卡完成回血 0 合法(封顶口径——heal_gained=0 非负溢出)" | int(cap["payload"].get("heal_gained", -1)) == 0 | (待填) | | | -| 517 | 425 | "节点共享池场景:冒险启动(seed 13)" | a13.start_adventure(13, {"treasure_unlocked": {"wide_scout_band": true}}) | (待填) | | | -| 518 | 436 | "奖励节点共享池:令牌耗尽同样不出候选(同库同池——%s)" ↩ % str(ncands13) | not has_attack13 | (待填) | | | -| 519 | 440 | "奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" ↩ % int(_db.adventure.curio_value("gp", "common")) | a13.stop().apply_node_curio("gp", "common") and a13.wallet().gp == gp13 + int(_db.adventure.curio_value("gp", "common")) | (待填) | | | -| 520 | 441 | "节点停点态兑付后清零(恰一次语义同构)" | a13.stop()._node_pending.is_empty() | (待填) | | | -| 521 | 443 | "放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)" | String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true)) | (待填) | | | +| 459 | 127 | "序列关数 = 行集关数(三关正式序列生效)" | int(_db.run.level.rows.size()) == 3 | (待填) | | | +| 460 | 144 | "生成图每个战斗/Boss 节点经解析链命中敌行集与棋盘行(扫描 %d 节点,缺失 %d)〔fv24〕" ↩ % [cov_scan, cov_miss] | cov_scan > 0 and cov_miss == 0 | (待填) | | | +| 461 | 149 | "满配冒险成功(42 / atk+900 / hp+400——cleared=true;v2 怪阶梯后旧 200/100 档不再满配)" | String(st["result"]) == "completed" and bool(sum.get("cleared", false)) | (待填) | | | +| 462 | 150 | "完成几关 = 3(levels_cleared)" | int(sum.get("levels_cleared", -1)) == 3 | (待填) | | | +| 463 | 154 | "钥石合计 = ΣBoss 首杀行值(%d——逐杀掉落新口径;断点未注入恒首杀)" ↩ % int(sum.get("keystone_total", -1)) | int(sum.get("keystone_total", -1)) == ks_exp | (待填) | | | +| 464 | 156 | "逐杀掉落明细恰 3 笔(L1/L2/L3——additive 摘要键 keystone_drops)" | drops.size() == 3 | (待填) | | | +| 465 | 157 | "摘要携带主种子(42)" | int(sum.get("master_seed", -1)) == 42 | (待填) | | | +| 466 | 168 | "关卡完成三选一恰呈现 2 次、恰兑付 2 次(L1/L2 后——末关不发;resolves 含节点回执另计)" | picks.size() == 2 and resolves.size() == 2 + node_picks | (待填) | | | +| 467 | 179 | "三关生成图各含 ≥1 奖励节点(节点三选一候选位在图)〔v4〕" | reward_levels == 3 | (待填) | | | +| 468 | 183 | "奖励分支点选→三选一候选可取且停点在途(候选 %s 件)" ↩ % str((rw.get("cands", []) as Array).size()) | bool(rw.get("pending", false)) and (rw.get("cands", []) as Array).size() >= 1 | (待填) | | | +| 469 | 196 | "奖励三选一点选兑付受理(件=%s)" ↩ % String(rw_pc.get("kind", "")) | rw_pick_ok | (待填) | | | +| 470 | 202 | "进战载荷两样=关卡序号+节点深度(实测 depth=%s 句柄=%s)〔v6〕" ↩ % [str(enc_probe.get("node_depth", -1)), str(node_probe.get("id", -1))] | not enc_probe.is_empty() and int(enc_probe.get("node_depth", -1)) == int(node_probe.get("depth", -1)) and int(enc_probe.get("level_index", -1)) == 1 and int(node_probe.get("id", -1)) != int(node_probe.get("depth", -1)) | (待填) | | | +| 471 | 211 | "奖励节点兑付回执与经停逐笔相等(context=reward_node——同账计数 %d/%d)" ↩ % [node_resolves, node_picks] | node_resolves == node_picks | (待填) | | | +| 472 | 258 | "三选一候选 2–3 枚且零重复(令牌洗牌不放回——kind+tier 复合身份)" | cand_ok | (待填) | | | +| 473 | 259 | "关卡完成部分回血封顶(floor(观测上限×比例) 封顶——购血观测值精确断言,核代码增补)" | heal_ok | (待填) | | | +| 474 | 260 | "钥石逐杀入账(首杀档位 + 累计逐位)" | ks_gain_ok | (待填) | | | +| 475 | 261 | "点选恰兑付选中一件(值=令牌档位表值;未选项零生效)" | resolve_ok | (待填) | | | +| 476 | 272 | "各战上限入参 ∈ [基础, 基础+3×件值+购血余量](覆盖值通道——双渠道共享上限 3;模拟档贪心购血合法叠加〔数值修复批 v3.4〕;首战=基础 %d)" ↩ % exp_base | seq_ok | (待填) | | | +| 477 | 275 | "同种子两局逐位可复现(summary/上限序列/行进序列/三选一载荷——reward_pick 封闭流)" | st2["summary"] == sum and st2["encounter_max_hp_seq"] == seq and st2["visited_ids"] == st["visited_ids"] and st2["reward_events"] == st["reward_events"] | (待填) | | | +| 478 | 285 | "零配 L1 败北种子可达(seed=%s)" ↩ % str(lose.get("s", -1)) | not lose.is_empty() | (待填) | | | +| 479 | 288 | "L1 败北摘要:未打穿 · 完成 0 关 · 钥石 0 · 止步第 1 层" | not bool(sl["cleared"]) and int(sl["levels_cleared"]) == 0 \\ and int(sl["keystone_total"]) == 0 and int(sl["stopped_at"]) == 1 | (待填) | | | +| 480 | 292 | "L1 败北败退明细:层号/节点编号/节点类型在案(数值验证取数面 2026-09-22)" | int(dl.get("level_index", -1)) == 1 and int(dl.get("node_id", -1)) >= 0 \\ and String(dl.get("node_type", "")) in ["battle", "boss"] | (待填) | | | +| 481 | 301 | "半配(atk+50)L2 败北种子可达(seed=%s)" ↩ % str(half.get("s", -1)) | not half.is_empty() | (待填) | | | +| 482 | 304 | "L2 败北摘要:L1 已过(钥石=L1 首杀掉落)· 止步第 2 层" | not bool(sh["cleared"]) and int(sh["levels_cleared"]) == 1 \\ and int(sh["keystone_total"]) == int(_db.crossrun.meta.boss_keystone_row(1).get("first_kill", 0)) \\ and int(sh["stopped_at"]) == 2 | (待填) | | | +| 483 | 309 | "L2 败北败退明细:层号=2 · 节点在案(数值验证取数面 2026-09-22)" | int(dh.get("level_index", -1)) == 2 and int(dh.get("node_id", -1)) >= 0 \\ and String(dh.get("node_type", "")) in ["battle", "boss"] | (待填) | | | +| 484 | 315 | "重复拒绝场景:冒险启动(seed 7——生成图确定同上)" | adr6.start_adventure(7) | (待填) | | | +| 485 | 323 | "重复拒绝场景就位:选路态放弃 → 已结算" | adr6.abandon() | (待填) | | | +| 486 | 326 | "重复关卡结束宣布拒绝(账本与摘要零变化——契约 7)" | adr6.summary == sum6_before | (待填) | | | +| 487 | 336 | "每战完成入账恰一次(gain %d = battles %d——GP 袋件入账单列)" ↩ % [gains, int(st["battles"])] | gains == int(st["battles"]) | (待填) | | | +| 488 | 337 | "钱包 clear 恰一次且为末事件(关卡收束不清——GP 冒险收束清零归冒险结构)" | clears == 1 and wallet.size() > 0 and String(wallet[wallet.size() - 1]["event"]) == "clear" | (待填) | | | +| 489 | 339 | "三关商店房各到达一次(每关入口→终点路径恰穿一店的驱动投影)且首达余额 > 0(%d——GP 跨关延续)" ↩ % (shop_seq[0] if shop_seq.size() > 0 else -1) | shop_seq.size() == 3 and int(shop_seq[0]) > 0 | (待填) | | | +| 490 | 350 | "同账场景种子可达(seed=%s——L2 商店房就位)" ↩ % str(same.get("s", -1)) | not same.is_empty() | (待填) | | | +| 491 | 363 | "进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" ↩ % [hp_price + l2_price_bonus, hp_price, l2_price_bonus] | gp0 >= hp_price + l2_price_bonus | (待填) | | | +| 492 | 364 | "购买 hp_up 受理(出账 + 加算区)" | r8.buy_product("hp_up") | (待填) | | | +| 493 | 366 | "统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" ↩ % acc_shop | acc_shop >= 3 | (待填) | | | +| 494 | 371 | "L2 关卡完成三选一停点在案(直调兑付前——零自动推进)〔拆件批 V2〕" | not (l2_cap["payload"] as Dictionary).is_empty() | (待填) | | | +| 495 | 372 | "未兑付推进拒绝(选择停点零自动推进契约)" | not a8.continue_to_next_level() | (待填) | | | +| 496 | 375 | "三选一点选兑付受理(候选取首件)" | a8.stop().pick_curio(String(l2_pick.get("kind", "")), String(l2_pick.get("tier", "")), String(l2_pick.get("feat_id", ""))) | (待填) | | | +| 497 | 378 | "三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" ↩ % [a8.journal().bonus_total("max_hp"), acc_shop, picked2] | a8.journal().bonus_total("max_hp") == expect_acc | (待填) | | | +| 498 | 380 | "关卡完成部分回血封顶(hp %d ≤ 生效上限 %d——血回满语义退役)" ↩ % [a8.stats().hero_hp, max_eff8] | a8.stats().hero_hp <= max_eff8 | (待填) | | | +| 499 | 381 | "兑付后推进受理(下一关开始)" | a8.continue_to_next_level() | (待填) | | | +| 500 | 384 | "恢复点数学:floor(100×30%)=30 · 封顶缺口 5 · 满血回 0 合法" | int(a9.apply_rest(30, 70, 100)) == 30 and int(a9.apply_rest(30, 95, 100)) == 5 \\ and int(a9.apply_rest(30, 100, 100)) == 0 | (待填) | | | +| 501 | 388 | "同一公式两个落点逐位一致(LevelRunner.rest_heal_amount 内置默认)" | int(lr9.rest_heal_amount(30, 70, 100)) == 30 and int(lr9.rest_heal_amount(30, 95, 100)) == 5 | (待填) | | | +| 502 | 392 | "放弃场景:冒险启动(seed 15——生成图确定同上)" | a10.start_adventure(15) | (待填) | | | +| 503 | 393 | "放弃场景:入口停点 pick 进首战(停点前移批 2026-09-23 驱动口径)" | a10.runner().pick_next_node(int(a10.runner().next_candidate_ids()[0])) | (待填) | | | +| 504 | 394 | "战斗中 abandon 拒绝(战斗域单事务在飞)" | not a10.abandon() | (待填) | | | +| 505 | 401 | "非战斗期(选路态)abandon 受理" | a10.runner().current_battle() == null and a10.abandon() | (待填) | | | +| 506 | 405 | "剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 wide_scout_band)" | a11.start_adventure(11, {"treasure_unlocked": {"wide_scout_band": true}}) | (待填) | | | +| 507 | 414 | "池耗即枯竭(attack/ap 令牌耗尽不出候选——%s)" ↩ % str(draw) | not draw_kinds.has("attack") and not draw_kinds.has("ap") | (待填) | | | +| 508 | 415 | "池耗后候选恒 ≥2(稀有档余量——防「三选项全不可选」死角)" | (draw as Array).size() >= 2 | (待填) | | | +| 509 | 424 | "池收缩——40 次重抽零 aux 候选(停点渠道 aux 卡退役,迭代 11 第①批 T7)" | not treasure_seen | (待填) | | | +| 510 | 433 | "满血关卡完成回血 0 合法(封顶口径——heal_gained=0 非负溢出)" | int(cap["payload"].get("heal_gained", -1)) == 0 | (待填) | | | +| 511 | 436 | "节点共享池场景:冒险启动(seed 13)" | a13.start_adventure(13, {"treasure_unlocked": {"wide_scout_band": true}}) | (待填) | | | +| 512 | 447 | "奖励节点共享池:令牌耗尽同样不出候选(同库同池——%s)" ↩ % str(ncands13) | not has_attack13 | (待填) | | | +| 513 | 451 | "奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" ↩ % int(_db.run.adventure.curio_value("gp", "common")) | a13.stop().apply_node_curio("gp", "common") and a13.wallet().gp == gp13 + int(_db.run.adventure.curio_value("gp", "common")) | (待填) | | | +| 514 | 452 | "节点停点态兑付后清零(恰一次语义同构)" | (a13.stop().node_curio_pending_state().get("candidates", []) as Array).is_empty() | (待填) | | | +| 515 | 454 | "放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)" | String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true)) | (待填) | | | -## S73 _smoke_feats(行 454–766 · 68 条 · 源 game/tests/unit/core/adventure_feats_test.gd · 去向:待分类回填) +## S73 _smoke_feats(行 465–777 · 68 条 · 源 game/tests/unit/core/adventure_feats_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 522 | 454 | "feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)" | _db.feats != null and (_db.feats.rows as Array).size() == 30 | (待填) | | | -| 523 | 455 | "feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)" | _db.feats.validate().is_empty() | (待填) | | | -| 524 | 457 | "专长带场景:冒险启动(seed 71)" | a_r0.start_adventure(71) | (待填) | | | -| 525 | 465 | "专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕" | feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0._curio.deck.size() == 132 | (待填) | | | -| 526 | 467 | "旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕" | _db.feats.row_by_id("arcane_ember").is_empty() and _db.feats.row_by_id("magma_body").is_empty() | (待填) | | | -| 527 | 468 | "迁出收尾:treasure_items 无 venom_edge/seal_burst 行(同调目录自然收窄)〔专长批 V13〕" | _db.treasure_items.row_by_id("venom_edge").is_empty() and _db.treasure_items.row_by_id("seal_burst").is_empty() | (待填) | | | -| 528 | 475 | "层级纪律:牌库 kind 全集=CURIO_KINDS+relic(秘宝 item_id 零入池)〔专长批 V18〕" | kinds_ok | (待填) | | | -| 529 | 481 | "棋盘事实不可变:新 30 行零棋盘改写类订阅(rewrite_board 已除名消费面)〔专长批 V19〕" | rewrite_rows == 0 | (待填) | | | -| 530 | 488 | "对外命名纪律:30 行 display_name/desc 零「雷/旗」字面(封印/陷阱风味系)〔专长批 V21 初筛〕" | bad_words == 0 | (待填) | | | -| 531 | 489 | "数值纪律:陷阱爆发倍率>1 · 血蜜回复<典型陷阱伤 · 石肤上限 3 在册(结构锚形态内 provisional)〔专长批 V22 机检半〕" | float((_db.feats.row_by_id("trap_burst").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("mult", 0)) > 1.0 and int((_db.feats.row_by_id("blood_honey").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("value", 99)) < 5 and int((_db.feats.row_by_id("stone_skin").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("cap", 0)) == 3 | (待填) | | | -| 532 | 507 | "新 schema 构造表 validate 零违例(四级 rarity/cap 字段/词表全过——流派标签已退役零 archetype)" | rt.validate().is_empty() | (待填) | | | -| 533 | 511 | "专长带场景:冒险启动(seed 72——注入 5 行临时表)" | a_r.start_adventure(72) | (待填) | | | -| 534 | 516 | "专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107" | deck_relic == 5 and a_r._curio.deck.size() == 107 | (待填) | | | -| 535 | 527 | "关卡完成渠道只出专长(60 次重抽 ≥2 种不同 relic——身份键 feat_id 同档可多件)" | feat_seen.size() >= 2 | (待填) | | | -| 536 | 528 | "节点渠道零专长(60 次重抽零 relic——专长在非 Boss 渠道只以专长凭证形态出现)" | not node_relic_seen | (待填) | | | -| 537 | 536 | "A5 专长凭证兑付受理(节点渠道——拿到即开)" | a_t.stop().apply_node_curio("feat_ticket", "excellent") | (待填) | | | -| 538 | 542 | "A5 拿到即开——停点候选已替换为专长三选一(全 relic)" | tkt_all_relic | (待填) | | | -| 539 | 543 | "A5 零摘牌——专长凭证兑付不动令牌库(不进持有集)" | a_t.deck().pool_stock("attack") == int(stock_before["attack"]) and a_t.deck().pool_stock("gp") == int(stock_before["gp"]) | (待填) | | | -| 540 | 544 | "A5 无专长凭证残留持有集" | not (a_t.feats_held() as Array).has("feat_ticket") | (待填) | | | -| 541 | 547 | "A5 专长面兑付受理(专长凭证开出候选可正常入持有集)" | (a_t.feats_held() as Array).size() == feats_n + 1 | (待填) | | | -| 542 | 552 | "A6 保底触发(pity=5 → 第 6 次展示必出——概率无关)" | a_t.deck().feat_ticket_consider(rg_t) | (待填) | | | -| 543 | 553 | "A6 触发后清零" | a_t.stop()._deck.ticket_pity == 0 | (待填) | | | -| 544 | 556 | "A6 限购满不再触发(保底冻结)" | not a_t.deck().feat_ticket_consider(rg_t) | (待填) | | | -| 545 | 564 | "A6 概率面存在(chance=20% 200 展示内必出现——零触发=概率面失守)" | offered | (待填) | | | -| 546 | 566 | "relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" ↩ % [deck_relic, deck_relic - 1] | a_r.stop().pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.deck().pool_stock("relic") == deck_relic - 1 | (待填) | | | -| 547 | 590 | "双管镐:第 %d 次付费挖格返 1 AP(每第 2 次——mod_eq 预评估口径)" ↩ % (i + 1) | refund | (待填) | | | -| 548 | 592 | "双管镐反例:第 %d 次不返(未满 2 次)" ↩ % (i + 1) | not refund | (待填) | | | -| 549 | 596 | "地质学家:首开新类型集 1 点〔专长批 V3〕" | orch._hits_of(geo_hits, "counter_add").size() == 1 | (待填) | | | -| 550 | 600 | "地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕" | orch._hits_of(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.fx_host().counters_ctx, "geo_types") == 0 | (待填) | | | -| 551 | 602 | "地质学家反例:同类型重复首开不叠点〔专长批 V3〕" | orch._hits_of(geo_hits3, "counter_add").is_empty() | (待填) | | | -| 552 | 606 | "安全帽:开安全格攒层 ×2〔专长批 V3〕" | EffectEngine.counter(orch.fx_host().counters_ctx, "helm_layers") == 2 | (待填) | | | -| 553 | 612 | "安全帽:踩雷耗层减伤=层值 2(一次清零随施加面 set 行)〔专长批 V3〕" | hh_reduce == 2 | (待填) | | | -| 554 | 616 | "跳格采掘:空白 0 格攒 1 点〔专长批 V3〕" | orch._hits_of(sk_zero, "counter_add").size() == 1 | (待填) | | | -| 555 | 620 | "跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕" | orch._hits_of(sk_full, "reveal_tiles").size() == 1 | (待填) | | | -| 556 | 621 | "跳格采掘反例:普通开格不计点〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty() | (待填) | | | -| 557 | 628 | "爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1 | (待填) | | | -| 558 | 630 | "边陲矿工:边缘格产出 +1·中心格反例零产出〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1 and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty() | (待填) | | | -| 559 | 638 | "封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕" | orch._hits_of(se_hits, "grant_ap").size() == 1 and String((orch._hits_of(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue" | (待填) | | | -| 560 | 647 | "封印匠:标中每第 3 次→中立移除恰一次(频率闸死)〔专长批 V3〕" | sm_remove == 1 | (待填) | | | -| 561 | 649 | "封印桩:标中本轮 AC +N(round 域——round_start 清零)〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus").size() == 1 and String((orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round" | (待填) | | | -| 562 | 653 | "示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "preview_area").size() == 1 | (待填) | | | -| 563 | 662 | "疼痛记忆:攻击附加层值伤害=层值 2·用后清零(set 行随施加面)〔专长批 V3〕" | pm_bonus == 2 | (待填) | | | -| 564 | 669 | "保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty() | (待填) | | | -| 565 | 673 | "保险丝:首点不出盾(2 点才出——预评估口径)〔专长批 V3〕" | fw_shield == 0 | (待填) | | | -| 566 | 675 | "见血开窍:踩雷揭示 1 格〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1 | (待填) | | | -| 567 | 682 | "陷阱本能:踩雷本轮攻击 +2(attack_bonus 通道——面板加算)〔专长批 V3〕" | ti_val == 2 | (待填) | | | -| 568 | 687 | "连环陷阱:同轮第 2 颗起每颗额外 +1 层(疼痛记忆同账)〔专长批 V3〕" | EffectEngine.counter(orch.fx_host().counters_ctx, "pain_layers") == ct_before + 2 | (待填) | | | -| 569 | 690 | "陷阱爆发:全体敌雷伤×倍率直伤(blast_scaled——施加面值=damage×mult)〔专长批 V3〕" | orch._hits_of(tb_hits, "blast_scaled").size() == 1 and float((orch._hits_of(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0 | (待填) | | | -| 570 | 694 | "血蜜:踩雷回复 N 生命〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "heal").size() == 1 | (待填) | | | -| 571 | 697 | "石肤:受伤 AC+1(本场域 cap 3——达上限不再涨由累积器封顶)〔专长批 V3〕" | orch._hits_of(ss_hits, "ac_bonus").size() == 1 and int((orch._hits_of(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3 | (待填) | | | -| 572 | 701 | "背水:生命≤50% 减伤·过半血反例零减伤〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1 and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty() | (待填) | | | -| 573 | 706 | "破封一击:挖开带封印危险格 +1 层·无封印反例零触发〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1 and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty() | (待填) | | | -| 574 | 714 | "弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕" | orch._hits_of(me_hits, "counter_add").size() == 1 | (待填) | | | -| 575 | 718 | "弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕" | me_atk == 4 | (待填) | | | -| 576 | 719 | "弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty() | (待填) | | | -| 577 | 722 | "一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕" | orch._hits_of(ld_hits, "grant_ap").size() == 1 | (待填) | | | -| 578 | 723 | "一滴不剩反例:上轮有余不发〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty() | (待填) | | | -| 579 | 729 | "收盘:本轮开格≥8→+1 GP〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1 | (待填) | | | -| 580 | 732 | "斩首学者:击杀揭示 1 格〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1 | (待填) | | | -| 581 | 735 | "处刑台:敌血量≤30% 挂单位标记加伤·阈值外反例零命中〔专长批 V3〕" | orch._hits_of(ex_hits, "target_attack_bonus").size() == 1 and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty() | (待填) | | | -| 582 | 739 | "战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1 | (待填) | | | -| 583 | 742 | "血税:双向同局账本(踩陷阱−2max_hp/封印命中+1atk——bonus_delta 战末汇入)〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "bonus_delta").size() == 1 and String((orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk" | (待填) | | | -| 584 | 747 | "淬毒刃纹(迁入):克制正面(magic_vs_heavy 矩阵值>0)追加·中性面反例零触发〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1 and orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty() | (待填) | | | -| 585 | 751 | "封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕" | orch._hits_of(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "open_ticket").size() == 1 | (待填) | | | -| 586 | 755 | "零持有订阅=零命中(relic 关着零扰动——伤害管线既有行为逐位不变)" | orch0.fx_host().fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}).is_empty() and orch0.fx_host().fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": true, "terrain_kind": []}).is_empty() and orch0.fx_host().fx_emit("mine_hit", {"damage": 5}).is_empty() and orch0.fx_host().fx_emit("battle_won", {"round": 1}).is_empty() | (待填) | | | -| 587 | 763 | "特性段空态隐藏(视图空态纪律同特性段)" | not panel._feat_section.visible | (待填) | | | -| 588 | 765 | "特性段持有呈现(逐件行常驻可读——「局内奖励选了什么看不到」缺口承载)" | panel._feat_section.visible and panel._feat_list.get_child_count() == 2 | (待填) | | | -| 589 | 766 | "特性卡文案表值化:desc 行字段直读(零硬编码映射)" | String(load("res://presentation/battle_narrator.gd").feat_card_text({"feat_id": "pain_memory", "display_name": "疼痛记忆", "desc": "触陷阱攒 1 层"})).contains("触陷阱攒 1 层") | (待填) | | | +| 516 | 465 | "feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)" | _db.crossrun.feats != null and (_db.crossrun.feats.rows as Array).size() == 30 | (待填) | | | +| 517 | 466 | "feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)" | _db.crossrun.feats.validate().is_empty() | (待填) | | | +| 518 | 468 | "专长带场景:冒险启动(seed 71)" | a_r0.start_adventure(71) | (待填) | | | +| 519 | 476 | "专长带入池:30 件四级直映射(普通 12/优秀 9/稀有 9/传说 0)——牌库 102+30=132〔专长批 V2+卡② 完美保险迁入〕" | feat_tokens == 30 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 9 and a_r0.deck().deck.size() == 132 | (待填) | | | +| 520 | 478 | "旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕" | _db.crossrun.feats.row_by_id("arcane_ember").is_empty() and _db.crossrun.feats.row_by_id("magma_body").is_empty() | (待填) | | | +| 521 | 479 | "迁出收尾:treasure_items 无 venom_edge/seal_burst 行(同调目录自然收窄)〔专长批 V13〕" | _db.crossrun.treasure_items.row_by_id("venom_edge").is_empty() and _db.crossrun.treasure_items.row_by_id("seal_burst").is_empty() | (待填) | | | +| 522 | 486 | "层级纪律:牌库 kind 全集=CURIO_KINDS+relic(秘宝 item_id 零入池)〔专长批 V18〕" | kinds_ok | (待填) | | | +| 523 | 492 | "棋盘事实不可变:新 30 行零棋盘改写类订阅(rewrite_board 已除名消费面)〔专长批 V19〕" | rewrite_rows == 0 | (待填) | | | +| 524 | 499 | "对外命名纪律:30 行 display_name/desc 零「雷/旗」字面(封印/陷阱风味系)〔专长批 V21 初筛〕" | bad_words == 0 | (待填) | | | +| 525 | 500 | "数值纪律:陷阱爆发倍率>1 · 血蜜回复<典型陷阱伤 · 石肤上限 3 在册(结构锚形态内 provisional)〔专长批 V22 机检半〕" | float((_db.crossrun.feats.row_by_id("trap_burst").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("mult", 0)) > 1.0 and int((_db.crossrun.feats.row_by_id("blood_honey").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("value", 99)) < 5 and int((_db.crossrun.feats.row_by_id("stone_skin").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("cap", 0)) == 3 | (待填) | | | +| 526 | 518 | "新 schema 构造表 validate 零违例(四级 rarity/cap 字段/词表全过——流派标签已退役零 archetype)" | rt.validate().is_empty() | (待填) | | | +| 527 | 522 | "专长带场景:冒险启动(seed 72——注入 5 行临时表)" | a_r.start_adventure(72) | (待填) | | | +| 528 | 527 | "专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107" | deck_relic == 5 and a_r.deck().deck.size() == 107 | (待填) | | | +| 529 | 538 | "关卡完成渠道只出专长(60 次重抽 ≥2 种不同 relic——身份键 feat_id 同档可多件)" | feat_seen.size() >= 2 | (待填) | | | +| 530 | 539 | "节点渠道零专长(60 次重抽零 relic——专长在非 Boss 渠道只以专长凭证形态出现)" | not node_relic_seen | (待填) | | | +| 531 | 547 | "A5 专长凭证兑付受理(节点渠道——拿到即开)" | a_t.stop().apply_node_curio("feat_ticket", "excellent") | (待填) | | | +| 532 | 553 | "A5 拿到即开——停点候选已替换为专长三选一(全 relic)" | tkt_all_relic | (待填) | | | +| 533 | 554 | "A5 零摘牌——专长凭证兑付不动令牌库(不进持有集)" | a_t.deck().pool_stock("attack") == int(stock_before["attack"]) and a_t.deck().pool_stock("gp") == int(stock_before["gp"]) | (待填) | | | +| 534 | 555 | "A5 无专长凭证残留持有集" | not (a_t.feats_held() as Array).has("feat_ticket") | (待填) | | | +| 535 | 558 | "A5 专长面兑付受理(专长凭证开出候选可正常入持有集)" | (a_t.feats_held() as Array).size() == feats_n + 1 | (待填) | | | +| 536 | 563 | "A6 保底触发(pity=5 → 第 6 次展示必出——概率无关)" | a_t.deck().feat_ticket_consider(rg_t) | (待填) | | | +| 537 | 564 | "A6 触发后清零" | a_t.deck().ticket_pity == 0 | (待填) | | | +| 538 | 567 | "A6 限购满不再触发(保底冻结)" | not a_t.deck().feat_ticket_consider(rg_t) | (待填) | | | +| 539 | 575 | "A6 概率面存在(chance=20% 200 展示内必出现——零触发=概率面失守)" | offered | (待填) | | | +| 540 | 577 | "relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" ↩ % [deck_relic, deck_relic - 1] | a_r.stop().pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.deck().pool_stock("relic") == deck_relic - 1 | (待填) | | | +| 541 | 601 | "双管镐:第 %d 次付费挖格返 1 AP(每第 2 次——mod_eq 预评估口径)" ↩ % (i + 1) | refund | (待填) | | | +| 542 | 603 | "双管镐反例:第 %d 次不返(未满 2 次)" ↩ % (i + 1) | not refund | (待填) | | | +| 543 | 607 | "地质学家:首开新类型集 1 点〔专长批 V3〕" | _hits_kind(geo_hits, "counter_add").size() == 1 | (待填) | | | +| 544 | 611 | "地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕" | _hits_kind(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.fx_host().counters_ctx, "geo_types") == 0 | (待填) | | | +| 545 | 613 | "地质学家反例:同类型重复首开不叠点〔专长批 V3〕" | _hits_kind(geo_hits3, "counter_add").is_empty() | (待填) | | | +| 546 | 617 | "安全帽:开安全格攒层 ×2〔专长批 V3〕" | EffectEngine.counter(orch.fx_host().counters_ctx, "helm_layers") == 2 | (待填) | | | +| 547 | 623 | "安全帽:踩雷耗层减伤=层值 2(一次清零随施加面 set 行)〔专长批 V3〕" | hh_reduce == 2 | (待填) | | | +| 548 | 627 | "跳格采掘:空白 0 格攒 1 点〔专长批 V3〕" | _hits_kind(sk_zero, "counter_add").size() == 1 | (待填) | | | +| 549 | 631 | "跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕" | _hits_kind(sk_full, "reveal_tiles").size() == 1 | (待填) | | | +| 550 | 632 | "跳格采掘反例:普通开格不计点〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty() | (待填) | | | +| 551 | 639 | "爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1 | (待填) | | | +| 552 | 641 | "边陲矿工:边缘格产出 +1·中心格反例零产出〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1 and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty() | (待填) | | | +| 553 | 649 | "封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕" | _hits_kind(se_hits, "grant_ap").size() == 1 and String((_hits_kind(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue" | (待填) | | | +| 554 | 658 | "封印匠:标中每第 3 次→中立移除恰一次(频率闸死)〔专长批 V3〕" | sm_remove == 1 | (待填) | | | +| 555 | 660 | "封印桩:标中本轮 AC +N(round 域——round_start 清零)〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus").size() == 1 and String((_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round" | (待填) | | | +| 556 | 664 | "示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "preview_area").size() == 1 | (待填) | | | +| 557 | 673 | "疼痛记忆:攻击附加层值伤害=层值 2·用后清零(set 行随施加面)〔专长批 V3〕" | pm_bonus == 2 | (待填) | | | +| 558 | 680 | "保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty() | (待填) | | | +| 559 | 684 | "保险丝:首点不出盾(2 点才出——预评估口径)〔专长批 V3〕" | fw_shield == 0 | (待填) | | | +| 560 | 686 | "见血开窍:踩雷揭示 1 格〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1 | (待填) | | | +| 561 | 693 | "陷阱本能:踩雷本轮攻击 +2(attack_bonus 通道——面板加算)〔专长批 V3〕" | ti_val == 2 | (待填) | | | +| 562 | 698 | "连环陷阱:同轮第 2 颗起每颗额外 +1 层(疼痛记忆同账)〔专长批 V3〕" | EffectEngine.counter(orch.fx_host().counters_ctx, "pain_layers") == ct_before + 2 | (待填) | | | +| 563 | 701 | "陷阱爆发:全体敌雷伤×倍率直伤(blast_scaled——施加面值=damage×mult)〔专长批 V3〕" | _hits_kind(tb_hits, "blast_scaled").size() == 1 and float((_hits_kind(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0 | (待填) | | | +| 564 | 705 | "血蜜:踩雷回复 N 生命〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "heal").size() == 1 | (待填) | | | +| 565 | 708 | "石肤:受伤 AC+1(本场域 cap 3——达上限不再涨由累积器封顶)〔专长批 V3〕" | _hits_kind(ss_hits, "ac_bonus").size() == 1 and int((_hits_kind(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3 | (待填) | | | +| 566 | 712 | "背水:生命≤50% 减伤·过半血反例零减伤〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1 and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty() | (待填) | | | +| 567 | 717 | "破封一击:挖开带封印危险格 +1 层·无封印反例零触发〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1 and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty() | (待填) | | | +| 568 | 725 | "弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕" | _hits_kind(me_hits, "counter_add").size() == 1 | (待填) | | | +| 569 | 729 | "弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕" | me_atk == 4 | (待填) | | | +| 570 | 730 | "弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty() | (待填) | | | +| 571 | 733 | "一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕" | _hits_kind(ld_hits, "grant_ap").size() == 1 | (待填) | | | +| 572 | 734 | "一滴不剩反例:上轮有余不发〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty() | (待填) | | | +| 573 | 740 | "收盘:本轮开格≥8→+1 GP〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1 | (待填) | | | +| 574 | 743 | "斩首学者:击杀揭示 1 格〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1 | (待填) | | | +| 575 | 746 | "处刑台:敌血量≤30% 挂单位标记加伤·阈值外反例零命中〔专长批 V3〕" | _hits_kind(ex_hits, "target_attack_bonus").size() == 1 and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty() | (待填) | | | +| 576 | 750 | "战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1 | (待填) | | | +| 577 | 753 | "血税:双向同局账本(踩陷阱−2max_hp/封印命中+1atk——bonus_delta 战末汇入)〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "bonus_delta").size() == 1 and String((_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk" | (待填) | | | +| 578 | 758 | "淬毒刃纹(迁入):克制正面(magic_vs_heavy 矩阵值>0)追加·中性面反例零触发〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1 and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty() | (待填) | | | +| 579 | 762 | "封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "open_ticket").size() == 1 | (待填) | | | +| 580 | 766 | "零持有订阅=零命中(relic 关着零扰动——伤害管线既有行为逐位不变)" | orch0.fx_host().fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}).is_empty() and orch0.fx_host().fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": true, "terrain_kind": []}).is_empty() and orch0.fx_host().fx_emit("mine_hit", {"damage": 5}).is_empty() and orch0.fx_host().fx_emit("battle_won", {"round": 1}).is_empty() | (待填) | | | +| 581 | 774 | "特性段空态隐藏(视图空态纪律同特性段)" | not panel._feat_section.visible | (待填) | | | +| 582 | 776 | "特性段持有呈现(逐件行常驻可读——「局内奖励选了什么看不到」缺口承载)" | panel._feat_section.visible and panel._feat_list.get_child_count() == 2 | (待填) | | | +| 583 | 777 | "特性卡文案表值化:desc 行字段直读(零硬编码映射)" | String(load("res://presentation/battle_narrator.gd").feat_card_text({"feat_id": "pain_memory", "display_name": "疼痛记忆", "desc": "触陷阱攒 1 层"})).contains("触陷阱攒 1 层") | (待填) | | | ## S74 _smoke_layout_cell_formula(行 65–68 · 2 条 · 源 game/tests/unit/core/board_gen_reward_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 590 | 65 | "L%d %dx%d 板网格高 %d ≤ 可用高 %d − 预留 24(格边 %dpx——下缘不贴边不裁切)" ↩ % [L, b.width, b.height, grid_h, int(avail.y), int(cell)] | grid_h <= int(avail.y) - 24 | (待填) | | | -| 591 | 67 | "L%d 格边在 26–46 档内(%dpx)" ↩ % [L, int(cell)] | cell >= 26.0 and cell <= 46.0 | (待填) | | | +| 584 | 65 | "L%d %dx%d 板网格高 %d ≤ 可用高 %d − 预留 24(格边 %dpx——下缘不贴边不裁切)" ↩ % [L, b.width, b.height, grid_h, int(avail.y), int(cell)] | grid_h <= int(avail.y) - 24 | (待填) | | | +| 585 | 67 | "L%d 格边在 26–46 档内(%dpx)" ↩ % [L, int(cell)] | cell >= 26.0 and cell <= 46.0 | (待填) | | | ## S75 _smoke_reward_identity_unit(行 83–116 · 10 条 · 源 game/tests/unit/core/board_gen_reward_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 592 | 83 | "roll_board 受理" | reward_economy.roll_board(eligible) | (待填) | | | -| 593 | 84 | "重复 roll_board 被拒(恰一次——double-roll 守卫)" | not reward_economy.roll_board(eligible) | (待填) | | | -| 594 | 95 | "query_identity 投影 = {reward_id, cell_type, tier} 且五型封闭(样本 %s)" ↩ % str(types_seen.keys()) | types_ok | (待填) | | | -| 595 | 97 | "清单外坐标查询 = null(封锁格无身份同口径)" | reward_economy.query_identity(Vector2i(99, 99)) == null | (待填) | | | -| 596 | 101 | "同格重复 grant 幂等(同一快照——受理恰一次)" | JSON.stringify(snap1) == JSON.stringify(snap2) | (待填) | | | -| 597 | 102 | "清单外 grant 拒绝" | reward_economy.grant(Vector2i(99, 99)) == null | (待填) | | | -| 598 | 106 | "清图批尾身份 = clear_blast(clear_default 唯一活行)" | String(cs1.cell_type) == "blast" and String(cs1.reward_id) == "clear_blast" | (待填) | | | -| 599 | 111 | "清图奖励 power ∈ 表区间 [%d, %d](roll 值 %d——配置推导)" ↩ % [cb_range[0], cb_range[1], int(cs1.power)] | int(cs1.power) >= int(cb_range[0]) and int(cs1.power) <= int(cb_range[1]) | (待填) | | | -| 600 | 113 | "grant_clear 同批次重调幂等" | JSON.stringify(cs1) == JSON.stringify(cs2) | (待填) | | | -| 601 | 116 | "映射退役后 grant 拒绝(生命周期闭合)" | reward_economy.grant(eligible[0].cell) == null | (待填) | | | +| 586 | 83 | "roll_board 受理" | reward_economy.roll_board(eligible) | (待填) | | | +| 587 | 84 | "重复 roll_board 被拒(恰一次——double-roll 守卫)" | not reward_economy.roll_board(eligible) | (待填) | | | +| 588 | 95 | "query_identity 投影 = {reward_id, cell_type, tier} 且五型封闭(样本 %s)" ↩ % str(types_seen.keys()) | types_ok | (待填) | | | +| 589 | 97 | "清单外坐标查询 = null(封锁格无身份同口径)" | reward_economy.query_identity(Vector2i(99, 99)) == null | (待填) | | | +| 590 | 101 | "同格重复 grant 幂等(同一快照——受理恰一次)" | JSON.stringify(snap1) == JSON.stringify(snap2) | (待填) | | | +| 591 | 102 | "清单外 grant 拒绝" | reward_economy.grant(Vector2i(99, 99)) == null | (待填) | | | +| 592 | 106 | "清图批尾身份 = clear_blast(clear_default 唯一活行)" | String(cs1.cell_type) == "blast" and String(cs1.reward_id) == "clear_blast" | (待填) | | | +| 593 | 111 | "清图奖励 power ∈ 表区间 [%d, %d](roll 值 %d——配置推导)" ↩ % [cb_range[0], cb_range[1], int(cs1.power)] | int(cs1.power) >= int(cb_range[0]) and int(cs1.power) <= int(cb_range[1]) | (待填) | | | +| 594 | 113 | "grant_clear 同批次重调幂等" | JSON.stringify(cs1) == JSON.stringify(cs2) | (待填) | | | +| 595 | 116 | "映射退役后 grant 拒绝(生命周期闭合)" | reward_economy.grant(eligible[0].cell) == null | (待填) | | | -## S76 _smoke_board_generator(行 135–210 · 24 条 · 源 game/tests/unit/core/board_gen_reward_test.gd · 去向:待分类回填) +## S76 _smoke_board_generator(行 139–212 · 23 条 · 源 game/tests/unit/core/board_gen_reward_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 602 | 135 | "BoardGen 行解析 (%d,0)" ↩ % L | not row.is_empty() | (待填) | | | -| 603 | 141 | "BoardGen 精确命中 (2,3)(覆盖批键域补齐)" | int(exact_row.get("node_depth", -1)) == 3 | (待填) | | | -| 604 | 151 | "R1 ceil 能力不失(夹具摘除 (2,3)→向上取整到 4·偏难口径)" | int(ceil_row.get("node_depth", -1)) == 4 | (待填) | | | -| 605 | 153 | "夹具表原样还原(行数与解析回到覆盖批现值)" | _db.board_gen.rows.size() == rows_before and int(_db.resolve_board_gen(2, 3).get("node_depth", -1)) == 3 | (待填) | | | -| 606 | 156 | "L%d 生成产物非空" ↩ % L | b1 != null | (待填) | | | -| 607 | 159 | "L%d clearable(安全格 8 邻连通)" ↩ % L | board_generator.is_clearable(b1) | (待填) | | | -| 608 | 160 | "L%d legality 全过" ↩ % L | board_generator.legality(b1, row) | (待填) | | | -| 609 | 167 | "L%d inferability %.3f %s target %.2f(accept_mode=%s)" ↩ % [L, inf, "≥" if inf >= float(row.inferability_target) else "<", float(row.inferability_target), mode] | inf >= float(row.inferability_target) or mode == "accept_best" | (待填) | | | -| 610 | 169 | "L%d 生成器产物零变体格(变异布置归战斗初始化第 4 步——生成器管线不布置,2026-09-19 用户拍板棋盘设置顺序)" ↩ % L | b1.variants.is_empty() | (待填) | | | -| 611 | 170 | "L%d 指标集输出降级标记与目标雷数(三分出口表)" ↩ % L | b1.metrics.has("degraded_product") and b1.metrics.has("target_mines") | (待填) | | | -| 612 | 171 | "L%d 降级产物标记 = (accept_mode==accept_best)(%s→%s)" ↩ % [L, mode, str(b1.degraded_product)] | b1.degraded_product == (mode == "accept_best") | (待填) | | | -| 613 | 173 | "L%d 非法产物标记 = (accept_mode==accept_worst)(%s→%s)" ↩ % [L, mode, str(b1.illegal_product)] | b1.illegal_product == (mode == "accept_worst") | (待填) | | | -| 614 | 175 | "L%d 降级/非法产物标记互斥(两出口不可同真)" ↩ % L | not b1.degraded_product or not b1.illegal_product | (待填) | | | -| 615 | 177 | "L%d 产物断言:实际雷数 %d ≤ 目标雷数 N=%d(先布后删只减不增)" ↩ % [L, int(b1.metrics.actual_mines), int(b1.metrics.target_mines)] | int(b1.metrics.actual_mines) <= int(b1.metrics.target_mines) | (待填) | | | -| 616 | 179 | "L%d attempts_used ∈ [1,%d]" ↩ % [L, int(row.max_gen_attempts)] | b1.metrics.get("attempts_used", 99) >= 1 and b1.metrics.get("attempts_used", 99) <= int(row.max_gen_attempts) | (待填) | | | -| 617 | 181 | "L%d 非法产物标记 = false" ↩ % L | not b1.illegal_product | (待填) | | | -| 618 | 185 | "L%d 同种子两次生成逐格相同" ↩ % L | _board_dump(b2) == _board_dump(b3) | (待填) | | | -| 619 | 187 | "L%d 异种子产物不同(退化护栏)" ↩ % L | _board_dump(b2) != _board_dump(b4) | (待填) | | | -| 620 | 193 | "L%d r0_max_cap 正式行投值在位(%d ≥ 出生保障面 9——板面 30%% 锚)" ↩ % [L, cap_val] | cap_val >= BoardData.SPAWN_AREA_CELLS | (待填) | | | -| 621 | 195 | "L%d 结构断言 R₀=%d ≤ cap=%d(R₀≤cap 重掷闸承载)" ↩ % [L, int(b1.metrics.r0_count), cap_val] | int(b1.metrics.r0_count) <= cap_val | (待填) | | | -| 622 | 197 | "L%d r0_cap_rejected 尝试事实指标在位(本图 %d 次触发)" ↩ % [L, int(b1.metrics.get("r0_cap_rejected", -1))] | int(b1.metrics.get("r0_cap_rejected", -1)) >= 0 | (待填) | | | -| 623 | 199 | "L%d ring_removed=%d ≤ 剃雷域候选危险格数((3+2r)²−9——全域剃雷退役)" ↩ % [L, int(b1.metrics.get("ring_removed", 99))] | int(b1.metrics.get("ring_removed", 99)) <= (2 + int(row.get("shave_ring_radius", 1))) * (2 + int(row.get("shave_ring_radius", 1))) * 4 - 5 | (待填) | | | -| 624 | 207 | "L1 cap 压线 9:出图 R₀=%d %s(三分出口诚实降级——超阈即 accept_worst 非法标记,玩家永不卡死)" ↩ % [int(b9.metrics.r0_count), "≤9" if int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS else ">9→illegal_product"] | b9 != null and (int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS or b9.illegal_product) | (待填) | | | -| 625 | 209 | "L1 cap 压线出图指标集完整(accept_mode=%s / 闸触发 %d 次)" ↩ % [String(b9.metrics.get("accept_mode", "")), int(b9.metrics.get("r0_cap_rejected", 0))] | int(b9.metrics.get("r0_cap_rejected", 0)) >= 0 and String(b9.metrics.get("accept_mode", "")) != "" | (待填) | | | +| 596 | 139 | "BoardGen 行解析 (%d,0)" ↩ % L | not row.is_empty() | (待填) | | | +| 597 | 145 | "BoardGen 精确命中 (2,3)(覆盖批键域补齐)" | int(exact_row.get("node_depth", -1)) == 3 | (待填) | | | +| 598 | 155 | "R1 ceil 能力不失(夹具摘除 (2,3)→向上取整到 4·偏难口径)" | int(ceil_row.get("node_depth", -1)) == 4 | (待填) | | | +| 599 | 157 | "夹具表原样还原(行数与解析回到覆盖批现值)" | _db.combat.board_gen.rows.size() == rows_before and int(_db.combat.board_gen.resolve_row(2, 3).get("node_depth", -1)) == 3 | (待填) | | | +| 600 | 160 | "L%d 生成产物非空" ↩ % L | b1 != null | (待填) | | | +| 601 | 163 | "L%d clearable(安全格 8 邻连通)" ↩ % L | board_generator.is_clearable(b1) | (待填) | | | +| 602 | 164 | "L%d legality 全过" ↩ % L | board_generator.legality(b1, row) | (待填) | | | +| 603 | 171 | "L%d inferability %.3f %s target %.2f(accept_mode=%s)" ↩ % [L, inf, "≥" if inf >= float(row.inferability_target) else "<", float(row.inferability_target), mode] | inf >= float(row.inferability_target) or mode == "accept_best" | (待填) | | | +| 604 | 173 | "L%d 生成器产物零变体格(变异布置归战斗初始化第 4 步——生成器管线不布置,2026-09-19 用户拍板棋盘设置顺序)" ↩ % L | b1.variants.is_empty() | (待填) | | | +| 605 | 174 | "L%d 指标集输出降级标记与目标雷数(三分出口表)" ↩ % L | b1.metrics.has("degraded_product") and b1.metrics.has("target_mines") | (待填) | | | +| 606 | 175 | "L%d 降级产物标记 = (accept_mode==accept_best)(%s→%s)" ↩ % [L, mode, str(b1.degraded_product)] | b1.degraded_product == (mode == "accept_best") | (待填) | | | +| 607 | 177 | "L%d 非法产物标记 = (accept_mode==accept_worst)(%s→%s)" ↩ % [L, mode, str(b1.illegal_product)] | b1.illegal_product == (mode == "accept_worst") | (待填) | | | +| 608 | 179 | "L%d 产物断言:实际雷数 %d ≤ 目标雷数 N=%d(先布后删只减不增)" ↩ % [L, int(b1.metrics.actual_mines), int(b1.metrics.target_mines)] | int(b1.metrics.actual_mines) <= int(b1.metrics.target_mines) | (待填) | | | +| 609 | 181 | "L%d attempts_used ∈ [1,%d]" ↩ % [L, int(row.max_gen_attempts)] | b1.metrics.get("attempts_used", 99) >= 1 and b1.metrics.get("attempts_used", 99) <= int(row.max_gen_attempts) | (待填) | | | +| 610 | 183 | "L%d 非法产物标记 = false" ↩ % L | not b1.illegal_product | (待填) | | | +| 611 | 187 | "L%d 同种子两次生成逐格相同" ↩ % L | _board_dump(b2) == _board_dump(b3) | (待填) | | | +| 612 | 189 | "L%d 异种子产物不同(退化护栏)" ↩ % L | _board_dump(b2) != _board_dump(b4) | (待填) | | | +| 613 | 195 | "L%d r0_max_cap 正式行投值在位(%d ≥ 出生保障面 9——板面 30%% 锚)" ↩ % [L, cap_val] | cap_val >= BoardData.SPAWN_AREA_CELLS | (待填) | | | +| 614 | 197 | "L%d 结构断言 R₀=%d ≤ cap=%d(R₀≤cap 重掷闸承载)" ↩ % [L, int(b1.metrics.r0_count), cap_val] | int(b1.metrics.r0_count) <= cap_val | (待填) | | | +| 615 | 199 | "L%d r0_cap_rejected 尝试事实指标在位(本图 %d 次触发)" ↩ % [L, int(b1.metrics.get("r0_cap_rejected", -1))] | int(b1.metrics.get("r0_cap_rejected", -1)) >= 0 | (待填) | | | +| 616 | 201 | "L%d ring_removed=%d ≤ 剃雷域候选危险格数((3+2r)²−9——全域剃雷退役)" ↩ % [L, int(b1.metrics.get("ring_removed", 99))] | int(b1.metrics.get("ring_removed", 99)) <= (2 + int(row.get("shave_ring_radius", 1))) * (2 + int(row.get("shave_ring_radius", 1))) * 4 - 5 | (待填) | | | +| 617 | 209 | "L1 cap 压线 9:出图 R₀=%d %s(三分出口诚实降级——超阈即 accept_worst 非法标记,玩家永不卡死)" ↩ % [int(b9.metrics.r0_count), "≤9" if int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS else ">9→illegal_product"] | b9 != null and (int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS or b9.illegal_product) | (待填) | | | +| 618 | 211 | "L1 cap 压线出图指标集完整(accept_mode=%s / 闸触发 %d 次)" ↩ % [String(b9.metrics.get("accept_mode", "")), int(b9.metrics.get("r0_cap_rejected", 0))] | int(b9.metrics.get("r0_cap_rejected", 0)) >= 0 and String(b9.metrics.get("accept_mode", "")) != "" | (待填) | | | -## S77 _smoke_power_interval(行 240–298 · 5 条 · 源 game/tests/unit/core/board_gen_reward_test.gd · 去向:待分类回填) +## S77 _smoke_power_interval(行 242–300 · 5 条 · 源 game/tests/unit/core/board_gen_reward_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 626 | 240 | "区间行配对齐全且 power = floor 中点(配置推导 %d 行:%s)" ↩ % [interval_rows.size(), str(interval_rows.keys())] | anchors_ok and interval_rows.size() >= 1 | (待填) | | | -| 627 | 282 | "24 板全区间行快照 power ∈ [power_min, power_max](含清图槽)" | in_domain | (待填) | | | -| 628 | 283 | "定值行快照 power 恒等表值——区间批零外溢" | fixed_ok | (待填) | | | -| 629 | 297 | "区间行上下阶实际流动(≥2 样本的区间行须 ≥2 个不同 roll 值)" | variance_ok | (待填) | | | -| 630 | 298 | "定值行(lo=hi)roll 恒等表值" | fixed_rows_ok | (待填) | | | +| 619 | 242 | "区间行配对齐全且 power = floor 中点(配置推导 %d 行:%s)" ↩ % [interval_rows.size(), str(interval_rows.keys())] | anchors_ok and interval_rows.size() >= 1 | (待填) | | | +| 620 | 284 | "24 板全区间行快照 power ∈ [power_min, power_max](含清图槽)" | in_domain | (待填) | | | +| 621 | 285 | "定值行快照 power 恒等表值——区间批零外溢" | fixed_ok | (待填) | | | +| 622 | 299 | "区间行上下阶实际流动(≥2 样本的区间行须 ≥2 个不同 roll 值)" | variance_ok | (待填) | | | +| 623 | 300 | "定值行(lo=hi)roll 恒等表值" | fixed_rows_ok | (待填) | | | ## S78 _init(行 17–17 · 1 条 · 源 game/tests/unit/core/board_generator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 631 | 17 | "数值表加载" | false | (待填) | | | +| 624 | 17 | "数值表加载" | false | (待填) | | | ## S79 _test_generate_shape_and_mines(行 42–55 · 5 条 · 源 game/tests/unit/core/board_generator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 632 | 42 | "生成返回棋盘" | false | (待填) | | | -| 633 | 47 | "棋盘尺寸与 board_gen 表一致(%dx%d)" ↩ % [want_w, want_h] | b.width == want_w and b.height == want_h | (待填) | | | -| 634 | 49 | "雷数非零且不超过总格数" | b.mines.size() > 0 and b.mines.size() < b.width * b.height | (待填) | | | -| 635 | 54 | "全部危险格在界内" | all_in_bounds | (待填) | | | -| 636 | 55 | "生成期指标集已落盘(metrics 非空)" | not b.metrics.is_empty() | (待填) | | | +| 625 | 42 | "生成返回棋盘" | false | (待填) | | | +| 626 | 47 | "棋盘尺寸与 board_gen 表一致(%dx%d)" ↩ % [want_w, want_h] | b.width == want_w and b.height == want_h | (待填) | | | +| 627 | 49 | "雷数非零且不超过总格数" | b.mines.size() > 0 and b.mines.size() < b.width * b.height | (待填) | | | +| 628 | 54 | "全部危险格在界内" | all_in_bounds | (待填) | | | +| 629 | 55 | "生成期指标集已落盘(metrics 非空)" | not b.metrics.is_empty() | (待填) | | | ## S80 _test_target_mine_count_single_source(行 75–83 · 3 条 · 源 game/tests/unit/core/board_generator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 637 | 75 | "target_mine_count 与原式同参同值(%d 档对拍全等)〔wd14〕" ↩ % checked | checked > 0 and bad == 0 | (待填) | | | -| 638 | 78 | "生成返回棋盘(雷数单点段)" | false | (待填) | | | -| 639 | 81 | "产物断言读数 = target_mine_count 同源(metrics.target_mines)〔wd14〕" | int(b.metrics.get("target_mines", -1)) == BoardGeneratorScript.target_mine_count({ "w": int(row1.board_width), "h": int(row1.board_height), "density": float(row1.mine_density)}) | (待填) | | | +| 630 | 75 | "target_mine_count 与原式同参同值(%d 档对拍全等)〔wd14〕" ↩ % checked | checked > 0 and bad == 0 | (待填) | | | +| 631 | 78 | "生成返回棋盘(雷数单点段)" | false | (待填) | | | +| 632 | 81 | "产物断言读数 = target_mine_count 同源(metrics.target_mines)〔wd14〕" | int(b.metrics.get("target_mines", -1)) == BoardGeneratorScript.target_mine_count({ "w": int(row1.board_width), "h": int(row1.board_height), "density": float(row1.mine_density)}) | (待填) | | | ## S81 _test_digits_equal_neighbor_mine_count(行 89–103 · 2 条 · 源 game/tests/unit/core/board_generator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 640 | 89 | "生成返回棋盘(数字段)" | false | (待填) | | | -| 641 | 103 | "每个安全格的数字恒等于八邻雷数(全盘逐格复算,错 %d 格)" ↩ % bad | bad == 0 | (待填) | | | +| 633 | 89 | "生成返回棋盘(数字段)" | false | (待填) | | | +| 634 | 103 | "每个安全格的数字恒等于八邻雷数(全盘逐格复算,错 %d 格)" ↩ % bad | bad == 0 | (待填) | | | ## S82 _test_same_seed_bit_identical(行 110–113 · 3 条 · 源 game/tests/unit/core/board_generator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 642 | 110 | "生成返回棋盘(确定性段)" | false | (待填) | | | -| 643 | 112 | "同种子生成的雷集逐位一致" | str(a.mines) == str(b.mines) | (待填) | | | -| 644 | 113 | "同种子生成的数字集逐位一致" | str(a.digits) == str(b.digits) | (待填) | | | +| 635 | 110 | "生成返回棋盘(确定性段)" | false | (待填) | | | +| 636 | 112 | "同种子生成的雷集逐位一致" | str(a.mines) == str(b.mines) | (待填) | | | +| 637 | 113 | "同种子生成的数字集逐位一致" | str(a.digits) == str(b.digits) | (待填) | | | ## S83 _test_clearable_and_inferability_selfconsistent(行 119–126 · 3 条 · 源 game/tests/unit/core/board_generator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 645 | 119 | "生成返回棋盘(可推理段)" | false | (待填) | | | -| 646 | 122 | "可推理性度量返回非空指标集" | not m.is_empty() | (待填) | | | -| 647 | 125 | "清图判定与安全格总数同在(安全格 %d 个,可清=%s)" ↩ % [safe_total, str(clearable)] | safe_total > 0 | (待填) | | | +| 638 | 119 | "生成返回棋盘(可推理段)" | false | (待填) | | | +| 639 | 122 | "可推理性度量返回非空指标集" | not m.is_empty() | (待填) | | | +| 640 | 125 | "清图判定与安全格总数同在(安全格 %d 个,可清=%s)" ↩ % [safe_total, str(clearable)] | safe_total > 0 | (待填) | | | ## S84 _test_all_links_valid(行 59–59 · 1 条 · 源 game/tests/unit/core/config_db_cross_validator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 648 | 59 | "全对齿的技能池零错(正向面——实际报错 %s)" ↩ % str(_cats(errs)) | errs.is_empty() | (待填) | | | +| 641 | 59 | "全对齿的技能池零错(正向面——实际报错 %s)" ↩ % str(_cats(errs)) | errs.is_empty() | (待填) | | | -## S85 _test_dangling_skill_id(行 70–72 · 2 条 · 源 game/tests/unit/core/config_db_cross_validator_test.gd · 去向:待分类回填) +## S85 _test_dangling_skill_id(行 70–71 · 1 条 · 源 game/tests/unit/core/config_db_cross_validator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 649 | 70 | "池内悬空 skill_id 被报(跨表护栏不静默放过)" | not errs.is_empty() | (待填) | | | -| 650 | 71 | "悬空项的错误类别可定位(category 非空)" | not errs.is_empty() and String((errs[0] as Dictionary).get("category", "")) != "" | (待填) | | | +| 642 | 70 | "悬空项的错误类别可定位(category 非空)" | not errs.is_empty() and String((errs[0] as Dictionary).get("category", "")) != "" | (待填) | | | -## S86 _test_cast_side_mismatch(行 84–84 · 1 条 · 源 game/tests/unit/core/config_db_cross_validator_test.gd · 去向:待分类回填) +## S86 _test_cast_side_mismatch(行 83–83 · 1 条 · 源 game/tests/unit/core/config_db_cross_validator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 651 | 84 | "buff 挂主角侧状态被报(施放类 side 守卫)" | _cats(errs).has("cast_side") | (待填) | | | +| 643 | 83 | "buff 挂主角侧状态被报(施放类 side 守卫)" | _cats(errs).has("cast_side") | (待填) | | | ## S87 _test_band_hit(行 58–62 · 2 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 652 | 58 | "带命中取偏难一侧:请求深度 4 命中带 5(逐字不回带 0)〔fv1〕" | hit.size() == 1 and String((hit[0] as Dictionary).get("enemy_id", "")) == "hobgoblin" | (待填) | | | -| 653 | 61 | "精确落带:请求深度 5 命中带 5〔fv1〕" | exact.size() == 1 and String((exact[0] as Dictionary).get("enemy_id", "")) == "hobgoblin" | (待填) | | | +| 644 | 58 | "带命中取偏难一侧:请求深度 4 命中带 5(逐字不回带 0)〔fv1〕" | hit.size() == 1 and String((hit[0] as Dictionary).get("enemy_id", "")) == "hobgoblin" | (待填) | | | +| 645 | 61 | "精确落带:请求深度 5 命中带 5〔fv1〕" | exact.size() == 1 and String((exact[0] as Dictionary).get("enemy_id", "")) == "hobgoblin" | (待填) | | | -## S88 _test_rowset_order_preserved(行 67–72 · 2 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) +## S88 _test_rowset_order_preserved(行 70–71 · 1 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 654 | 67 | "行集返回同带全部行(2 行)〔fv2〕" | rows.size() == 2 | (待填) | | | -| 655 | 71 | "行集按表文件行序返回(goblin 先于 bugbear——行序即契约)〔fv2〕" | ids == ["goblin", "bugbear"] | (待填) | | | +| 646 | 70 | "行集按表文件行序返回(goblin 先于 bugbear——行序即契约)〔fv2〕" | ids == ["goblin", "bugbear"] | (待填) | | | -## S89 _test_top_band_clamp(行 78–87 · 3 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) +## S89 _test_top_band_clamp(行 77–86 · 3 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 656 | 78 | "越顶带 clamp 取顶带行集(battle 顶带 5)〔fv3〕" | rows.size() == 1 and String((rows[0] as Dictionary).get("enemy_id", "")) == "hobgoblin" | (待填) | | | -| 657 | 84 | "越顶带发 warning resolve_depth_clamp〔fv3〕" | clamp_hit | (待填) | | | -| 658 | 87 | "同键重复请求警告去重(四元组注册表)〔fv3〕" | _diags.size() == warn_before | (待填) | | | +| 647 | 77 | "越顶带 clamp 取顶带行集(battle 顶带 5)〔fv3〕" | rows.size() == 1 and String((rows[0] as Dictionary).get("enemy_id", "")) == "hobgoblin" | (待填) | | | +| 648 | 83 | "越顶带发 warning resolve_depth_clamp〔fv3〕" | clamp_hit | (待填) | | | +| 649 | 86 | "同键重复请求警告去重(四元组注册表)〔fv3〕" | _diags.size() == warn_before | (待填) | | | -## S90 _test_row_kind_routing(行 93–95 · 1 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) +## S90 _test_row_kind_routing(行 92–94 · 1 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 659 | 93 | "行类路由:同请求深度 battle 与 boss 命中不同键域〔fv4〕" | battle_rows.size() == 2 and boss_rows.size() == 1 and String((boss_rows[0] as Dictionary).get("enemy_id", "")) == "ogre_chief" | (待填) | | | +| 650 | 92 | "行类路由:同请求深度 battle 与 boss 命中不同键域〔fv4〕" | battle_rows.size() == 2 and boss_rows.size() == 1 and String((boss_rows[0] as Dictionary).get("enemy_id", "")) == "ogre_chief" | (待填) | | | -## S91 _test_request_oob(行 101–107 · 3 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) +## S91 _test_request_oob(行 100–106 · 3 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 660 | 101 | "关卡序号 <1 → fatal request_oob〔fv5〕" | _has_fatal_oob() | (待填) | | | -| 661 | 104 | "节点深度 <0 → fatal request_oob〔fv5〕" | _has_fatal_oob() | (待填) | | | -| 662 | 107 | "row_kind 非法 → fatal request_oob〔fv5〕" | _has_fatal_oob() | (待填) | | | +| 651 | 100 | "关卡序号 <1 → fatal request_oob〔fv5〕" | _has_fatal_oob() | (待填) | | | +| 652 | 103 | "节点深度 <0 → fatal request_oob〔fv5〕" | _has_fatal_oob() | (待填) | | | +| 653 | 106 | "row_kind 非法 → fatal request_oob〔fv5〕" | _has_fatal_oob() | (待填) | | | -## S92 _test_difficulty_clamp(行 119–122 · 3 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) +## S92 _test_difficulty_clamp(行 118–121 · 3 条 · 源 game/tests/unit/core/config_db_resolver_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 663 | 119 | "难度乘数越域被 clamp:hp 99 → 上限 10" | absf(float(m.get("hp_mult", 0.0)) - 10.0) < 0.001 | (待填) | | | -| 664 | 120 | "难度乘数越域被 clamp:atk 0.01 → 下限 0.1" | absf(float(m.get("atk_mult", 0.0)) - 0.1) < 0.001 | (待填) | | | -| 665 | 121 | "未登记坐标返回空修饰(零值不伪装成 ×1)" | _resolver.resolve_difficulty_modifiers(3, 3).is_empty() | (待填) | | | +| 654 | 118 | "难度乘数越域被 clamp:hp 99 → 上限 10" | absf(float(m.get("hp_mult", 0.0)) - 10.0) < 0.001 | (待填) | | | +| 655 | 119 | "难度乘数越域被 clamp:atk 0.01 → 下限 0.1" | absf(float(m.get("atk_mult", 0.0)) - 0.1) < 0.001 | (待填) | | | +| 656 | 120 | "未登记坐标返回空修饰(零值不伪装成 ×1)" | _resolver.run.difficulty.resolve_modifiers(3, 3).is_empty() | (待填) | | | ## S93 _test_field_in_table(行 106–114 · 4 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 666 | 106 | "N 落表:level.tres 读数 2(实际 %d)〔fv6〕" ↩ % n_in_table | n_in_table == 2 | (待填) | | | -| 667 | 110 | "N=1 合法:validate 零错〔fv6〕" | t.validate().is_empty() | (待填) | | | -| 668 | 112 | "N 越域 0:validate 判 domain 错〔fv6〕" | _has_n_domain_err(t.validate()) | (待填) | | | -| 669 | 114 | "N 越域 -1:validate 判 domain 错〔fv6〕" | _has_n_domain_err(t.validate()) | (待填) | | | +| 657 | 106 | "N 落表:level.tres 读数 2(实际 %d)〔fv6〕" ↩ % n_in_table | n_in_table == 2 | (待填) | | | +| 658 | 110 | "N=1 合法:validate 零错〔fv6〕" | t.validate().is_empty() | (待填) | | | +| 659 | 112 | "N 越域 0:validate 判 domain 错〔fv6〕" | _has_n_domain_err(t.validate()) | (待填) | | | +| 660 | 114 | "N 越域 -1:validate 判 domain 错〔fv6〕" | _has_n_domain_err(t.validate()) | (待填) | | | ## S94 _test_layer_distance(行 131–133 · 3 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 670 | 131 | "层距 N 内可见:锚层 1 取节点 4(层距 2)〔fv1〕" | vis.has(4) | (待填) | | | -| 671 | 132 | "层距 N+1 不可见:锚层 1 排节点 5(层距 3)〔fv1〕" | not vis.has(5) | (待填) | | | -| 672 | 133 | "层距 N+2 不可见:锚层 1 排节点 6(层距 4)〔fv1〕" | not vis.has(6) | (待填) | | | +| 661 | 131 | "层距 N 内可见:锚层 1 取节点 4(层距 2)〔fv1〕" | vis.has(4) | (待填) | | | +| 662 | 132 | "层距 N+1 不可见:锚层 1 排节点 5(层距 3)〔fv1〕" | not vis.has(5) | (待填) | | | +| 663 | 133 | "层距 N+2 不可见:锚层 1 排节点 6(层距 4)〔fv1〕" | not vis.has(6) | (待填) | | | ## S95 _test_candidate_exempt(行 142–143 · 2 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 673 | 142 | "候选恒显:跨层跳边候选(层距 N+1)仍在可见集〔fv4〕" | vis.has(3) | (待填) | | | -| 674 | 143 | "候选豁免不改层规则:同锚点非候选节点 2(层距 4)不可见〔fv4〕" | not vis.has(2) | (待填) | | | +| 664 | 142 | "候选恒显:跨层跳边候选(层距 N+1)仍在可见集〔fv4〕" | vis.has(3) | (待填) | | | +| 665 | 143 | "候选豁免不改层规则:同锚点非候选节点 2(层距 4)不可见〔fv4〕" | not vis.has(2) | (待填) | | | ## S96 _test_boss_exempt(行 152–155 · 4 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 675 | 152 | "入口停点锚层 0:层距 N 内(节点 2)可见〔fv1〕" | vis.has(2) | (待填) | | | -| 676 | 153 | "入口停点锚层 0:层距 N+2(节点 4)不可见〔fv1〕" | not vis.has(4) | (待填) | | | -| 677 | 154 | "Boss 恒显:层距 6 的末位 Boss 仍在可见集〔fv3〕" | vis.has(7) | (待填) | | | -| 678 | 155 | "Boss 豁免不扩散:同层距的非 Boss 节点 6 不在可见集〔fv3〕" | not vis.has(6) | (待填) | | | +| 666 | 152 | "入口停点锚层 0:层距 N 内(节点 2)可见〔fv1〕" | vis.has(2) | (待填) | | | +| 667 | 153 | "入口停点锚层 0:层距 N+2(节点 4)不可见〔fv1〕" | not vis.has(4) | (待填) | | | +| 668 | 154 | "Boss 恒显:层距 6 的末位 Boss 仍在可见集〔fv3〕" | vis.has(7) | (待填) | | | +| 669 | 155 | "Boss 豁免不扩散:同层距的非 Boss 节点 6 不在可见集〔fv3〕" | not vis.has(6) | (待填) | | | ## S97 _test_single_circle(行 165–167 · 3 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 679 | 165 | "单圈回雾:已过远端节点 0(层距 5)不因 visited 豁免〔fv7〕" | not vis.has(0) | (待填) | | | -| 680 | 166 | "单圈回雾:已过中距节点 2(层距 4)不可见〔fv7〕" | not vis.has(2) | (待填) | | | -| 681 | 167 | "单圈只按锚层取窗:已过近端节点 4(层距 2)仍可见〔fv7〕" | vis.has(4) | (待填) | | | +| 670 | 165 | "单圈回雾:已过远端节点 0(层距 5)不因 visited 豁免〔fv7〕" | not vis.has(0) | (待填) | | | +| 671 | 166 | "单圈回雾:已过中距节点 2(层距 4)不可见〔fv7〕" | not vis.has(2) | (待填) | | | +| 672 | 167 | "单圈只按锚层取窗:已过近端节点 4(层距 2)仍可见〔fv7〕" | vis.has(4) | (待填) | | | ## S98 _test_single_chain_entry(行 176–179 · 3 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 682 | 176 | "单链退化形态:入口锚层 0,句柄 2 可见〔fv1〕" | vis.has(2) | (待填) | | | -| 683 | 177 | "单链退化形态:层距 N+1(句柄 3)不可见〔fv1〕" | not vis.has(3) | (待填) | | | -| 684 | 178 | "单链编号空间 = 图句柄 id:Boss 以句柄 6 入集(链上层距 4 的战斗节点 4 入雾)〔fv1〕" | vis.has(6) and not vis.has(4) | (待填) | | | +| 673 | 176 | "单链退化形态:入口锚层 0,句柄 2 可见〔fv1〕" | vis.has(2) | (待填) | | | +| 674 | 177 | "单链退化形态:层距 N+1(句柄 3)不可见〔fv1〕" | not vis.has(3) | (待填) | | | +| 675 | 178 | "单链编号空间 = 图句柄 id:Boss 以句柄 6 入集(链上层距 4 的战斗节点 4 入雾)〔fv1〕" | vis.has(6) and not vis.has(4) | (待填) | | | ## S99 _test_n_from_table(行 188–196 · 3 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 685 | 188 | "N 读表:N=1 时节点 4(层距 2)出窗〔fv6〕" | not r.type_visible_ids().has(4) | (待填) | | | -| 686 | 190 | "N 读表:N=3 时节点 5(层距 3)入窗〔fv6〕" | r.type_visible_ids().has(5) | (待填) | | | -| 687 | 196 | "N 读表:N ≥ 全图层高 → 全部非豁免节点可见〔fv6〕" | all_in | (待填) | | | +| 676 | 188 | "N 读表:N=1 时节点 4(层距 2)出窗〔fv6〕" | not r.type_visible_ids().has(4) | (待填) | | | +| 677 | 190 | "N 读表:N=3 时节点 5(层距 3)入窗〔fv6〕" | r.type_visible_ids().has(5) | (待填) | | | +| 678 | 196 | "N 读表:N ≥ 全图层高 → 全部非豁免节点可见〔fv6〕" | all_in | (待填) | | | ## S100 _test_resume_rebuild(行 234–235 · 1 条 · 源 game/tests/unit/core/level_map_type_vis_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 688 | 234 | "续玩重建同值:resume 路径与同停点直入 type_visible_ids() 逐位一致〔fv7〕" | resumed.type_visible_ids() == direct.type_visible_ids() | (待填) | | | +| 679 | 234 | "续玩重建同值:resume 路径与同停点直入 type_visible_ids() 逐位一致〔fv7〕" | resumed.type_visible_ids() == direct.type_visible_ids() | (待填) | | | ## S101 _smoke_table_migration(行 262–269 · 4 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 689 | 262 | "shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕" | _db.shop.relic_base_price == 15.0 | (待填) | | | -| 690 | 263 | "shop.spend_discount 表值 == 迁移前字面量(0.8)〔wd11〕" | _db.shop.spend_discount == 0.8 | (待填) | | | -| 691 | 264 | "hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕" | _db.hero.display_name == "探险者" | (待填) | | | -| 692 | 268 | "spawn 后英雄单位 display_name 读表同值(探险者)〔wd16〕" | spawned and String(units.hero_unit().get("display_name", "")) == String(_db.hero.display_name) | (待填) | | | +| 680 | 262 | "shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕" | _db.run.shop.relic_base_price == 15.0 | (待填) | | | +| 681 | 263 | "shop.spend_discount 表值 == 迁移前字面量(0.8)〔wd11〕" | _db.run.shop.spend_discount == 0.8 | (待填) | | | +| 682 | 264 | "hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕" | _db.combat.hero.display_name == "探险者" | (待填) | | | +| 683 | 268 | "spawn 后英雄单位 display_name 读表同值(探险者)〔wd16〕" | spawned and String(units.hero_unit().get("display_name", "")) == String(_db.combat.hero.display_name) | (待填) | | | ## S102 _smoke_generated_coverage(行 283–320 · 6 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 693 | 283 | "L%d 参数档缺行(覆盖断言无源可扫)〔v11〕" ↩ % L | false | (待填) | | | -| 694 | 290 | "L%d 种子 %d 生成图为空(覆盖扫描面空转)〔v11〕" ↩ % [L, seed_val] | false | (待填) | | | -| 695 | 317 | "生成图覆盖扫描面非空(三关参数档×种子 %d–%d 共 %d 个节点读数)〔v11〕" ↩ % [SWEEP_SEED_FROM, SWEEP_SEED_TO - 1, scanned] | scanned > 0 | (待填) | | | -| 696 | 318 | "生成图每个 battle/boss 节点深度的敌侧解析落带 ≥ 请求深度(band_of 非 -1·顶 clamp 消解禁)〔v11〕" | enemy_ok | (待填) | | | -| 697 | 319 | "生成图每个节点深度在 board_gen 精确命中(非空且 node_depth 等值)〔v11〕" | board_ok | (待填) | | | -| 698 | 320 | "多终点 Boss 各终点解析带键两两互异(L2/L3×20 种子)〔v11〕" | boss_band_ok | (待填) | | | +| 684 | 283 | "L%d 参数档缺行(覆盖断言无源可扫)〔v11〕" ↩ % L | false | (待填) | | | +| 685 | 290 | "L%d 种子 %d 生成图为空(覆盖扫描面空转)〔v11〕" ↩ % [L, seed_val] | false | (待填) | | | +| 686 | 317 | "生成图覆盖扫描面非空(三关参数档×种子 %d–%d 共 %d 个节点读数)〔v11〕" ↩ % [SWEEP_SEED_FROM, SWEEP_SEED_TO - 1, scanned] | scanned > 0 | (待填) | | | +| 687 | 318 | "生成图每个 battle/boss 节点深度的敌侧解析落带 ≥ 请求深度(band_of 非 -1·顶 clamp 消解禁)〔v11〕" | enemy_ok | (待填) | | | +| 688 | 319 | "生成图每个节点深度在 board_gen 精确命中(非空且 node_depth 等值)〔v11〕" | board_ok | (待填) | | | +| 689 | 320 | "多终点 Boss 各终点解析带键两两互异(L2/L3×20 种子)〔v11〕" | boss_band_ok | (待填) | | | ## S103 _smoke_level(行 331–387 · 17 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 699 | 331 | "L1 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" ↩ % [ACTIVE_GRAPH_SEED, nodes1.size()] | not nodes1.is_empty() | (待填) | | | -| 700 | 334 | "关卡·满配(atk+200/hp+100)完成(实测:%s @节点 %d)" ↩ % [win.result, win.defeat_node_index] | win.result == "victory" | (待填) | | | -| 701 | 342 | "L1 关卡完成路径逐跳为图出边(入口起步·末节点无出边——句柄轴行进自洽)" | _path_in_graph(nodes1, win.visited_ids) | (待填) | | | -| 702 | 343 | "L1 关卡完成路径战斗/Boss 节点数 = 行进 battles 读数(路径 %d 节点·战斗 %d 颗·battles %d)" ↩ % [win.visited_ids.size(), path_battles, int(win.battles)] | path_battles == int(win.battles) and path_battles >= 2 | (待填) | | | -| 703 | 353 | "L1 终点 Boss 深度 = 参数档层高 %d 且关卡完成路径末节点落该深度(实测终点深度 %d·末句柄 %d)" ↩ % [int(params1.get("layers", -1)), boss_depth1, last_id] | boss_depth1 == int(params1.get("layers", -1)) and int(by_id1.get(last_id, {}).get("depth", -1)) == boss_depth1 | (待填) | | | -| 704 | 355 | "L1 终点恰一个(大岔口自 L2 起的图侧投影)" | _terminal_ids(nodes1).size() == 1 and _ids_of_type(nodes1, "boss").size() == 1 | (待填) | | | -| 705 | 356 | "L1 生成图含岔口(出度 ≥2 节点 %d 处)" ↩ % _fork_ids(nodes1).size() | _fork_ids(nodes1).size() >= 1 | (待填) | | | -| 706 | 357 | "进 Boss HP 携带在域内(%d)" ↩ % win.hp_at_boss | win.hp_at_boss >= 1 and win.hp_at_boss <= int(_db.hero.max_hp) * 3 | (待填) | | | -| 707 | 363 | "恢复点公式无源(现行图入口后一位无可直达的恢复候选)" | false | (待填) | | | -| 708 | 366 | "恢复点公式·缺口充分档:回复 = floor(生效max_hp×%d%%) = %d(生效 max %d,实测 %d)" ↩ % [pct1, expect_pct, int(rest_wide["max"]), int(rest_wide["healed"])] | int(rest_wide["healed"]) == expect_pct and expect_pct > 0 | (待填) | | | -| 709 | 370 | "恢复点公式缺口封顶档无源(同图二次驱动未达恢复候选)" | false | (待填) | | | -| 710 | 372 | "恢复点公式·缺口不足档:回复封顶到缺口 2(实测 %d,比例项 %d)" ↩ % [int(rest_narrow["healed"]), expect_pct] | int(rest_narrow["healed"]) == mini(expect_pct, 2) and expect_pct > 2 | (待填) | | | -| 711 | 376 | "关卡·裸装(atk≈1/hp≈10)败北路径可达(实测:%s)" ↩ % lose.result | lose.result == "defeat" | (待填) | | | -| 712 | 377 | "败北节点记录在域内(节点 %d,图节点数 %d)" ↩ % [lose.defeat_node_index, nodes1.size()] | lose.defeat_node_index >= 0 and lose.defeat_node_index < nodes1.size() | (待填) | | | -| 713 | 382 | "关卡回放断言:同种子两遍结算步骤流逐行相同" | JSON.stringify(rep1.rows) == JSON.stringify(rep2.rows) | (待填) | | | -| 714 | 383 | "关卡步骤流非空(%d 行)" ↩ % rep1.rows.size() | rep1.rows.size() > 0 | (待填) | | | -| 715 | 385 | "模拟后 hero 表恢复基线(atk=%d hp=%d ac=%d——对配置快照)" ↩ % [int(_db.hero.atk), int(_db.hero.max_hp), int(_db.hero.armor_class)] | int(_db.hero.atk) == int(hero_base.atk) and int(_db.hero.max_hp) == int(hero_base.hp) \\ and int(_db.hero.armor_class) == int(hero_base.ac) | (待填) | | | +| 690 | 331 | "L1 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" ↩ % [ACTIVE_GRAPH_SEED, nodes1.size()] | not nodes1.is_empty() | (待填) | | | +| 691 | 334 | "关卡·满配(atk+200/hp+100)完成(实测:%s @节点 %d)" ↩ % [win.result, win.defeat_node_index] | win.result == "victory" | (待填) | | | +| 692 | 342 | "L1 关卡完成路径逐跳为图出边(入口起步·末节点无出边——句柄轴行进自洽)" | _path_in_graph(nodes1, win.visited_ids) | (待填) | | | +| 693 | 343 | "L1 关卡完成路径战斗/Boss 节点数 = 行进 battles 读数(路径 %d 节点·战斗 %d 颗·battles %d)" ↩ % [win.visited_ids.size(), path_battles, int(win.battles)] | path_battles == int(win.battles) and path_battles >= 2 | (待填) | | | +| 694 | 353 | "L1 终点 Boss 深度 = 参数档层高 %d 且关卡完成路径末节点落该深度(实测终点深度 %d·末句柄 %d)" ↩ % [int(params1.get("layers", -1)), boss_depth1, last_id] | boss_depth1 == int(params1.get("layers", -1)) and int(by_id1.get(last_id, {}).get("depth", -1)) == boss_depth1 | (待填) | | | +| 695 | 355 | "L1 终点恰一个(大岔口自 L2 起的图侧投影)" | _terminal_ids(nodes1).size() == 1 and _ids_of_type(nodes1, "boss").size() == 1 | (待填) | | | +| 696 | 356 | "L1 生成图含岔口(出度 ≥2 节点 %d 处)" ↩ % _fork_ids(nodes1).size() | _fork_ids(nodes1).size() >= 1 | (待填) | | | +| 697 | 357 | "进 Boss HP 携带在域内(%d)" ↩ % win.hp_at_boss | win.hp_at_boss >= 1 and win.hp_at_boss <= int(_db.combat.hero.max_hp) * 3 | (待填) | | | +| 698 | 363 | "恢复点公式无源(现行图入口后一位无可直达的恢复候选)" | false | (待填) | | | +| 699 | 366 | "恢复点公式·缺口充分档:回复 = floor(生效max_hp×%d%%) = %d(生效 max %d,实测 %d)" ↩ % [pct1, expect_pct, int(rest_wide["max"]), int(rest_wide["healed"])] | int(rest_wide["healed"]) == expect_pct and expect_pct > 0 | (待填) | | | +| 700 | 370 | "恢复点公式缺口封顶档无源(同图二次驱动未达恢复候选)" | false | (待填) | | | +| 701 | 372 | "恢复点公式·缺口不足档:回复封顶到缺口 2(实测 %d,比例项 %d)" ↩ % [int(rest_narrow["healed"]), expect_pct] | int(rest_narrow["healed"]) == mini(expect_pct, 2) and expect_pct > 2 | (待填) | | | +| 702 | 376 | "关卡·裸装(atk≈1/hp≈10)败北路径可达(实测:%s)" ↩ % lose.result | lose.result == "defeat" | (待填) | | | +| 703 | 377 | "败北节点记录在域内(节点 %d,图节点数 %d)" ↩ % [lose.defeat_node_index, nodes1.size()] | lose.defeat_node_index >= 0 and lose.defeat_node_index < nodes1.size() | (待填) | | | +| 704 | 382 | "关卡回放断言:同种子两遍结算步骤流逐行相同" | JSON.stringify(rep1.rows) == JSON.stringify(rep2.rows) | (待填) | | | +| 705 | 383 | "关卡步骤流非空(%d 行)" ↩ % rep1.rows.size() | rep1.rows.size() > 0 | (待填) | | | +| 706 | 385 | "模拟后 hero 表恢复基线(atk=%d hp=%d ac=%d——对配置快照)" ↩ % [int(_db.combat.hero.atk), int(_db.combat.hero.max_hp), int(_db.combat.hero.armor_class)] | int(_db.combat.hero.atk) == int(hero_base.atk) and int(_db.combat.hero.max_hp) == int(hero_base.hp) \\ and int(_db.combat.hero.armor_class) == int(hero_base.ac) | (待填) | | | ## S104 _smoke_level_graph(行 442–761 · 70 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 716 | 442 | "LevelTable 行级校验零错误(图合法性移交 validate_graph)" | _db.level.validate().is_empty() | (待填) | | | -| 717 | 446 | "L2 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" ↩ % [ACTIVE_GRAPH_SEED, nodes2.size()] | not nodes2.is_empty() | (待填) | | | -| 718 | 449 | "L2 节点数 %d ≥ 层高 %d+1 且 = 句柄去重读数 %d(全员句柄唯一)" ↩ % [nodes2.size(), int(params2.get("layers", -1)), _nodes_by_id(nodes2).size()] | nodes2.size() >= int(params2.get("layers", -1)) + 1 and _nodes_by_id(nodes2).size() == nodes2.size() | (待填) | | | -| 719 | 451 | "L2 五型计数合计 = 节点数(battle %d + boss %d + reward %d + rest %d + shop %d = %d)" ↩ % [tcount["battle"], tcount["boss"], tcount["reward"], tcount["rest"], tcount["shop"], nodes2.size()] | int(tcount["battle"]) + int(tcount["boss"]) + int(tcount["reward"]) + int(tcount["rest"]) + int(tcount["shop"]) == nodes2.size() | (待填) | | | -| 720 | 453 | "L2 战斗节点数 %d ≥ 参数档战斗保底 %d 且 Boss 节点数 %d = 参数档出口数 %d" ↩ % [tcount["battle"], int(params2.get("battle_floor", -1)), tcount["boss"], int(params2.get("exit_count", -1))] | int(tcount["battle"]) >= int(params2.get("battle_floor", -1)) and int(tcount["boss"]) == int(params2.get("exit_count", -1)) | (待填) | | | -| 721 | 455 | "L2 恢复/奖励/商店节点各 ≥1(实测 rest %d · reward %d · shop %d——计数由逐节点扫描同一读数)" ↩ % [tcount["rest"], tcount["reward"], tcount["shop"]] | int(tcount["rest"]) >= 1 and int(tcount["reward"]) >= 1 and int(tcount["shop"]) >= 1 | (待填) | | | -| 722 | 457 | "L2 岔口(出度 ≥2 的节点)= %d 处(≥1)" ↩ % branch | branch >= 1 | (待填) | | | -| 723 | 467 | "图校验正例:两岔合流小图零错误〔v1〕" | LT.validate_graph(good).is_empty() | (待填) | | | -| 724 | 470 | "图校验拒绝:句柄重复(图内唯一句柄)〔v1〕" | _has_err_category(LT.validate_graph(dup), "dup_key") | (待填) | | | -| 725 | 473 | "图校验正例:多入口合法(入口唯一判据随模板池退役)〔v1〕" | LT.validate_graph(dual).is_empty() | (待填) | | | -| 726 | 476 | "图校验拒绝:Boss 带出边(终点不可外出)〔v1〕" | _has_err_category(LT.validate_graph(boss_out), "structural") | (待填) | | | -| 727 | 479 | "图校验拒绝:回边 1→0(目标深度须严格大于源)〔v1〕" | _has_err_category(LT.validate_graph(cyc), "structural") | (待填) | | | -| 728 | 482 | "图校验拒绝:出边指向不存在句柄(悬空致命)〔v1〕" | _has_err_category(LT.validate_graph(dang), "dangling_next") | (待填) | | | -| 729 | 488 | "图校验拒绝:非 Boss 终点(终点恒为 Boss)〔v1〕" | _has_err_category(LT.validate_graph(orphan), "structural") | (待填) | | | -| 730 | 491 | "L2·满配 min 路径完成(实测:%s)" ↩ % win_min.result | win_min.result == "victory" | (待填) | | | -| 731 | 496 | "L2 min 路径逐跳为图出边·末节点为 Boss(实测路径 %s·末句柄型别 %s)" ↩ % [str(win_min.visited_ids), n2_last_type] | _path_in_graph(nodes2, win_min.visited_ids) and n2_last_type == "boss" | (待填) | | | -| 732 | 498 | "L2 min 路径终点深度 = 参数档层高 %d(行进读数 %d——终点落最长路径步数)" ↩ % [int(params2.get("layers", -1)), int(win_min.boss_depth)] | int(win_min.boss_depth) == int(params2.get("layers", -1)) | (待填) | | | -| 733 | 500 | "L2 min 路径穿商店恰 1 家(路径句柄 %s 型别计数 = 1——必经恰一店的行进面)" ↩ % str(win_min.visited_ids) | _path_shop_count(nodes2, win_min.visited_ids) == 1 | (待填) | | | -| 734 | 502 | "选路口发生 %d 次(每节点兑付后——零自动推进)" ↩ % win_min.choices | win_min.choices >= 3 | (待填) | | | -| 735 | 504 | "L2·满配 max 路径(候选取最大句柄)完成(实测:%s·路径 %s)" ↩ % [win_max.result, str(win_max.visited_ids)] | win_max.result == "victory" | (待填) | | | -| 736 | 509 | "L2 max 路径逐跳为图出边·末节点为 Boss·穿商店恰 1 家" | _path_in_graph(nodes2, win_max.visited_ids) and n2_last_max_type == "boss" and _path_shop_count(nodes2, win_max.visited_ids) == 1 | (待填) | | | -| 737 | 512 | "L2 max 与 min 路径分岔(方向轴生效——两路终点句柄 %d / %d 不同)" ↩ % [n2_last, n2_last_max] | n2_last != n2_last_max and win_max.visited_ids != win_min.visited_ids | (待填) | | | -| 738 | 519 | "手动驱动:L2 启动(入口战斗直接开打——入口无前置兑付;图 = 参数档 × 种子 %d)" ↩ % ACTIVE_GRAPH_SEED | runner.start_level(2, ACTIVE_GRAPH_SEED) | (待填) | | | -| 739 | 526 | "首战胜利后:settlement 释放 + 进选路态(战斗胜利不再自动推进——接口漂移清偿)" | String(res_box.get("result", "")) == "victory" and runner.current_battle() == null and runner.awaiting_choice() | (待填) | | | -| 740 | 529 | "runner 消费面图 = 生成面图(节点数 %d/%d 且句柄集逐位相同)" ↩ % [rnodes.size(), nodes2.size()] | rnodes.size() == nodes2.size() and _ids_of_type(rnodes, "battle").size() + _ids_of_type(rnodes, "boss").size() + _ids_of_type(rnodes, "reward").size() \\ + _ids_of_type(rnodes, "rest").size() + _ids_of_type(rnodes, "shop").size() == nodes2.size() and _nodes_by_id(rnodes) == _nodes_by_id(nodes2) | (待填) | | | -| 741 | 537 | "入口选路口候选非空且全在图内(候选 %s·类型 %s)" ↩ % [str(cand), str(cand_types)] | not cand.is_empty() and not cand_types.has("<不在图>") | (待填) | | | -| 742 | 538 | "候选下一跳类型两两互异(岔口去路类型不重复契约——实测 %s)" ↩ % str(cand_types) | cand_types.size() == _uniq(cand_types).size() | (待填) | | | -| 743 | 545 | "候选外句柄 %d 拒绝且仍待选(拒绝语义)" ↩ % outside_id | outside_id >= 0 and not runner.pick_next_node(outside_id) and runner.awaiting_choice() | (待填) | | | -| 744 | 555 | "入口岔口含奖励位与战斗位(句柄 %d/%d——候选型别可定位)" ↩ % [reward_cand, battle_cand] | reward_cand >= 0 and battle_cand >= 0 | (待填) | | | -| 745 | 556 | "选奖励候选 %d 受理" ↩ % reward_cand | runner.pick_next_node(reward_cand) | (待填) | | | -| 746 | 557 | "到达奖励点:直通兑付(确认前不出选路口)" | String(runner.current_node().get("type", "")) == "reward" and not runner.awaiting_choice() | (待填) | | | -| 747 | 559 | "兑付确认后才出现选路口(continue_after_node 泛化语义)候选 = 该点图出边 %s" ↩ % str(reward_outs) | runner.continue_after_node() and runner.awaiting_choice() and _sorted_ids(runner.next_candidate_ids()) == _sorted_ids(reward_outs) | (待填) | | | -| 748 | 566 | "选下一跳战斗候选 %d 开战" ↩ % next_battle | next_battle >= 0 and runner.pick_next_node(next_battle) and runner.current_battle() != null | (待填) | | | -| 749 | 591 | "首遇位零踩雷:完美档 12 种子(节点 0 熊地精)恒零踩雷" | green_ok | (待填) | | | -| 750 | 592 | "首遇战胜局 ≥7/12(实际 %d——阶段拔高后锚行退出绿档,多数胜口径)" ↩ % green_wins | green_wins >= 7 | (待填) | | | -| 751 | 593 | "首遇战胜后 HP 下探读数 %d(>0——绿档记录)" ↩ % green_min_hp | green_min_hp > 0 | (待填) | | | -| 752 | 597 | "跨表覆盖扫描面非空:L2 现行图战斗/Boss 节点 %d 颗" ↩ % int(cov2["nodes"]) | int(cov2["nodes"]) >= 1 | (待填) | | | -| 753 | 598 | "L2 现行图每战斗/Boss 节点深度敌侧解析落带 ≥ 请求深度(违例 %d 处)" ↩ % int(cov2["enemy_bad"]) | int(cov2["enemy_bad"]) == 0 | (待填) | | | -| 754 | 599 | "L2 现行图每节点深度 board_gen 精确命中(非空且 node_depth 等值·违例 %d 处)" ↩ % int(cov2["board_bad"]) | int(cov2["board_bad"]) == 0 | (待填) | | | -| 755 | 607 | "巨蜘蛛首遇带(2,battle,%d——扫表取最低带)行集 = {giant_spider} 恰一行且 unit_count=2 无 unit_count_max(固定 2 只——首遇教学位不加骰)〔fv22〕" ↩ % l2_spider_band | l2_spider_band >= 0 and l2_spider_ids.has("giant_spider") and l2_spider_ids.size() == 1 and l2_spider_set.size() == 1 \\ and int(l2_spider_set[0].get("unit_count", -1)) == 2 and not l2_spider_set[0].has("unit_count_max") | (待填) | | | -| 756 | 617 | "L2 全部终点深度 %s 的 Boss 行集均 = {drider} 恰一行且首行魔法甲(护甲类型重配批:关卡 2 唯一非轻甲;P4 乙 Boss 行集多行合法——零内容期仍单行)〔fv24〕" ↩ % str(l2_terms) | l2_boss_ok | (待填) | | | -| 757 | 624 | "首批混编行集 (2,battle,5) = 双行且行序 [bugbear 重击手 → giant_spider 巢群](R6 行序契约——文件序 = 声明序 = 生成序,首行默认目标 = 前排坦克;迭代 18 卡⑤投放)〔fv18〕" | mix_shape_ok | (待填) | | | -| 758 | 628 | "首批混编行集 (2,battle,5) 静态最坏骰和 = Σ(unit_count_max,固定行缺省=unit_count) = %d ≤ 4(行集实例总数守卫同口径——重击手固定 1 + 巢群 [2,3])〔fv19〕" ↩ % mix_sum_max | mix_sum_max <= 4 | (待填) | | | -| 759 | 629 | "首批混编新行 (2,battle,5,giant_spider) 行值 = 复制巢群行(P4 甲零数值新增——2-3 只/hp 26/atk [1,2]/light/ac 2;既有 19 行现值零变更)" | int(l2_mix_set[1].get("unit_count", 0)) == 2 and int(l2_mix_set[1].get("unit_count_max", 0)) == 3 \\ and int(l2_mix_set[1].get("hp", 0)) == 26 and int(l2_mix_set[1].get("armor_class", -1)) == 2 | (待填) | | | -| 760 | 636 | "L3 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" ↩ % [ACTIVE_GRAPH_SEED, nodes3.size()] | not nodes3.is_empty() | (待填) | | | -| 761 | 639 | "L3 节点数 %d ≥ 层高 %d+1 且 = 句柄去重读数 %d(全员句柄唯一)" ↩ % [nodes3.size(), int(params3.get("layers", -1)), _nodes_by_id(nodes3).size()] | nodes3.size() >= int(params3.get("layers", -1)) + 1 and _nodes_by_id(nodes3).size() == nodes3.size() | (待填) | | | -| 762 | 641 | "L3 五型计数合计 = 节点数(battle %d + boss %d + reward %d + rest %d + shop %d = %d)" ↩ % [tcount3["battle"], tcount3["boss"], tcount3["reward"], tcount3["rest"], tcount3["shop"], nodes3.size()] | int(tcount3["battle"]) + int(tcount3["boss"]) + int(tcount3["reward"]) + int(tcount3["rest"]) + int(tcount3["shop"]) == nodes3.size() | (待填) | | | -| 763 | 643 | "L3 战斗节点数 %d ≥ 参数档战斗保底 %d 且 Boss 节点数 %d = 参数档出口数 %d" ↩ % [tcount3["battle"], int(params3.get("battle_floor", -1)), tcount3["boss"], int(params3.get("exit_count", -1))] | int(tcount3["battle"]) >= int(params3.get("battle_floor", -1)) and int(tcount3["boss"]) == int(params3.get("exit_count", -1)) | (待填) | | | -| 764 | 645 | "L3 恢复/奖励/商店节点各 ≥1(实测 rest %d · reward %d · shop %d——计数由逐节点扫描同一读数)" ↩ % [tcount3["rest"], tcount3["reward"], tcount3["shop"]] | int(tcount3["rest"]) >= 1 and int(tcount3["reward"]) >= 1 and int(tcount3["shop"]) >= 1 | (待填) | | | -| 765 | 647 | "L3 岔口(出度 ≥2 的节点)= %d 处(≥1)" ↩ % branch3 | branch3 >= 1 | (待填) | | | -| 766 | 648 | "L3 入口数 %d = 参数档 entry_count · 终点数 %d = exit_count ≥2(多终点大岔口自 L2 起)" ↩ % [_entry_ids(nodes3).size(), _terminal_ids(nodes3).size()] | _entry_ids(nodes3).size() == int(params3.get("entry_count", -1)) and _terminal_ids(nodes3).size() == int(params3.get("exit_count", -1)) and _terminal_ids(nodes3).size() >= 2 | (待填) | | | -| 767 | 652 | "跨表覆盖扫描面非空:L3 现行图战斗/Boss 节点 %d 颗" ↩ % int(cov3["nodes"]) | int(cov3["nodes"]) >= 1 | (待填) | | | -| 768 | 653 | "L3 现行图每战斗/Boss 节点深度敌侧解析落带 ≥ 请求深度(违例 %d 处)" ↩ % int(cov3["enemy_bad"]) | int(cov3["enemy_bad"]) == 0 | (待填) | | | -| 769 | 654 | "L3 现行图每节点深度 board_gen 精确命中(非空且 node_depth 等值·违例 %d 处)" ↩ % int(cov3["board_bad"]) | int(cov3["board_bad"]) == 0 | (待填) | | | -| 770 | 661 | "灰矮人首遇带(3,battle,%d——扫表取最低带)行集 = {duergar} 恰一行、首行重甲、固定 1 只不加骰〔fv23〕" ↩ % l3_lead_band | l3_lead_band >= 0 and l3_lead_ids.has("duergar") and l3_lead_ids.size() == 1 \\ and String(l3_lead_set[0].get("armor_type", "")) == "heavy" \\ and int(l3_lead_set[0].get("unit_count", -1)) == 1 and not l3_lead_set[0].has("unit_count_max") | (待填) | | | -| 771 | 672 | "L3 全部终点深度 %s 的 Boss 行集均 = {duergar_king} 恰一行且首行重甲(重甲收束「轻甲→魔法甲→重甲」教学梯度)〔fv24〕" ↩ % str(l3_terms) | l3_boss_ok | (待填) | | | -| 772 | 695 | "L3 首遇位绿档:成长代理档口径(atk_pct=200/hp_pct=100)完美档 12 种子首遇战(节点 0 灰矮人)全胜且零踩雷" | green3_ok | (待填) | | | -| 773 | 696 | "L3 首遇战胜后 HP 下探读数 %d(>0——绿档记录)" ↩ % green3_min_hp | green3_min_hp > 0 | (待填) | | | -| 774 | 706 | "混编行集 spawn 受理((1,battle,5) 双行 地精1+熊地精2——定值无区间路径,行集构成纪律放开面正向)" | mix_sys.spawn(1, 5, int(_db.hero.max_hp)) | (待填) | | | -| 775 | 708 | "混编实例总数 = 各行数量求和(1+2=3——行集实例总数 ≤4 守卫内)" | mix_units.size() == 3 | (待填) | | | -| 776 | 709 | "混编生成序 = 文件行序(行序即契约——地精在前熊地精在后,handle_id 跨行连续派发)" | String(mix_units[0].get("enemy_id")) == "goblin" and String(mix_units[1].get("enemy_id")) == "bugbear" and String(mix_units[2].get("enemy_id")) == "bugbear" | (待填) | | | -| 777 | 710 | "混编各行行值独立投影(display_name/hp/atk 按各自行值——同场不同种不同值)" | String(mix_units[0].get("display_name")) == "地精" and int(mix_units[1].get("hp")) == 20 and int(mix_units[1].get("atk")) == 3 | (待填) | | | -| 778 | 714 | "行集实例总数守卫:两行各定值 3(总 6 > 4)spawn 被拒(行集构成纪律③——R3 fail-fast 运行面)" | not over_sys.spawn(1, 6, int(_db.hero.max_hp)) | (待填) | | | -| 779 | 719 | "混编断言段注入行全量还原(表回 28 行——迭代 35 生成图深度扩面覆盖批后的基线行数;首批混编投放基线 20 行+覆盖 8 行)" | int(_db.enemy.rows.size()) == 28 | (待填) | | | -| 780 | 725 | "可见性冒烟:L2 入口停点就位(defer_entry——无当前节点,锚层 0)" | vis_ok and vis_runner.awaiting_choice() and vis_runner.current_node_id() == -1 | (待填) | | | -| 781 | 731 | "可见性冒烟:入口停点候选集 ⊆ 类型可见集(下一跳恒可见)〔fv4〕" | vis_cand_in | (待填) | | | -| 782 | 736 | "可见性冒烟:L2 全部终点 Boss 句柄 %s 恒在可见集(终点不入雾)〔fv3〕" ↩ % str(_ids_of_type(nodes2, "boss")) | vis_boss_ok | (待填) | | | -| 783 | 738 | "可见性冒烟:N 读数据表 = %d(provisional 2——代码零硬编码)〔fv6〕" ↩ % vis_n | vis_n == 2 | (待填) | | | -| 784 | 760 | "可见性冒烟:入口锚层 0 时层距 ≤ %d 的节点全在可见集(扫描面 %d 颗·违例 0)〔fv1〕" ↩ % [vis_n, near_count] | near_count >= 1 and near_ok | (待填) | | | -| 785 | 761 | "可见性冒烟:入口锚层 0 时层距 > %d 的非终点节点全入雾(扫描面 %d 颗·违例 0)〔fv1〕" ↩ % [vis_n, fog_count] | fog_count >= 1 and fog_ok | (待填) | | | +| 707 | 442 | "LevelTable 行级校验零错误(图合法性移交 validate_graph)" | _db.run.level.validate().is_empty() | (待填) | | | +| 708 | 446 | "L2 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" ↩ % [ACTIVE_GRAPH_SEED, nodes2.size()] | not nodes2.is_empty() | (待填) | | | +| 709 | 449 | "L2 节点数 %d ≥ 层高 %d+1 且 = 句柄去重读数 %d(全员句柄唯一)" ↩ % [nodes2.size(), int(params2.get("layers", -1)), _nodes_by_id(nodes2).size()] | nodes2.size() >= int(params2.get("layers", -1)) + 1 and _nodes_by_id(nodes2).size() == nodes2.size() | (待填) | | | +| 710 | 451 | "L2 五型计数合计 = 节点数(battle %d + boss %d + reward %d + rest %d + shop %d = %d)" ↩ % [tcount["battle"], tcount["boss"], tcount["reward"], tcount["rest"], tcount["shop"], nodes2.size()] | int(tcount["battle"]) + int(tcount["boss"]) + int(tcount["reward"]) + int(tcount["rest"]) + int(tcount["shop"]) == nodes2.size() | (待填) | | | +| 711 | 453 | "L2 战斗节点数 %d ≥ 参数档战斗保底 %d 且 Boss 节点数 %d = 参数档出口数 %d" ↩ % [tcount["battle"], int(params2.get("battle_floor", -1)), tcount["boss"], int(params2.get("exit_count", -1))] | int(tcount["battle"]) >= int(params2.get("battle_floor", -1)) and int(tcount["boss"]) == int(params2.get("exit_count", -1)) | (待填) | | | +| 712 | 455 | "L2 恢复/奖励/商店节点各 ≥1(实测 rest %d · reward %d · shop %d——计数由逐节点扫描同一读数)" ↩ % [tcount["rest"], tcount["reward"], tcount["shop"]] | int(tcount["rest"]) >= 1 and int(tcount["reward"]) >= 1 and int(tcount["shop"]) >= 1 | (待填) | | | +| 713 | 457 | "L2 岔口(出度 ≥2 的节点)= %d 处(≥1)" ↩ % branch | branch >= 1 | (待填) | | | +| 714 | 467 | "图校验正例:两岔合流小图零错误〔v1〕" | LT.validate_graph(good).is_empty() | (待填) | | | +| 715 | 470 | "图校验拒绝:句柄重复(图内唯一句柄)〔v1〕" | _has_err_category(LT.validate_graph(dup), "dup_key") | (待填) | | | +| 716 | 473 | "图校验正例:多入口合法(入口唯一判据随模板池退役)〔v1〕" | LT.validate_graph(dual).is_empty() | (待填) | | | +| 717 | 476 | "图校验拒绝:Boss 带出边(终点不可外出)〔v1〕" | _has_err_category(LT.validate_graph(boss_out), "structural") | (待填) | | | +| 718 | 479 | "图校验拒绝:回边 1→0(目标深度须严格大于源)〔v1〕" | _has_err_category(LT.validate_graph(cyc), "structural") | (待填) | | | +| 719 | 482 | "图校验拒绝:出边指向不存在句柄(悬空致命)〔v1〕" | _has_err_category(LT.validate_graph(dang), "dangling_next") | (待填) | | | +| 720 | 488 | "图校验拒绝:非 Boss 终点(终点恒为 Boss)〔v1〕" | _has_err_category(LT.validate_graph(orphan), "structural") | (待填) | | | +| 721 | 491 | "L2·满配 min 路径完成(实测:%s)" ↩ % win_min.result | win_min.result == "victory" | (待填) | | | +| 722 | 496 | "L2 min 路径逐跳为图出边·末节点为 Boss(实测路径 %s·末句柄型别 %s)" ↩ % [str(win_min.visited_ids), n2_last_type] | _path_in_graph(nodes2, win_min.visited_ids) and n2_last_type == "boss" | (待填) | | | +| 723 | 498 | "L2 min 路径终点深度 = 参数档层高 %d(行进读数 %d——终点落最长路径步数)" ↩ % [int(params2.get("layers", -1)), int(win_min.boss_depth)] | int(win_min.boss_depth) == int(params2.get("layers", -1)) | (待填) | | | +| 724 | 500 | "L2 min 路径穿商店恰 1 家(路径句柄 %s 型别计数 = 1——必经恰一店的行进面)" ↩ % str(win_min.visited_ids) | _path_shop_count(nodes2, win_min.visited_ids) == 1 | (待填) | | | +| 725 | 502 | "选路口发生 %d 次(每节点兑付后——零自动推进)" ↩ % win_min.choices | win_min.choices >= 3 | (待填) | | | +| 726 | 504 | "L2·满配 max 路径(候选取最大句柄)完成(实测:%s·路径 %s)" ↩ % [win_max.result, str(win_max.visited_ids)] | win_max.result == "victory" | (待填) | | | +| 727 | 509 | "L2 max 路径逐跳为图出边·末节点为 Boss·穿商店恰 1 家" | _path_in_graph(nodes2, win_max.visited_ids) and n2_last_max_type == "boss" and _path_shop_count(nodes2, win_max.visited_ids) == 1 | (待填) | | | +| 728 | 512 | "L2 max 与 min 路径分岔(方向轴生效——两路终点句柄 %d / %d 不同)" ↩ % [n2_last, n2_last_max] | n2_last != n2_last_max and win_max.visited_ids != win_min.visited_ids | (待填) | | | +| 729 | 519 | "手动驱动:L2 启动(入口战斗直接开打——入口无前置兑付;图 = 参数档 × 种子 %d)" ↩ % ACTIVE_GRAPH_SEED | runner.start_level(2, ACTIVE_GRAPH_SEED) | (待填) | | | +| 730 | 526 | "首战胜利后:settlement 释放 + 进选路态(战斗胜利不再自动推进——接口漂移清偿)" | String(res_box.get("result", "")) == "victory" and runner.current_battle() == null and runner.awaiting_choice() | (待填) | | | +| 731 | 529 | "runner 消费面图 = 生成面图(节点数 %d/%d 且句柄集逐位相同)" ↩ % [rnodes.size(), nodes2.size()] | rnodes.size() == nodes2.size() and _ids_of_type(rnodes, "battle").size() + _ids_of_type(rnodes, "boss").size() + _ids_of_type(rnodes, "reward").size() \\ + _ids_of_type(rnodes, "rest").size() + _ids_of_type(rnodes, "shop").size() == nodes2.size() and _nodes_by_id(rnodes) == _nodes_by_id(nodes2) | (待填) | | | +| 732 | 537 | "入口选路口候选非空且全在图内(候选 %s·类型 %s)" ↩ % [str(cand), str(cand_types)] | not cand.is_empty() and not cand_types.has("<不在图>") | (待填) | | | +| 733 | 538 | "候选下一跳类型两两互异(岔口去路类型不重复契约——实测 %s)" ↩ % str(cand_types) | cand_types.size() == _uniq(cand_types).size() | (待填) | | | +| 734 | 545 | "候选外句柄 %d 拒绝且仍待选(拒绝语义)" ↩ % outside_id | outside_id >= 0 and not runner.pick_next_node(outside_id) and runner.awaiting_choice() | (待填) | | | +| 735 | 555 | "入口岔口含奖励位与战斗位(句柄 %d/%d——候选型别可定位)" ↩ % [reward_cand, battle_cand] | reward_cand >= 0 and battle_cand >= 0 | (待填) | | | +| 736 | 556 | "选奖励候选 %d 受理" ↩ % reward_cand | runner.pick_next_node(reward_cand) | (待填) | | | +| 737 | 557 | "到达奖励点:直通兑付(确认前不出选路口)" | String(runner.current_node().get("type", "")) == "reward" and not runner.awaiting_choice() | (待填) | | | +| 738 | 559 | "兑付确认后才出现选路口(continue_after_node 泛化语义)候选 = 该点图出边 %s" ↩ % str(reward_outs) | runner.continue_after_node() and runner.awaiting_choice() and _sorted_ids(runner.next_candidate_ids()) == _sorted_ids(reward_outs) | (待填) | | | +| 739 | 566 | "选下一跳战斗候选 %d 开战" ↩ % next_battle | next_battle >= 0 and runner.pick_next_node(next_battle) and runner.current_battle() != null | (待填) | | | +| 740 | 591 | "首遇位零踩雷:完美档 12 种子(节点 0 熊地精)恒零踩雷" | green_ok | (待填) | | | +| 741 | 592 | "首遇战胜局 ≥7/12(实际 %d——阶段拔高后锚行退出绿档,多数胜口径)" ↩ % green_wins | green_wins >= 7 | (待填) | | | +| 742 | 593 | "首遇战胜后 HP 下探读数 %d(>0——绿档记录)" ↩ % green_min_hp | green_min_hp > 0 | (待填) | | | +| 743 | 597 | "跨表覆盖扫描面非空:L2 现行图战斗/Boss 节点 %d 颗" ↩ % int(cov2["nodes"]) | int(cov2["nodes"]) >= 1 | (待填) | | | +| 744 | 598 | "L2 现行图每战斗/Boss 节点深度敌侧解析落带 ≥ 请求深度(违例 %d 处)" ↩ % int(cov2["enemy_bad"]) | int(cov2["enemy_bad"]) == 0 | (待填) | | | +| 745 | 599 | "L2 现行图每节点深度 board_gen 精确命中(非空且 node_depth 等值·违例 %d 处)" ↩ % int(cov2["board_bad"]) | int(cov2["board_bad"]) == 0 | (待填) | | | +| 746 | 607 | "巨蜘蛛首遇带(2,battle,%d——扫表取最低带)行集 = {giant_spider} 恰一行且 unit_count=2 无 unit_count_max(固定 2 只——首遇教学位不加骰)〔fv22〕" ↩ % l2_spider_band | l2_spider_band >= 0 and l2_spider_ids.has("giant_spider") and l2_spider_ids.size() == 1 and l2_spider_set.size() == 1 \\ and int(l2_spider_set[0].get("unit_count", -1)) == 2 and not l2_spider_set[0].has("unit_count_max") | (待填) | | | +| 747 | 617 | "L2 全部终点深度 %s 的 Boss 行集均 = {drider} 恰一行且首行魔法甲(护甲类型重配批:关卡 2 唯一非轻甲;P4 乙 Boss 行集多行合法——零内容期仍单行)〔fv24〕" ↩ % str(l2_terms) | l2_boss_ok | (待填) | | | +| 748 | 624 | "首批混编行集 (2,battle,5) = 双行且行序 [bugbear 重击手 → giant_spider 巢群](R6 行序契约——文件序 = 声明序 = 生成序,首行默认目标 = 前排坦克;迭代 18 卡⑤投放)〔fv18〕" | mix_shape_ok | (待填) | | | +| 749 | 628 | "首批混编行集 (2,battle,5) 静态最坏骰和 = Σ(unit_count_max,固定行缺省=unit_count) = %d ≤ 4(行集实例总数守卫同口径——重击手固定 1 + 巢群 [2,3])〔fv19〕" ↩ % mix_sum_max | mix_sum_max <= 4 | (待填) | | | +| 750 | 629 | "首批混编新行 (2,battle,5,giant_spider) 行值 = 复制巢群行(P4 甲零数值新增——2-3 只/hp 26/atk [1,2]/light/ac 2;既有 19 行现值零变更)" | int(l2_mix_set[1].get("unit_count", 0)) == 2 and int(l2_mix_set[1].get("unit_count_max", 0)) == 3 \\ and int(l2_mix_set[1].get("hp", 0)) == 26 and int(l2_mix_set[1].get("armor_class", -1)) == 2 | (待填) | | | +| 751 | 636 | "L3 现行起关图取数非空(种子 %d × 参数档 → 节点 %d 颗)" ↩ % [ACTIVE_GRAPH_SEED, nodes3.size()] | not nodes3.is_empty() | (待填) | | | +| 752 | 639 | "L3 节点数 %d ≥ 层高 %d+1 且 = 句柄去重读数 %d(全员句柄唯一)" ↩ % [nodes3.size(), int(params3.get("layers", -1)), _nodes_by_id(nodes3).size()] | nodes3.size() >= int(params3.get("layers", -1)) + 1 and _nodes_by_id(nodes3).size() == nodes3.size() | (待填) | | | +| 753 | 641 | "L3 五型计数合计 = 节点数(battle %d + boss %d + reward %d + rest %d + shop %d = %d)" ↩ % [tcount3["battle"], tcount3["boss"], tcount3["reward"], tcount3["rest"], tcount3["shop"], nodes3.size()] | int(tcount3["battle"]) + int(tcount3["boss"]) + int(tcount3["reward"]) + int(tcount3["rest"]) + int(tcount3["shop"]) == nodes3.size() | (待填) | | | +| 754 | 643 | "L3 战斗节点数 %d ≥ 参数档战斗保底 %d 且 Boss 节点数 %d = 参数档出口数 %d" ↩ % [tcount3["battle"], int(params3.get("battle_floor", -1)), tcount3["boss"], int(params3.get("exit_count", -1))] | int(tcount3["battle"]) >= int(params3.get("battle_floor", -1)) and int(tcount3["boss"]) == int(params3.get("exit_count", -1)) | (待填) | | | +| 755 | 645 | "L3 恢复/奖励/商店节点各 ≥1(实测 rest %d · reward %d · shop %d——计数由逐节点扫描同一读数)" ↩ % [tcount3["rest"], tcount3["reward"], tcount3["shop"]] | int(tcount3["rest"]) >= 1 and int(tcount3["reward"]) >= 1 and int(tcount3["shop"]) >= 1 | (待填) | | | +| 756 | 647 | "L3 岔口(出度 ≥2 的节点)= %d 处(≥1)" ↩ % branch3 | branch3 >= 1 | (待填) | | | +| 757 | 648 | "L3 入口数 %d = 参数档 entry_count · 终点数 %d = exit_count ≥2(多终点大岔口自 L2 起)" ↩ % [_entry_ids(nodes3).size(), _terminal_ids(nodes3).size()] | _entry_ids(nodes3).size() == int(params3.get("entry_count", -1)) and _terminal_ids(nodes3).size() == int(params3.get("exit_count", -1)) and _terminal_ids(nodes3).size() >= 2 | (待填) | | | +| 758 | 652 | "跨表覆盖扫描面非空:L3 现行图战斗/Boss 节点 %d 颗" ↩ % int(cov3["nodes"]) | int(cov3["nodes"]) >= 1 | (待填) | | | +| 759 | 653 | "L3 现行图每战斗/Boss 节点深度敌侧解析落带 ≥ 请求深度(违例 %d 处)" ↩ % int(cov3["enemy_bad"]) | int(cov3["enemy_bad"]) == 0 | (待填) | | | +| 760 | 654 | "L3 现行图每节点深度 board_gen 精确命中(非空且 node_depth 等值·违例 %d 处)" ↩ % int(cov3["board_bad"]) | int(cov3["board_bad"]) == 0 | (待填) | | | +| 761 | 661 | "灰矮人首遇带(3,battle,%d——扫表取最低带)行集 = {duergar} 恰一行、首行重甲、固定 1 只不加骰〔fv23〕" ↩ % l3_lead_band | l3_lead_band >= 0 and l3_lead_ids.has("duergar") and l3_lead_ids.size() == 1 \\ and String(l3_lead_set[0].get("armor_type", "")) == "heavy" \\ and int(l3_lead_set[0].get("unit_count", -1)) == 1 and not l3_lead_set[0].has("unit_count_max") | (待填) | | | +| 762 | 672 | "L3 全部终点深度 %s 的 Boss 行集均 = {duergar_king} 恰一行且首行重甲(重甲收束「轻甲→魔法甲→重甲」教学梯度)〔fv24〕" ↩ % str(l3_terms) | l3_boss_ok | (待填) | | | +| 763 | 695 | "L3 首遇位绿档:成长代理档口径(atk_pct=200/hp_pct=100)完美档 12 种子首遇战(节点 0 灰矮人)全胜且零踩雷" | green3_ok | (待填) | | | +| 764 | 696 | "L3 首遇战胜后 HP 下探读数 %d(>0——绿档记录)" ↩ % green3_min_hp | green3_min_hp > 0 | (待填) | | | +| 765 | 706 | "混编行集 spawn 受理((1,battle,5) 双行 地精1+熊地精2——定值无区间路径,行集构成纪律放开面正向)" | mix_sys.spawn({"level_index": 1, "node_depth": 5, "hero_hp": int(_db.combat.hero.max_hp)}) | (待填) | | | +| 766 | 708 | "混编实例总数 = 各行数量求和(1+2=3——行集实例总数 ≤4 守卫内)" | mix_units.size() == 3 | (待填) | | | +| 767 | 709 | "混编生成序 = 文件行序(行序即契约——地精在前熊地精在后,handle_id 跨行连续派发)" | String(mix_units[0].get("enemy_id")) == "goblin" and String(mix_units[1].get("enemy_id")) == "bugbear" and String(mix_units[2].get("enemy_id")) == "bugbear" | (待填) | | | +| 768 | 710 | "混编各行行值独立投影(display_name/hp/atk 按各自行值——同场不同种不同值)" | String(mix_units[0].get("display_name")) == "地精" and int(mix_units[1].get("hp")) == 20 and int(mix_units[1].get("atk")) == 3 | (待填) | | | +| 769 | 714 | "行集实例总数守卫:两行各定值 3(总 6 > 4)spawn 被拒(行集构成纪律③——R3 fail-fast 运行面)" | not over_sys.spawn({"level_index": 1, "node_depth": 6, "hero_hp": int(_db.combat.hero.max_hp)}) | (待填) | | | +| 770 | 719 | "混编断言段注入行全量还原(表回 28 行——迭代 35 生成图深度扩面覆盖批后的基线行数;首批混编投放基线 20 行+覆盖 8 行)" | int(_db.combat.enemy.rows.size()) == 28 | (待填) | | | +| 771 | 725 | "可见性冒烟:L2 入口停点就位(defer_entry——无当前节点,锚层 0)" | vis_ok and vis_runner.awaiting_choice() and vis_runner.current_node_id() == -1 | (待填) | | | +| 772 | 731 | "可见性冒烟:入口停点候选集 ⊆ 类型可见集(下一跳恒可见)〔fv4〕" | vis_cand_in | (待填) | | | +| 773 | 736 | "可见性冒烟:L2 全部终点 Boss 句柄 %s 恒在可见集(终点不入雾)〔fv3〕" ↩ % str(_ids_of_type(nodes2, "boss")) | vis_boss_ok | (待填) | | | +| 774 | 738 | "可见性冒烟:N 读数据表 = %d(provisional 2——代码零硬编码)〔fv6〕" ↩ % vis_n | vis_n == 2 | (待填) | | | +| 775 | 760 | "可见性冒烟:入口锚层 0 时层距 ≤ %d 的节点全在可见集(扫描面 %d 颗·违例 0)〔fv1〕" ↩ % [vis_n, near_count] | near_count >= 1 and near_ok | (待填) | | | +| 776 | 761 | "可见性冒烟:入口锚层 0 时层距 > %d 的非终点节点全入雾(扫描面 %d 颗·违例 0)〔fv1〕" ↩ % [vis_n, fog_count] | fog_count >= 1 and fog_ok | (待填) | | | ## S105 _smoke_gp_wallet(行 798–834 · 9 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 786 | 798 | "GP 到账:L1 启动(普通战档)" | runner.start_level(1, 42) | (待填) | | | -| 787 | 808 | "L1 首战胜利(battle_finished gp_dropped 载荷在案)" | fin.has("p") and String(fin["p"].get("result", "")) == "victory" | (待填) | | | -| 788 | 811 | "L1 普通战 gp_dropped − escrow ∈ [2+0, 4+0](掷定+关级加值 %d,escrow %+d)" ↩ % [gp_dropped - escrow_sum, escrow_sum] | gp_dropped - escrow_sum >= 2 and gp_dropped - escrow_sum <= 4 | (待填) | | | -| 789 | 816 | "钱包恰入账一次(gain 事件 ×%d)" ↩ % gains.size() | gains.size() == 1 | (待填) | | | -| 790 | 818 | "入账金额 = gp_dropped 载荷(%d)" ↩ % gp_dropped | int(gains[0]["amount"]) == gp_dropped | (待填) | | | -| 791 | 819 | "钱包读数 = 入账后投影(gp=%d,破财负值场景经 clamp ≥0)" ↩ % runner.gp | runner.gp == maxi(0, gp_dropped) and runner.gp == int(gains[0]["gp"]) | (待填) | | | -| 792 | 826 | "L2 Boss 战启动((2,9)——Boss 档掷定 [6,10] + 关级加值 1)" | boss.begin_battle({"level_index": 2, "node_depth": 9, "node_type": "boss", "hero_hp": int(_db.hero.max_hp)}) | (待填) | | | -| 793 | 831 | "L2 Boss 战终局(%s——gp_dropped 于战斗末掷定上抛)" ↩ % String(boss.battle_result) | fin_b.has("p") | (待填) | | | -| 794 | 834 | "L2 Boss 战 gp_dropped − escrow ∈ [6+1, 10+1](掷定+关级加值 %d,escrow %+d)" ↩ % [gp_b - escrow_b, escrow_b] | gp_b - escrow_b >= 7 and gp_b - escrow_b <= 11 | (待填) | | | +| 777 | 798 | "GP 到账:L1 启动(普通战档)" | runner.start_level(1, 42) | (待填) | | | +| 778 | 808 | "L1 首战胜利(battle_finished gp_dropped 载荷在案)" | fin.has("p") and String(fin["p"].get("result", "")) == "victory" | (待填) | | | +| 779 | 811 | "L1 普通战 gp_dropped − escrow ∈ [2+0, 4+0](掷定+关级加值 %d,escrow %+d)" ↩ % [gp_dropped - escrow_sum, escrow_sum] | gp_dropped - escrow_sum >= 2 and gp_dropped - escrow_sum <= 4 | (待填) | | | +| 780 | 816 | "钱包恰入账一次(gain 事件 ×%d)" ↩ % gains.size() | gains.size() == 1 | (待填) | | | +| 781 | 818 | "入账金额 = gp_dropped 载荷(%d)" ↩ % gp_dropped | int(gains[0]["amount"]) == gp_dropped | (待填) | | | +| 782 | 819 | "钱包读数 = 入账后投影(gp=%d,破财负值场景经 clamp ≥0)" ↩ % runner.gp | runner.gp == maxi(0, gp_dropped) and runner.gp == int(gains[0]["gp"]) | (待填) | | | +| 783 | 826 | "L2 Boss 战启动((2,9)——Boss 档掷定 [6,10] + 关级加值 1)" | boss.begin_battle({"level_index": 2, "node_depth": 9, "node_type": "boss", "hero_hp": int(_db.combat.hero.max_hp)}) | (待填) | | | +| 784 | 831 | "L2 Boss 战终局(%s——gp_dropped 于战斗末掷定上抛)" ↩ % String(boss.battle_result) | fin_b.has("p") | (待填) | | | +| 785 | 834 | "L2 Boss 战 gp_dropped − escrow ∈ [6+1, 10+1](掷定+关级加值 %d,escrow %+d)" ↩ % [gp_b - escrow_b, escrow_b] | gp_b - escrow_b >= 7 and gp_b - escrow_b <= 11 | (待填) | | | ## S106 _smoke_gp_escrow(行 872–936 · 12 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 795 | 872 | "GP 袋(gp 型安全格)可挖掘局种子可达(seed=%s)" ↩ % str(scan.get("seed", -1)) | not scan.is_empty() | (待填) | | | -| 796 | 885 | "GP局战斗终局(先手 999 收束)" | settlement2.battle_result == "victory" | (待填) | | | -| 797 | 890 | "GP 袋兑付行在案(GP 袋 +3 → 战斗内缓冲,origin 指回来源格)" | gain_ok | (待填) | | | -| 798 | 891 | "escrow 累计缓冲 ≥3(实测 %d——战斗域中间态,不入钱包不入增益容器)" ↩ % int(settlement2.reward_grant().escrow) | int(settlement2.reward_grant().escrow) >= 3 | (待填) | | | -| 799 | 893 | "格面兑付计入 gp_dropped(%d − escrow %d ∈ [2,4]——战斗末一次到账,随上抛即弃)" ↩ % [gp_d, int(settlement2.reward_grant().escrow)] | gp_d - int(settlement2.reward_grant().escrow) >= 2 and gp_d - int(settlement2.reward_grant().escrow) <= 4 | (待填) | | | -| 800 | 899 | "破财直验战斗启动" | settle_l.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp), "hero_atk": 999, "node_type": "boss"}) | (待填) | | | -| 801 | 903 | "破财 ×3 → escrow −6(负向缓冲记账,零钱包触达)" | int(settle_l.reward_grant().escrow) == -6 | (待填) | | | -| 802 | 908 | "破财压制 gp_dropped 负值上抛(%d + 6 = %d ∈ Boss 档掷定 [%d,%d] + 关级加值 %d——掷定不受破财污染)" ↩ % [ gp_l, gp_l + 6, int(boss_drop.get("roll_min", 0)), int(boss_drop.get("roll_max", 0)), boss_bonus1] | gp_l + 6 >= int(boss_drop.get("roll_min", 0)) + boss_bonus1 and gp_l + 6 <= int(boss_drop.get("roll_max", 0)) + boss_bonus1 | (待填) | | | -| 803 | 918 | "clamp 场景:L1 启动(冒险钱包开局 0)" | runner.start_level(1, 4242) | (待填) | | | -| 804 | 930 | "破财压制入账载荷为负(gp_dropped=%d)" ↩ % gp3 | gp3 < 0 | (待填) | | | -| 805 | 935 | "破财入账恰一次且投影读数 0(amount=%d,clamp 后 gp=0)" ↩ % gp3 | gain3.size() == 1 and int(gain3[0]["amount"]) == gp3 and int(gain3[0]["gp"]) == 0 | (待填) | | | -| 806 | 936 | "钱包全程不透支(gp=%d,事件投影最小读数 %d ≥ 0)" ↩ % [runner.gp, int(ev_box["min_gp"])] | runner.gp == 0 and int(ev_box["min_gp"]) >= 0 | (待填) | | | +| 786 | 872 | "GP 袋(gp 型安全格)可挖掘局种子可达(seed=%s)" ↩ % str(scan.get("seed", -1)) | not scan.is_empty() | (待填) | | | +| 787 | 885 | "GP局战斗终局(先手 999 收束)" | settlement2.battle_result == "victory" | (待填) | | | +| 788 | 890 | "GP 袋兑付行在案(GP 袋 +3 → 战斗内缓冲,origin 指回来源格)" | gain_ok | (待填) | | | +| 789 | 891 | "escrow 累计缓冲 ≥3(实测 %d——战斗域中间态,不入钱包不入增益容器)" ↩ % int(settlement2.reward_grant().escrow) | int(settlement2.reward_grant().escrow) >= 3 | (待填) | | | +| 790 | 893 | "格面兑付计入 gp_dropped(%d − escrow %d ∈ [2,4]——战斗末一次到账,随上抛即弃)" ↩ % [gp_d, int(settlement2.reward_grant().escrow)] | gp_d - int(settlement2.reward_grant().escrow) >= 2 and gp_d - int(settlement2.reward_grant().escrow) <= 4 | (待填) | | | +| 791 | 899 | "破财直验战斗启动" | settle_l.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": 999, "node_type": "boss"}) | (待填) | | | +| 792 | 903 | "破财 ×3 → escrow −6(负向缓冲记账,零钱包触达)" | int(settle_l.reward_grant().escrow) == -6 | (待填) | | | +| 793 | 908 | "破财压制 gp_dropped 负值上抛(%d + 6 = %d ∈ Boss 档掷定 [%d,%d] + 关级加值 %d——掷定不受破财污染)" ↩ % [ gp_l, gp_l + 6, int(boss_drop.get("roll_min", 0)), int(boss_drop.get("roll_max", 0)), boss_bonus1] | gp_l + 6 >= int(boss_drop.get("roll_min", 0)) + boss_bonus1 and gp_l + 6 <= int(boss_drop.get("roll_max", 0)) + boss_bonus1 | (待填) | | | +| 794 | 918 | "clamp 场景:L1 启动(冒险钱包开局 0)" | runner.start_level(1, 4242) | (待填) | | | +| 795 | 930 | "破财压制入账载荷为负(gp_dropped=%d)" ↩ % gp3 | gp3 < 0 | (待填) | | | +| 796 | 935 | "破财入账恰一次且投影读数 0(amount=%d,clamp 后 gp=0)" ↩ % gp3 | gain3.size() == 1 and int(gain3[0]["amount"]) == gp3 and int(gain3[0]["gp"]) == 0 | (待填) | | | +| 797 | 936 | "钱包全程不透支(gp=%d,事件投影最小读数 %d ≥ 0)" ↩ % [runner.gp, int(ev_box["min_gp"])] | runner.gp == 0 and int(ev_box["min_gp"]) >= 0 | (待填) | | | ## S107 _smoke_shop_room(行 1002–1088 · 25 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 807 | 1002 | "商店房到达种子可达(seed=%s——进店 GP ≥ 16)" ↩ % str(shop_run.get("seed", -1)) | not shop_run.is_empty() | (待填) | | | -| 808 | 1015 | "L2 行进到商店房:当前节点型别 = shop 且出边恰一条指终点 Boss(句柄 %d·出边 %s)" ↩ % [shop_id, str(shop_outs)] | String(runner.current_node().get("type", "")) == "shop" and shop_outs.size() == 1 \\ and String(s_byid.get(int(shop_outs[0]), {}).get("type", "")) == "boss" \\ and (s_byid.get(int(shop_outs[0]), {}).get("next", []) as Array).is_empty() | (待填) | | | -| 809 | 1019 | "L2 到店路径穿商店恰 1 家(已到句柄 %s 型别计数——必经恰一店的行进面)" ↩ % str(runner.visited_ids()) | _path_shop_count(s_nodes, runner.visited_ids()) == 1 | (待填) | | | -| 810 | 1021 | "L2 商店节点(句柄 %d)入边恰一条(店位归终点的直接前驱——行进面复核)" ↩ % shop_id | shop_preds == 1 | (待填) | | | -| 811 | 1023 | "到店商店节点无出边(夹具停摆——去终点方向无法从图解析)" | false | (待填) | | | -| 812 | 1025 | "到店开店态(覆盖层数据源就位——离开前不出选路口,零自动推进)" | not runner.awaiting_choice() and runner.next_candidate_ids().is_empty() | (待填) | | | -| 813 | 1028 | "三商品全陈列(atk_up/hp_up/armor_up——无刷新无摇号)" | prods.size() == 3 | (待填) | | | -| 814 | 1030 | "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 | (待填) | | | -| 815 | 1033 | "购买 atk_up 受理(现价 6)" | runner.buy_product("atk_up") | (待填) | | | -| 816 | 1034 | "钱包扣现价 6(%d → %d)" ↩ % [gp0, runner.gp] | runner.gp == gp0 - 6 | (待填) | | | -| 817 | 1036 | "统一冒险加算区 atk +1(加算区住加算日记)" | runner.journal().bonus_total("atk") == 1 and runner.journal().bonus_total("max_hp") == 0 and runner.journal().bonus_total("armor_class") == 0 | (待填) | | | -| 818 | 1038 | "atk_up 二购现价 = 4+2+2=8(步长 2 × 已购 1 档,现价现算)" | int(by_id["atk_up"]["price"]) == 8 and int(by_id["atk_up"]["purchased"]) == 1 | (待填) | | | -| 819 | 1045 | "购买 hp_up 受理(现价 4)" | runner.buy_product("hp_up") | (待填) | | | -| 820 | 1046 | "钱包扣 4(%d)" ↩ % runner.gp | runner.gp == gp0 - 6 - 4 | (待填) | | | -| 821 | 1047 | "生命上限 +3(%d → %d——加算区 max_hp 轴)" ↩ % [mhp_before, runner.effective_max_hp()] | runner.effective_max_hp() == mhp_before + 3 | (待填) | | | -| 822 | 1048 | "当前生命同步 +3 封顶新上限(%d → %d)" ↩ % [hp_before, runner.hero_hp] | runner.hero_hp == mini(hp_before + 3, runner.effective_max_hp()) | (待填) | | | -| 823 | 1050 | "购买 armor_up 受理(现价 6)" | runner.buy_product("armor_up") | (待填) | | | -| 824 | 1051 | "AC = 10+1(armor_class 加算区轴,无百分比通道)" | runner.effective_ac() == int(_db.hero.armor_class) + 1 | (待填) | | | -| 825 | 1066 | "余额不足购买被拒(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 | (待填) | | | -| 826 | 1068 | "离开商店 → 回选路口(候选 = 店出边 %s——零自动推进收口)" ↩ % str(shop_outs) | runner.continue_after_node() and runner.awaiting_choice() and _sorted_ids(runner.next_candidate_ids()) == _sorted_ids(shop_outs) | (待填) | | | -| 827 | 1071 | "主角攻击生效值 = 2+1(基础表值 + 加算区)" | runner.effective_atk() == int(_db.hero.atk) + 1 | (待填) | | | -| 828 | 1072 | "选终点 Boss(句柄 %d)开战" ↩ % int(shop_outs[0]) | runner.pick_next_node(int(shop_outs[0])) and runner.current_battle() != null | (待填) | | | -| 829 | 1076 | "战斗开始参数 = 基础表值 + 加算区(atk %d = 2+1 / max_hp %d = %d+%d×3)" ↩ % [int(snap["hero"].get("atk", -1)), int(snap["hero"].get("max_hp", -1)), int(_db.hero.max_hp), hp_buys] | int(snap["hero"].get("atk", -1)) == int(_db.hero.atk) + 1 and int(snap["hero"].get("max_hp", -1)) == int(_db.hero.max_hp) + 3 * hp_buys | (待填) | | | -| 830 | 1087 | "冒险收束(Boss 战终局:%s)" ↩ % String(fin_end.get("p", {}).get("result", "")) | fin_end.has("p") and String(fin_end["p"].get("result", "")) in ["victory", "defeat"] | (待填) | | | -| 831 | 1088 | "冒险收束钱包清零(局内货币生命周期终点——不入存档不入摘要)" | runner.gp == 0 | (待填) | | | +| 798 | 1002 | "商店房到达种子可达(seed=%s——进店 GP ≥ 16)" ↩ % str(shop_run.get("seed", -1)) | not shop_run.is_empty() | (待填) | | | +| 799 | 1015 | "L2 行进到商店房:当前节点型别 = shop 且出边恰一条指终点 Boss(句柄 %d·出边 %s)" ↩ % [shop_id, str(shop_outs)] | String(runner.current_node().get("type", "")) == "shop" and shop_outs.size() == 1 \\ and String(s_byid.get(int(shop_outs[0]), {}).get("type", "")) == "boss" \\ and (s_byid.get(int(shop_outs[0]), {}).get("next", []) as Array).is_empty() | (待填) | | | +| 800 | 1019 | "L2 到店路径穿商店恰 1 家(已到句柄 %s 型别计数——必经恰一店的行进面)" ↩ % str(runner.visited_ids()) | _path_shop_count(s_nodes, runner.visited_ids()) == 1 | (待填) | | | +| 801 | 1021 | "L2 商店节点(句柄 %d)入边恰一条(店位归终点的直接前驱——行进面复核)" ↩ % shop_id | shop_preds == 1 | (待填) | | | +| 802 | 1023 | "到店商店节点无出边(夹具停摆——去终点方向无法从图解析)" | false | (待填) | | | +| 803 | 1025 | "到店开店态(覆盖层数据源就位——离开前不出选路口,零自动推进)" | not runner.awaiting_choice() and runner.next_candidate_ids().is_empty() | (待填) | | | +| 804 | 1028 | "三商品全陈列(atk_up/hp_up/armor_up——无刷新无摇号)" | prods.size() == 3 | (待填) | | | +| 805 | 1030 | "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 | (待填) | | | +| 806 | 1033 | "购买 atk_up 受理(现价 6)" | runner.buy_product("atk_up") | (待填) | | | +| 807 | 1034 | "钱包扣现价 6(%d → %d)" ↩ % [gp0, runner.gp] | runner.gp == gp0 - 6 | (待填) | | | +| 808 | 1036 | "统一冒险加算区 atk +1(加算区住加算日记)" | runner.journal().bonus_total("atk") == 1 and runner.journal().bonus_total("max_hp") == 0 and runner.journal().bonus_total("armor_class") == 0 | (待填) | | | +| 809 | 1038 | "atk_up 二购现价 = 4+2+2=8(步长 2 × 已购 1 档,现价现算)" | int(by_id["atk_up"]["price"]) == 8 and int(by_id["atk_up"]["purchased"]) == 1 | (待填) | | | +| 810 | 1045 | "购买 hp_up 受理(现价 4)" | runner.buy_product("hp_up") | (待填) | | | +| 811 | 1046 | "钱包扣 4(%d)" ↩ % runner.gp | runner.gp == gp0 - 6 - 4 | (待填) | | | +| 812 | 1047 | "生命上限 +3(%d → %d——加算区 max_hp 轴)" ↩ % [mhp_before, runner.effective_max_hp()] | runner.effective_max_hp() == mhp_before + 3 | (待填) | | | +| 813 | 1048 | "当前生命同步 +3 封顶新上限(%d → %d)" ↩ % [hp_before, runner.hero_hp] | runner.hero_hp == mini(hp_before + 3, runner.effective_max_hp()) | (待填) | | | +| 814 | 1050 | "购买 armor_up 受理(现价 6)" | runner.buy_product("armor_up") | (待填) | | | +| 815 | 1051 | "AC = 10+1(armor_class 加算区轴,无百分比通道)" | runner.effective_ac() == int(_db.combat.hero.armor_class) + 1 | (待填) | | | +| 816 | 1066 | "余额不足购买被拒(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 | (待填) | | | +| 817 | 1068 | "离开商店 → 回选路口(候选 = 店出边 %s——零自动推进收口)" ↩ % str(shop_outs) | runner.continue_after_node() and runner.awaiting_choice() and _sorted_ids(runner.next_candidate_ids()) == _sorted_ids(shop_outs) | (待填) | | | +| 818 | 1071 | "主角攻击生效值 = 2+1(基础表值 + 加算区)" | runner.effective_atk() == int(_db.combat.hero.atk) + 1 | (待填) | | | +| 819 | 1072 | "选终点 Boss(句柄 %d)开战" ↩ % int(shop_outs[0]) | runner.pick_next_node(int(shop_outs[0])) and runner.current_battle() != null | (待填) | | | +| 820 | 1076 | "战斗开始参数 = 基础表值 + 加算区(atk %d = 2+1 / max_hp %d = %d+%d×3)" ↩ % [int(snap["hero"].get("atk", -1)), int(snap["hero"].get("max_hp", -1)), int(_db.combat.hero.max_hp), hp_buys] | int(snap["hero"].get("atk", -1)) == int(_db.combat.hero.atk) + 1 and int(snap["hero"].get("max_hp", -1)) == int(_db.combat.hero.max_hp) + 3 * hp_buys | (待填) | | | +| 821 | 1087 | "冒险收束(Boss 战终局:%s)" ↩ % String(fin_end.get("p", {}).get("result", "")) | fin_end.has("p") and String(fin_end["p"].get("result", "")) in ["victory", "defeat"] | (待填) | | | +| 822 | 1088 | "冒险收束钱包清零(局内货币生命周期终点——不入存档不入摘要)" | runner.gp == 0 | (待填) | | | ## S108 _smoke_shop_graph(行 1110–1167 · 9 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 832 | 1110 | "L%d 参数档缺行(商店编排断言无源可扫)" ↩ % L | false | (待填) | | | -| 833 | 1115 | "L%d 种子 %d 生成图为空(编排断言扫描面空转)" ↩ % [L, seed_val] | false | (待填) | | | -| 834 | 1141 | "编排断言扫描面非空(三关参数档 × 种子 %d–%d=图 %d 张·终点 %d·路径 %d)〔v5〕" ↩ % [SWEEP_SEED_FROM, SWEEP_SEED_TO - 1, graph_total, term_total, path_total] | graph_total == 3 * (SWEEP_SEED_TO - SWEEP_SEED_FROM) and term_total >= 3 and path_total >= 3 | (待填) | | | -| 835 | 1143 | "性质① 每个终点的直接前驱恰一家商店(入边一条且源型别=shop·违例 %d/%d 终点)〔v5〕" ↩ % [pred_bad, term_total] | pred_bad == 0 | (待填) | | | -| 836 | 1144 | "性质② 每图商店节点数 = 终点数(违例 %d/%d 图)〔v5〕" ↩ % [count_bad, graph_total] | count_bad == 0 | (待填) | | | -| 837 | 1145 | "性质③ 每条入口→终点路径恰穿一家商店(违例 %d/%d 条路径)〔v5〕" ↩ % [path_bad, path_total] | path_bad == 0 | (待填) | | | -| 838 | 1151 | "L%d 现行图终点 %d 个 = 商店 %d 个·入口→终点路径 %d 条全穿恰一店" ↩ % [L, cur_terms.size(), cur_shops.size(), _all_entry_terminal_paths(cur_nodes).size()] | cur_terms.size() == cur_shops.size() and not cur_terms.is_empty() and _all_entry_terminal_paths(cur_nodes).size() >= cur_terms.size() and _paths_all_one_shop(cur_nodes) | (待填) | | | -| 839 | 1163 | "L%d 现行图终点深度 %s 逐点 Boss 行集解析命中且落带 ≥ 终点深度" ↩ % [L, str(tdepths)] | hit_ok | (待填) | | | -| 840 | 1166 | "L1 现行图跨表覆盖(战斗/Boss 节点 %d 颗非空·敌侧违例 %d·板侧违例 %d)" ↩ % [int(l1_cov["nodes"]), int(l1_cov["enemy_bad"]), int(l1_cov["board_bad"])] | int(l1_cov["nodes"]) >= 1 and int(l1_cov["enemy_bad"]) == 0 and int(l1_cov["board_bad"]) == 0 | (待填) | | | +| 823 | 1110 | "L%d 参数档缺行(商店编排断言无源可扫)" ↩ % L | false | (待填) | | | +| 824 | 1115 | "L%d 种子 %d 生成图为空(编排断言扫描面空转)" ↩ % [L, seed_val] | false | (待填) | | | +| 825 | 1141 | "编排断言扫描面非空(三关参数档 × 种子 %d–%d=图 %d 张·终点 %d·路径 %d)〔v5〕" ↩ % [SWEEP_SEED_FROM, SWEEP_SEED_TO - 1, graph_total, term_total, path_total] | graph_total == 3 * (SWEEP_SEED_TO - SWEEP_SEED_FROM) and term_total >= 3 and path_total >= 3 | (待填) | | | +| 826 | 1143 | "性质① 每个终点的直接前驱恰一家商店(入边一条且源型别=shop·违例 %d/%d 终点)〔v5〕" ↩ % [pred_bad, term_total] | pred_bad == 0 | (待填) | | | +| 827 | 1144 | "性质② 每图商店节点数 = 终点数(违例 %d/%d 图)〔v5〕" ↩ % [count_bad, graph_total] | count_bad == 0 | (待填) | | | +| 828 | 1145 | "性质③ 每条入口→终点路径恰穿一家商店(违例 %d/%d 条路径)〔v5〕" ↩ % [path_bad, path_total] | path_bad == 0 | (待填) | | | +| 829 | 1151 | "L%d 现行图终点 %d 个 = 商店 %d 个·入口→终点路径 %d 条全穿恰一店" ↩ % [L, cur_terms.size(), cur_shops.size(), _all_entry_terminal_paths(cur_nodes).size()] | cur_terms.size() == cur_shops.size() and not cur_terms.is_empty() and _all_entry_terminal_paths(cur_nodes).size() >= cur_terms.size() and _paths_all_one_shop(cur_nodes) | (待填) | | | +| 830 | 1163 | "L%d 现行图终点深度 %s 逐点 Boss 行集解析命中且落带 ≥ 终点深度" ↩ % [L, str(tdepths)] | hit_ok | (待填) | | | +| 831 | 1166 | "L1 现行图跨表覆盖(战斗/Boss 节点 %d 颗非空·敌侧违例 %d·板侧违例 %d)" ↩ % [int(l1_cov["nodes"]), int(l1_cov["enemy_bad"]), int(l1_cov["board_bad"])] | int(l1_cov["nodes"]) >= 1 and int(l1_cov["enemy_bad"]) == 0 and int(l1_cov["board_bad"]) == 0 | (待填) | | | ## S109 _smoke_multi_terminal_settlement(行 1175–1222 · 5 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 841 | 1175 | "L2 现行图为多终点(Boss 终点 %s 两颗以上——大岔口承载面)" ↩ % str(boss_ids) | boss_ids.size() >= 2 | (待填) | | | -| 842 | 1187 | "L2 min/max 两路各打进各自终点 Boss(末句柄 %d / %d 互异且都在图 Boss 集 %s 内;两路 result=%s/%s)" ↩ % [min_last, max_last, str(boss_ids), String(run_min.get("result", "")), String(run_max.get("result", ""))] | String(run_min.get("result", "")) == "victory" and String(run_max.get("result", "")) == "victory" and boss_ids.has(min_last) and boss_ids.has(max_last) and min_last != max_last | (待填) | | | -| 843 | 1191 | "min 路未到 max 终点 %d、max 路未到 min 终点 %d(区域隔离——走 A 区不误兑付 B 区 Boss)" ↩ % [max_last, min_last] | not min_vis.has(max_last) and not max_vis.has(min_last) | (待填) | | | -| 844 | 1198 | "L2 全程驱动启动受理" | runner.start_level(2, 42) | (待填) | | | -| 845 | 1220 | "任一 Boss 胜即本关收束恰一次(count=%d·载荷 result=%s·node_id=%s 在图 Boss 集 %s 内)" ↩ % [int(fin["count"]), String(payload.get("result", "")), str(payload.get("node_id", -1)), str(boss_ids)] | int(fin["count"]) == 1 and String(payload.get("result", "")) == "victory" and boss_ids.has(int(payload.get("node_id", -1))) | (待填) | | | +| 832 | 1175 | "L2 现行图为多终点(Boss 终点 %s 两颗以上——大岔口承载面)" ↩ % str(boss_ids) | boss_ids.size() >= 2 | (待填) | | | +| 833 | 1187 | "L2 min/max 两路各打进各自终点 Boss(末句柄 %d / %d 互异且都在图 Boss 集 %s 内;两路 result=%s/%s)" ↩ % [min_last, max_last, str(boss_ids), String(run_min.get("result", "")), String(run_max.get("result", ""))] | String(run_min.get("result", "")) == "victory" and String(run_max.get("result", "")) == "victory" and boss_ids.has(min_last) and boss_ids.has(max_last) and min_last != max_last | (待填) | | | +| 834 | 1191 | "min 路未到 max 终点 %d、max 路未到 min 终点 %d(区域隔离——走 A 区不误兑付 B 区 Boss)" ↩ % [max_last, min_last] | not min_vis.has(max_last) and not max_vis.has(min_last) | (待填) | | | +| 835 | 1198 | "L2 全程驱动启动受理" | runner.start_level(2, 42) | (待填) | | | +| 836 | 1220 | "任一 Boss 胜即本关收束恰一次(count=%d·载荷 result=%s·node_id=%s 在图 Boss 集 %s 内)" ↩ % [int(fin["count"]), String(payload.get("result", "")), str(payload.get("node_id", -1)), str(boss_ids)] | int(fin["count"]) == 1 and String(payload.get("result", "")) == "victory" and boss_ids.has(int(payload.get("node_id", -1))) | (待填) | | | ## S110 _smoke_shop_pool(行 1257–1391 · 29 条 · 源 game/tests/unit/core/level_shop_economy_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 846 | 1257 | "池模式商店房场景可达(seed=%s)" ↩ % str(scen.get("s", -1)) | not scen.is_empty() | (待填) | | | -| 847 | 1265 | "进店抽 3(货架 %d 卡——同构交互)" ↩ % first_shelf_a.size() | first_shelf_a.size() == 3 | (待填) | | | -| 848 | 1271 | "刷新首价=基价(%d)" ↩ % cost0 | cost0 == int(_db.adventure.shop_refresh_cost_base) | (待填) | | | -| 849 | 1272 | "基础免费 0 次(Q2——天赋增益位后续添加)" | r8.shop_refresh_free_left() == 0 | (待填) | | | -| 850 | 1274 | "GP 刷新受理(gp=%d cost=%d)" ↩ % [gp0, cost0] | r8.shop_refresh() | (待填) | | | -| 851 | 1275 | "GP 刷新扣费(%d→%d)" ↩ % [gp0, int(r8.gp)] | int(r8.gp) == gp0 - cost0 | (待填) | | | -| 852 | 1276 | "刷新爬价 ×curve(%d)" ↩ % int(r8.shop_refresh_cost()) | int(r8.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.adventure.shop_refresh_cost_curve))) | (待填) | | | -| 853 | 1284 | "同种子同货架逐位(确定性重放承载——断点货架续玩语义)" | same_shelf | (待填) | | | -| 854 | 1294 | "relic 现价 = ceil(15.0×b^(关序−1))(读表与迁移前字面量逐位一致,%d)〔wd11〕" ↩ % relic_price_now | relic_price_now == relic_price_pre | (待填) | | | -| 855 | 1300 | "货架 kind 在封闭词表或保底 relic 卡(%s)" ↩ % ckind | is_relic or shelf_kinds.has(ckind) | (待填) | | | -| 856 | 1307 | "首购现价 = ceil(基价×b^(关序−1))(%s: %d = %d 推导)" ↩ % [ckind, int(c.get("price", -1)), expect_p] | int(c.get("price", -1)) == expect_p | (待填) | | | -| 857 | 1314 | "可购卡在架(GP %d——种子扫描保证首购空间)" ↩ % int(r8.gp) | not buy_c.is_empty() | (待填) | | | -| 858 | 1322 | "购买受理(pool 口——%s\|%s)" ↩ % [kind, tier] | r8.buy_pool_item(kind) | (待填) | | | -| 859 | 1323 | "钱包扣现价(%d → %d)" ↩ % [gp0, int(r8.gp)] | int(r8.gp) == gp0 - int(buy_c.get("price", 0)) | (待填) | | | -| 860 | 1324 | "加算区 +令牌档位件值(池件=奖励件本体——%s\|%s %+d)" ↩ % [kind, tier, int(_db.adventure.curio_value(kind, tier))] | a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.adventure.curio_value(kind, tier)) | (待填) | | | -| 861 | 1325 | "共享池账本 -1(跨渠道单点——taken %d → %d)" ↩ % [taken0, int(a8._curio.taken.get(kind, 0))] | int(a8._curio.taken.get(kind, 0)) == taken0 + 1 | (待填) | | | -| 862 | 1337 | "A 链爬价 = ceil(base×b^(关序−1)×A^1)(进店第 2 购——%d)" ↩ % expect_p2 | int(second.get("price", -1)) == expect_p2 | (待填) | | | -| 863 | 1338 | "同 visit 第二购受理(A 链软闸在价不在限)" | r8.buy_pool_item(String(second.get("kind_id", ""))) | (待填) | | | -| 864 | 1340 | "A 链爬价场景未构造(抬 GP 后货架仍无可购第二卡)——场景绑定失败" | false | (待填) | | | -| 865 | 1348 | "切边件零入店(40 次重抽零 gp/aux——负交易切边+三轴 doctrine)" | not cut_seen | (待填) | | | -| 866 | 1359 | "库存尽剔牌库(attack 令牌耗尽——池消耗即上限跨渠道共库 reward→shop 向)" | not attack_seen | (待填) | | | -| 867 | 1361 | "池空消费拒绝(handler 零副作用——买光即枯竭硬闸)" | not a8.stop().consume_pool_purchase("attack", "common") and a8.deck().pool_stock("attack") == stock_before | (待填) | | | -| 868 | 1364 | "商店消费口受理(停点件兑付单点同口——真单点库)〔拆件批 V3〕" | a8.stop().consume_pool_purchase("attack", "common") | (待填) | | | -| 869 | 1370 | "商店购买进奖励牌库摘牌(买一件=奖励面少一件——shop→reward 向共库)" | not reward_attack_seen | (待填) | | | -| 870 | 1375 | "离开商店 → 回选路口(货架态清零落档——零自动推进收口)" | r8.continue_after_node() and r8.awaiting_choice() | (待填) | | | -| 871 | 1377 | "断点扩键 shop_shelf 在案(离开店=空架合法态——P2 甲扩列)" | bp.has("shop_shelf") and (bp["shop_shelf"] as Array).is_empty() | (待填) | | | -| 872 | 1378 | "断点扩键 shop_visit_buys 在案(离开店清零——A 链输入不入档语义)" | bp.has("shop_visit_buys") and int(bp["shop_visit_buys"]) == 0 | (待填) | | | -| 873 | 1383 | "终局池读数六键出讫(run_adventure pool 面——四池种+T5 专长凭证+T6 heal 行,读数只锚一次载体)" | ps.size() == 6 | (待填) | | | -| 874 | 1391 | "终局池余量在域 [0,配额](%s)" ↩ % str(ps) | ps_ok | (待填) | | | +| 837 | 1257 | "池模式商店房场景可达(seed=%s)" ↩ % str(scen.get("s", -1)) | not scen.is_empty() | (待填) | | | +| 838 | 1265 | "进店抽 3(货架 %d 卡——同构交互)" ↩ % first_shelf_a.size() | first_shelf_a.size() == 3 | (待填) | | | +| 839 | 1271 | "刷新首价=基价(%d)" ↩ % cost0 | cost0 == int(_db.run.adventure.shop_refresh_cost_base) | (待填) | | | +| 840 | 1272 | "基础免费 0 次(Q2——天赋增益位后续添加)" | r8.shop_refresh_free_left() == 0 | (待填) | | | +| 841 | 1274 | "GP 刷新受理(gp=%d cost=%d)" ↩ % [gp0, cost0] | r8.shop_refresh() | (待填) | | | +| 842 | 1275 | "GP 刷新扣费(%d→%d)" ↩ % [gp0, int(r8.gp)] | int(r8.gp) == gp0 - cost0 | (待填) | | | +| 843 | 1276 | "刷新爬价 ×curve(%d)" ↩ % int(r8.shop_refresh_cost()) | int(r8.shop_refresh_cost()) == int(ceil(float(cost0) * float(_db.run.adventure.shop_refresh_cost_curve))) | (待填) | | | +| 844 | 1284 | "同种子同货架逐位(确定性重放承载——断点货架续玩语义)" | same_shelf | (待填) | | | +| 845 | 1294 | "relic 现价 = ceil(15.0×b^(关序−1))(读表与迁移前字面量逐位一致,%d)〔wd11〕" ↩ % relic_price_now | relic_price_now == relic_price_pre | (待填) | | | +| 846 | 1300 | "货架 kind 在封闭词表或保底 relic 卡(%s)" ↩ % ckind | is_relic or shelf_kinds.has(ckind) | (待填) | | | +| 847 | 1307 | "首购现价 = ceil(基价×b^(关序−1))(%s: %d = %d 推导)" ↩ % [ckind, int(c.get("price", -1)), expect_p] | int(c.get("price", -1)) == expect_p | (待填) | | | +| 848 | 1314 | "可购卡在架(GP %d——种子扫描保证首购空间)" ↩ % int(r8.gp) | not buy_c.is_empty() | (待填) | | | +| 849 | 1322 | "购买受理(pool 口——%s\|%s)" ↩ % [kind, tier] | r8.buy_pool_item(kind) | (待填) | | | +| 850 | 1323 | "钱包扣现价(%d → %d)" ↩ % [gp0, int(r8.gp)] | int(r8.gp) == gp0 - int(buy_c.get("price", 0)) | (待填) | | | +| 851 | 1324 | "加算区 +令牌档位件值(池件=奖励件本体——%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)) | (待填) | | | +| 852 | 1325 | "共享池账本 -1(跨渠道单点——taken %d → %d)" ↩ % [taken0, int(a8.deck().taken.get(kind, 0))] | int(a8.deck().taken.get(kind, 0)) == taken0 + 1 | (待填) | | | +| 853 | 1337 | "A 链爬价 = ceil(base×b^(关序−1)×A^1)(进店第 2 购——%d)" ↩ % expect_p2 | int(second.get("price", -1)) == expect_p2 | (待填) | | | +| 854 | 1338 | "同 visit 第二购受理(A 链软闸在价不在限)" | r8.buy_pool_item(String(second.get("kind_id", ""))) | (待填) | | | +| 855 | 1340 | "A 链爬价场景未构造(抬 GP 后货架仍无可购第二卡)——场景绑定失败" | false | (待填) | | | +| 856 | 1348 | "切边件零入店(40 次重抽零 gp/aux——负交易切边+三轴 doctrine)" | not cut_seen | (待填) | | | +| 857 | 1359 | "库存尽剔牌库(attack 令牌耗尽——池消耗即上限跨渠道共库 reward→shop 向)" | not attack_seen | (待填) | | | +| 858 | 1361 | "池空消费拒绝(handler 零副作用——买光即枯竭硬闸)" | not a8.stop().consume_pool_purchase("attack", "common") and a8.deck().pool_stock("attack") == stock_before | (待填) | | | +| 859 | 1364 | "商店消费口受理(停点件兑付单点同口——真单点库)〔拆件批 V3〕" | a8.stop().consume_pool_purchase("attack", "common") | (待填) | | | +| 860 | 1370 | "商店购买进奖励牌库摘牌(买一件=奖励面少一件——shop→reward 向共库)" | not reward_attack_seen | (待填) | | | +| 861 | 1375 | "离开商店 → 回选路口(货架态清零落档——零自动推进收口)" | r8.continue_after_node() and r8.awaiting_choice() | (待填) | | | +| 862 | 1377 | "断点扩键 shop_shelf 在案(离开店=空架合法态——P2 甲扩列)" | bp.has("shop_shelf") and (bp["shop_shelf"] as Array).is_empty() | (待填) | | | +| 863 | 1378 | "断点扩键 shop_visit_buys 在案(离开店清零——A 链输入不入档语义)" | bp.has("shop_visit_buys") and int(bp["shop_visit_buys"]) == 0 | (待填) | | | +| 864 | 1383 | "终局池读数六键出讫(run_adventure pool 面——四池种+T5 专长凭证+T6 heal 行,读数只锚一次载体)" | ps.size() == 6 | (待填) | | | +| 865 | 1391 | "终局池余量在域 [0,配额](%s)" ↩ % str(ps) | ps_ok | (待填) | | | ## S111 _test_positive(行 46–56 · 3 条 · 源 game/tests/unit/core/level_table_validate_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 875 | 46 | "四检正例:多入口多终点图零错误〔v1〕" | _errs(multi).is_empty() | (待填) | | | -| 876 | 53 | "四检正例:含岔口单终点图零错误〔v1〕" | _errs(forked).is_empty() | (待填) | | | -| 877 | 56 | "四检正例:单链退化图零错误(不设必有岔口)〔v1〕" | _errs(chain).is_empty() | (待填) | | | +| 866 | 46 | "四检正例:多入口多终点图零错误〔v1〕" | _errs(multi).is_empty() | (待填) | | | +| 867 | 53 | "四检正例:含岔口单终点图零错误〔v1〕" | _errs(forked).is_empty() | (待填) | | | +| 868 | 56 | "四检正例:单链退化图零错误(不设必有岔口)〔v1〕" | _errs(chain).is_empty() | (待填) | | | ## S112 _test_negative(行 63–89 · 11 条 · 源 game/tests/unit/core/level_table_validate_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 878 | 63 | "负例:终点非 Boss 命中「终点恒为 Boss」〔v1〕" | _has(_errs(non_boss_end), "structural", "终点 1 非 Boss") | (待填) | | | -| 879 | 66 | "负例:Boss 带出边被拒〔v1〕" | _has(_errs(boss_out), "structural", "Boss 节点 1 带出边") | (待填) | | | -| 880 | 68 | "负例:回边(目标深度不严格大于源)命中前进方向判据〔v1〕" | _has(_errs(back_edge), "structural", "违反前进方向") | (待填) | | | -| 881 | 71 | "负例:环(2→1 回边)被前进方向拦下〔v1〕" | _has(_errs(cycle), "structural", "违反前进方向") | (待填) | | | -| 882 | 77 | "判据②正证:两独立链(双入口双终点)不误伤·全员可达〔v1〕" | _errs(two_components).is_empty() | (待填) | | | -| 883 | 79 | "判据②兜底面:自环先被前进方向拦下(④ 与 ② 互斥的实证)〔v1〕" | _has(_errs(self_loop), "structural", "违反前进方向") | (待填) | | | -| 884 | 82 | "负例:悬空出边报 dangling_next〔v1〕" | _has(_errs(dangling), "dangling_next") | (待填) | | | -| 885 | 84 | "负例:句柄重复报 dup_key〔v1〕" | _has(_errs(dup), "dup_key") | (待填) | | | -| 886 | 86 | "负例:battle 节点缺深度被良构基底拒〔v1〕" | _has(_errs(no_depth), "structural", "缺深度") | (待填) | | | -| 887 | 87 | "负例:空节点集被拒〔v1〕" | _has(_errs([]), "structural", "图节点集为空") | (待填) | | | -| 888 | 89 | "负例:类型出域报 enum〔v1〕" | _has(_errs(bad_type), "enum") | (待填) | | | +| 869 | 63 | "负例:终点非 Boss 命中「终点恒为 Boss」〔v1〕" | _has(_errs(non_boss_end), "structural", "终点 1 非 Boss") | (待填) | | | +| 870 | 66 | "负例:Boss 带出边被拒〔v1〕" | _has(_errs(boss_out), "structural", "Boss 节点 1 带出边") | (待填) | | | +| 871 | 68 | "负例:回边(目标深度不严格大于源)命中前进方向判据〔v1〕" | _has(_errs(back_edge), "structural", "违反前进方向") | (待填) | | | +| 872 | 71 | "负例:环(2→1 回边)被前进方向拦下〔v1〕" | _has(_errs(cycle), "structural", "违反前进方向") | (待填) | | | +| 873 | 77 | "判据②正证:两独立链(双入口双终点)不误伤·全员可达〔v1〕" | _errs(two_components).is_empty() | (待填) | | | +| 874 | 79 | "判据②兜底面:自环先被前进方向拦下(④ 与 ② 互斥的实证)〔v1〕" | _has(_errs(self_loop), "structural", "违反前进方向") | (待填) | | | +| 875 | 82 | "负例:悬空出边报 dangling_next〔v1〕" | _has(_errs(dangling), "dangling_next") | (待填) | | | +| 876 | 84 | "负例:句柄重复报 dup_key〔v1〕" | _has(_errs(dup), "dup_key") | (待填) | | | +| 877 | 86 | "负例:battle 节点缺深度被良构基底拒〔v1〕" | _has(_errs(no_depth), "structural", "缺深度") | (待填) | | | +| 878 | 87 | "负例:空节点集被拒〔v1〕" | _has(_errs([]), "structural", "图节点集为空") | (待填) | | | +| 879 | 89 | "负例:类型出域报 enum〔v1〕" | _has(_errs(bad_type), "enum") | (待填) | | | ## S113 _test_scope_not_extended(行 97–102 · 3 条 · 源 game/tests/unit/core/level_table_validate_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 889 | 97 | "范围反证:无商店图不在加载校验内报错(商店位由冒烟断言把关)〔v1〕" | _errs(no_shop).is_empty() | (待填) | | | -| 890 | 99 | "范围反证:双入口合法(入口唯一判据已随模板池退役,个数归产出侧)〔v1〕" | _errs(two_entries).is_empty() | (待填) | | | -| 891 | 102 | "范围反证:节点缺 next 字段仍被良构基底拒〔v1〕" | _has(_errs(missing_key), "structural", "缺字段 next") | (待填) | | | +| 880 | 97 | "范围反证:无商店图不在加载校验内报错(商店位由冒烟断言把关)〔v1〕" | _errs(no_shop).is_empty() | (待填) | | | +| 881 | 99 | "范围反证:双入口合法(入口唯一判据已随模板池退役,个数归产出侧)〔v1〕" | _errs(two_entries).is_empty() | (待填) | | | +| 882 | 102 | "范围反证:节点缺 next 字段仍被良构基底拒〔v1〕" | _has(_errs(missing_key), "structural", "缺字段 next") | (待填) | | | ## S114 _meta_pricing_and_buy(行 138–151 · 4 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 892 | 138 | "树购买受理(Lv 0→1、余额 %d−%d)" ↩ % [bal0, price0] | mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 1 and sv.keystone_balance() == bal0 - price0 | (待填) | | | -| 893 | 141 | "购买持久(reload 后级与余额不变)" | sv_r.tree_level("sp_hp") == 1 and sv_r.keystone_balance() == bal0 - price0 | (待填) | | | -| 894 | 148 | "余额不足 false 零副作用(0 余额购买拒绝——三域零变化〔天赋树批 V3〕)" | not mp_dry.tree_buy("sp_hp") and sv_dry.keystone_balance() == 0 and sv_dry.tree_nodes() == tn_before and sv_dry.fork_sides() == fs_before | (待填) | | | -| 895 | 151 | "未知节点 false 零副作用(词表外出域)" | not mp_dry.tree_buy("no_such_node") | (待填) | | | +| 883 | 138 | "树购买受理(Lv 0→1、余额 %d−%d)" ↩ % [bal0, price0] | mp.tree_buy("sp_hp") and sv.meta_state.tree_state().nodes.get("sp_hp", 0) == 1 and sv.meta_state.keystone_balance() == bal0 - price0 | (待填) | | | +| 884 | 141 | "购买持久(reload 后级与余额不变)" | sv_r.meta_state.tree_state().nodes.get("sp_hp", 0) == 1 and sv_r.meta_state.keystone_balance() == bal0 - price0 | (待填) | | | +| 885 | 148 | "余额不足 false 零副作用(0 余额购买拒绝——三域零变化〔天赋树批 V3〕)" | not mp_dry.tree_buy("sp_hp") and sv_dry.meta_state.keystone_balance() == 0 and sv_dry.meta_state.tree_state().nodes == tn_before and sv_dry.meta_state.tree_state().fork_sides == fs_before | (待填) | | | +| 886 | 151 | "未知节点 false 零副作用(词表外出域)" | not mp_dry.tree_buy("no_such_node") | (待填) | | | -## S115 _meta_settle_and_workshop(行 168–341 · 42 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) +## S115 _meta_settle_and_workshop(行 168–342 · 42 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 896 | 168 | "新档首通 L1:boss_drops[0] {first:true, amount=first_kill}(验收5——明细随摘要归档)" | bool(drop1.get("first", false)) and int(drop1.get("amount", 0)) == int(row1.get("first_kill", -1)) and int(drop1.get("level", 0)) == 1 | (待填) | | | -| 897 | 171 | "明细 total = keystone_total(逐杀掉落全源——无二次派生加账)" | int(d1.get("total", 0)) == int(row1.get("first_kill", 0)) | (待填) | | | -| 898 | 173 | "余额 = 累计入账(逐杀掉落合计——无重复加账)" | sv.keystone_balance() == int(row1.get("first_kill", 0)) | (待填) | | | -| 899 | 181 | "再通 L1:{first:false, amount=repeat_kill}(首杀标记置位后转小额)" | not bool(drop2.get("first", true)) and int(drop2.get("amount", 0)) == int(row1.get("repeat_kill", -1)) | (待填) | | | -| 900 | 187 | "重复杀第三轮恒定不递减(验收5——无衰减曲线)" | not bool(d3["boss_drops"][0].get("first", true)) and int(d3["boss_drops"][0].get("amount", 0)) == int(row1.get("repeat_kill", -1)) | (待填) | | | -| 901 | 195 | "两关完成逐关归因:L1 重复档 + L2 首杀档(first_kill 金额)" | not bool(d4["boss_drops"][0].get("first", true)) and bool(d4["boss_drops"][1].get("first", false)) and int(d4["boss_drops"][1].get("amount", 0)) == int(row2.get("first_kill", -1)) | (待填) | | | -| 902 | 203 | "败北零关卡完成摘要:零入账零标记(钥石只增不减——验收1;败北局 clear_counts 不变)" | dz["boss_drops"].is_empty() and sv.keystone_balance() == bal_before and sv.clear_count(1) == cc1_before | (待填) | | | -| 903 | 210 | "条件件误购拒绝(购买件专属——契约 8 分配面)〔秘宝批 V3 反例〕" | not mp_u.buy_treasure_unlock("sky_eye") and not sv_u.item_unlocked("sky_eye") and not mp_u.buy_treasure_unlock("ranged_pick") | (待填) | | | -| 904 | 213 | "余额不足购买拒绝且零副作用(余额 10 < 价 25)〔秘宝批 V3〕" | not mp_u.buy_treasure_unlock("twin_loader") and sv_u.keystone_balance() == 10 | (待填) | | | -| 905 | 215 | "钥石购买解锁:余额足→解锁态落档+余额扣减正确(价 10)〔秘宝批 V3〕" | mp_u.buy_treasure_unlock("morning_order") and sv_u.item_unlocked("morning_order") and sv_u.keystone_balance() == 0 | (待填) | | | -| 906 | 218 | "已解锁重复购买拒绝〔秘宝批 V3〕" | not mp_u.buy_treasure_unlock("morning_order") | (待填) | | | -| 907 | 220 | "条件未达不解锁(远程镐=冒险成功 1 次)〔秘宝批 V4〕" | mp_u.refresh_treasure_unlocks().is_empty() and not sv_u.item_unlocked("ranged_pick") | (待填) | | | -| 908 | 223 | "远程镐=adventure_cleared≥1 达标恰解锁一次+重复刷新幂等(C6:冒险成功=打通整局冒险)〔秘宝批 V4〕" | (mp_u.refresh_treasure_unlocks() as Array).has("ranged_pick") and sv_u.item_unlocked("ranged_pick") and mp_u.refresh_treasure_unlocks().is_empty() | (待填) | | | -| 909 | 227 | "刷新执照=boss_kill_count≥3(阈值读 unlock_requires 行值不硬编)〔秘宝批 V4〕" | (mp_u.refresh_treasure_unlocks() as Array).has("refresh_license") | (待填) | | | -| 910 | 230 | "天眼单条件不足不解锁(AND 语义——cleared 3 达 boss 4 未达)〔秘宝批 V4〕" | not sv_u.item_unlocked("sky_eye") | (待填) | | | -| 911 | 233 | "天眼双条件 AND(冒险成功≥3 且 Boss≥4)达标解锁〔秘宝批 V4〕" | (mp_u.refresh_treasure_unlocks() as Array).has("sky_eye") | (待填) | | | -| 912 | 236 | "信标网络=真视类工具累计使用≥10(行为计数新域——use_treasure 受理成功累计)〔秘宝批 V4〕" | (mp_u.refresh_treasure_unlocks() as Array).has("beacon_network") | (待填) | | | -| 913 | 241 | "解锁态持久投影一致(reload 面同源读数;5 件=购买 1+条件 4)〔秘宝批 V4〕" | r_n == 5 | (待填) | | | -| 914 | 244 | "摘要最新在前 + 元素含掉落明细副本(复盘可归因——验收5)" | rs.size() >= 5 and String(rs[0].get("result", "")) != "" and rs[0].has("boss_drops") and (rs[1] as Dictionary).has("keystone_drops") and int(rs[0].get("master_seed", -1)) == 15 | (待填) | | | -| 915 | 253 | "摘要 cap 10 且最新在前(12 连 settle 截断后 seed 11 居首)" | sv_c.recent_summaries().size() == 10 and int(sv_c.recent_summaries()[0].get("master_seed", -1)) == 11 | (待填) | | | -| 916 | 258 | "白板档同调位求值 = 0(0 起步裁决)且 Y=0(未购无刷新能力——P1 甲初始 0 同构)" | Attunement.slots(_db, sv_a) == 0 and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 0 | (待填) | | | -| 917 | 260 | "满位禁选(Slots 0 时装配拒绝;兼未解锁拒绝前置——直置解锁后下方装配)〔秘宝批 V5〕" | mp_a.toggle_attunement("morning_order") == false | (待填) | | | -| 918 | 265 | "Y 升级购买入账(Lv1 → 每冒险 +1 次刷新)" | mp_a.buy_attune_refresh() and sv_a.attune_refresh_level() == 1 and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 1 | (待填) | | | -| 919 | 270 | "完成 1 关 → Slots 恒 0(旧「按击杀 Boss 次数自动涨」条件已删——槽位唯一来源=树点亮)且 Boss 击杀总数=1(计数域保留)〔天赋树批 V14〕" | Attunement.slots(_db, sv_a) == 0 and sv_a.boss_kill_count() == 1 | (待填) | | | -| 920 | 273 | "树上点亮同调位 1 → Slots=1(槽位收编树节点——Q2 裁甲)" | Attunement.slots(_db, sv_a) == 1 | (待填) | | | -| 921 | 275 | "稀有件入普通格拒绝(件档 rank 3 > 装入位序 1——契约 2 档位格校验)〔秘宝批 V5〕" | not mp_a.toggle_attunement("sky_eye") | (待填) | | | -| 922 | 277 | "装配持久写入 + 满位禁选(Slots 1 装第二件拒绝)〔秘宝批 V5 换主语〕" | mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("twin_loader") == false | (待填) | | | -| 923 | 279 | "卸下恒可再装配(自卸自装——营地换装=冒险开始前合法面,装配不可变只锁运行中)〔秘宝批换主语〕" | mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("morning_order") | (待填) | | | -| 924 | 284 | "装配关系读回持久(reload 后投影一致——写新件 id)〔秘宝批 V15〕" | (mp_a2.attune_camp_status()["equipped"] as Array).has("morning_order") | (待填) | | | -| 925 | 288 | "充能重置场景:装配冒险启动(seed 12)〔秘宝批 V8 换主语〕" | a12.start_adventure(12, {"attunement": ["incense_stick", "sky_eye"]}) | (待填) | | | -| 926 | 290 | "充能按装备 active_charged 件种行(新目录表值:线香 battle 级 1 次/天眼 level 级 1 次——周期重置点归 V8 逐形态面;读数=行派生)" | a12._loadout.charges_left("incense_stick", a12.runner().treasure_inventory) == 1 and a12._loadout.charges_left("sky_eye", a12.runner().treasure_inventory) == 1 | (待填) | | | -| 927 | 296 | "刷新双计数器——X 优先消耗(x1y2 → x0y2)" | a12.stop().refresh_curio() and a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank()._refresh_y == 2 | (待填) | | | -| 928 | 298 | "刷新双计数器——X 耗尽转 Y 池、池竭拒绝(x0y2 → y0 → 第四次 false 零副作用)" | a12.stop().refresh_curio() and a12.stop().refresh_curio() and a12.refresh_bank()._refresh_y == 0 and a12.stop().refresh_curio() == false | (待填) | | | -| 929 | 302 | "刷新 X 每碰三选一重置(不结转;新目录无 bonus_refresh 行源——X 机制保留恒 0 空转,存量加仓走 _refresh_stock=刷新练习 V8)〔秘宝批换代〕" | a12.refresh_bank()._refresh_x == 0 and a12.refresh_bank().free_left() == 0 | (待填) | | | -| 930 | 313 | "开局传参夹具前置:Fork 侧 a 激活 rule 键非空 + Y=Lv2 行表值 2(防断言空转)" | rules_e.has("posture_first_strike") and mp_e.attune_refresh_charges() == 2 | (待填) | | | -| 931 | 317 | "主菜单开新冒险参数面:同调装配 = 存档装配集(营地装了什么开局就带什么)" | (p_new.get("attunement", []) as Array) == (sv_e.attunement_equipped() as Array) | (待填) | | | -| 932 | 319 | "主菜单开新冒险参数面:Y 刷新池 = 当前等级行表值(营地升几档开局就给几次)" | int(p_new.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges() | (待填) | | | -| 933 | 321 | "主菜单开新冒险参数面:树 rule 键集 = Fork 激活侧 effects(点哪侧带哪侧)" | (p_new.get("tree_rules", []) as Array) == rules_e | (待填) | | | -| 934 | 323 | "主菜单开新冒险参数面:入口特有键(treasure_unlocked/shop_mode)随 extra 原样并入" | bool((p_new.get("treasure_unlocked", {}) as Dictionary).get("wide_scout_band", false)) and String(p_new.get("shop_mode", "")) == "pool" | (待填) | | | -| 935 | 327 | "主菜单继续上局参数面:三键与开新冒险同源同值(续玩不吃白板开局)" | (p_resume.get("attunement", []) as Array) == (sv_e.attunement_equipped() as Array) and int(p_resume.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges() and (p_resume.get("tree_rules", []) as Array) == rules_e | (待填) | | | -| 936 | 338 | "继续上局受理面:resume_adventure 受理三键参数面(尾裁微批b 受理闭环——受理不再吃白板)" | adr_res.resume_adventure(bp_min, p_resume) | (待填) | | | -| 937 | 340 | "继续上局受理面:tree_rules 随参数面进 runner._tree_rules(消费面 set_tree_rules 下传现成)" | (adr_res._tree_rules as Array) == rules_e | (待填) | | | +| 887 | 168 | "新档首通 L1:boss_drops[0] {first:true, amount=first_kill}(验收5——明细随摘要归档)" | bool(drop1.get("first", false)) and int(drop1.get("amount", 0)) == int(row1.get("first_kill", -1)) and int(drop1.get("level", 0)) == 1 | (待填) | | | +| 888 | 171 | "明细 total = keystone_total(逐杀掉落全源——无二次派生加账)" | int(d1.get("total", 0)) == int(row1.get("first_kill", 0)) | (待填) | | | +| 889 | 173 | "余额 = 累计入账(逐杀掉落合计——无重复加账)" | sv.meta_state.keystone_balance() == int(row1.get("first_kill", 0)) | (待填) | | | +| 890 | 181 | "再通 L1:{first:false, amount=repeat_kill}(首杀标记置位后转小额)" | not bool(drop2.get("first", true)) and int(drop2.get("amount", 0)) == int(row1.get("repeat_kill", -1)) | (待填) | | | +| 891 | 187 | "重复杀第三轮恒定不递减(验收5——无衰减曲线)" | not bool(d3["boss_drops"][0].get("first", true)) and int(d3["boss_drops"][0].get("amount", 0)) == int(row1.get("repeat_kill", -1)) | (待填) | | | +| 892 | 195 | "两关完成逐关归因:L1 重复档 + L2 首杀档(first_kill 金额)" | not bool(d4["boss_drops"][0].get("first", true)) and bool(d4["boss_drops"][1].get("first", false)) and int(d4["boss_drops"][1].get("amount", 0)) == int(row2.get("first_kill", -1)) | (待填) | | | +| 893 | 203 | "败北零关卡完成摘要:零入账零标记(钥石只增不减——验收1;败北局 clear_counts 不变)" | dz["boss_drops"].is_empty() and sv.meta_state.keystone_balance() == bal_before and sv.meta_state.career_state().clear_counts.get(str(1), 0) == cc1_before | (待填) | | | +| 894 | 210 | "条件件误购拒绝(购买件专属——契约 8 分配面)〔秘宝批 V3 反例〕" | not mp_u.buy_treasure_unlock("sky_eye") and not sv_u.meta_state.item_unlocked("sky_eye") and not mp_u.buy_treasure_unlock("ranged_pick") | (待填) | | | +| 895 | 213 | "余额不足购买拒绝且零副作用(余额 10 < 价 25)〔秘宝批 V3〕" | not mp_u.buy_treasure_unlock("twin_loader") and sv_u.meta_state.keystone_balance() == 10 | (待填) | | | +| 896 | 215 | "钥石购买解锁:余额足→解锁态落档+余额扣减正确(价 10)〔秘宝批 V3〕" | mp_u.buy_treasure_unlock("morning_order") and sv_u.meta_state.item_unlocked("morning_order") and sv_u.meta_state.keystone_balance() == 0 | (待填) | | | +| 897 | 218 | "已解锁重复购买拒绝〔秘宝批 V3〕" | not mp_u.buy_treasure_unlock("morning_order") | (待填) | | | +| 898 | 220 | "条件未达不解锁(远程镐=冒险成功 1 次)〔秘宝批 V4〕" | mp_u.refresh_treasure_unlocks().is_empty() and not sv_u.meta_state.item_unlocked("ranged_pick") | (待填) | | | +| 899 | 223 | "远程镐=adventure_cleared≥1 达标恰解锁一次+重复刷新幂等(C6:冒险成功=打通整局冒险)〔秘宝批 V4〕" | (mp_u.refresh_treasure_unlocks() as Array).has("ranged_pick") and sv_u.meta_state.item_unlocked("ranged_pick") and mp_u.refresh_treasure_unlocks().is_empty() | (待填) | | | +| 900 | 227 | "刷新执照=boss_kill_count≥3(阈值读 unlock_requires 行值不硬编)〔秘宝批 V4〕" | (mp_u.refresh_treasure_unlocks() as Array).has("refresh_license") | (待填) | | | +| 901 | 230 | "天眼单条件不足不解锁(AND 语义——cleared 3 达 boss 4 未达)〔秘宝批 V4〕" | not sv_u.meta_state.item_unlocked("sky_eye") | (待填) | | | +| 902 | 233 | "天眼双条件 AND(冒险成功≥3 且 Boss≥4)达标解锁〔秘宝批 V4〕" | (mp_u.refresh_treasure_unlocks() as Array).has("sky_eye") | (待填) | | | +| 903 | 236 | "信标网络=真视类工具累计使用≥10(行为计数新域——use_treasure 受理成功累计)〔秘宝批 V4〕" | (mp_u.refresh_treasure_unlocks() as Array).has("beacon_network") | (待填) | | | +| 904 | 241 | "解锁态持久投影一致(reload 面同源读数;5 件=购买 1+条件 4)〔秘宝批 V4〕" | r_n == 5 | (待填) | | | +| 905 | 244 | "摘要最新在前 + 元素含掉落明细副本(复盘可归因——验收5)" | rs.size() >= 5 and String(rs[0].get("result", "")) != "" and rs[0].has("boss_drops") and (rs[1] as Dictionary).has("keystone_drops") and int(rs[0].get("master_seed", -1)) == 15 | (待填) | | | +| 906 | 253 | "摘要 cap 10 且最新在前(12 连 settle 截断后 seed 11 居首)" | sv_c.meta_state.career_state().recent_summaries.size() == 10 and int(sv_c.meta_state.career_state().recent_summaries[0].get("master_seed", -1)) == 11 | (待填) | | | +| 907 | 258 | "白板档同调位求值 = 0(0 起步裁决)且 Y=0(未购无刷新能力——P1 甲初始 0 同构)" | Attunement.slots(_db, sv_a) == 0 and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 0 | (待填) | | | +| 908 | 260 | "满位禁选(Slots 0 时装配拒绝;兼未解锁拒绝前置——直置解锁后下方装配)〔秘宝批 V5〕" | mp_a.toggle_attunement("morning_order") == false | (待填) | | | +| 909 | 265 | "Y 升级购买入账(Lv1 → 每冒险 +1 次刷新)" | mp_a.buy_attune_refresh() and sv_a.meta_state.attune_state().refresh_level == 1 and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 1 | (待填) | | | +| 910 | 270 | "完成 1 关 → Slots 恒 0(旧「按击杀 Boss 次数自动涨」条件已删——槽位唯一来源=树点亮)且 Boss 击杀总数=1(计数域保留)〔天赋树批 V14〕" | Attunement.slots(_db, sv_a) == 0 and sv_a.meta_state.career_state().boss_kill_count == 1 | (待填) | | | +| 911 | 273 | "树上点亮同调位 1 → Slots=1(槽位收编树节点——Q2 裁甲)" | Attunement.slots(_db, sv_a) == 1 | (待填) | | | +| 912 | 275 | "稀有件入普通格拒绝(件档 rank 3 > 装入位序 1——契约 2 档位格校验)〔秘宝批 V5〕" | not mp_a.toggle_attunement("sky_eye") | (待填) | | | +| 913 | 277 | "装配持久写入 + 满位禁选(Slots 1 装第二件拒绝)〔秘宝批 V5 换主语〕" | mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("twin_loader") == false | (待填) | | | +| 914 | 279 | "卸下恒可再装配(自卸自装——营地换装=冒险开始前合法面,装配不可变只锁运行中)〔秘宝批换主语〕" | mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("morning_order") | (待填) | | | +| 915 | 284 | "装配关系读回持久(reload 后投影一致——写新件 id)〔秘宝批 V15〕" | (mp_a2.attune_camp_status()["equipped"] as Array).has("morning_order") | (待填) | | | +| 916 | 288 | "充能重置场景:装配冒险启动(seed 12)〔秘宝批 V8 换主语〕" | a12.start_adventure(12, {"attunement": ["incense_stick", "sky_eye"]}) | (待填) | | | +| 917 | 290 | "充能按装备 active_charged 件种行(新目录表值:线香 battle 级 1 次/天眼 level 级 1 次——周期重置点归 V8 逐形态面;读数=行派生)" | a12._loadout.charges_left("incense_stick", a12.runner().treasure_inventory) == 1 and a12._loadout.charges_left("sky_eye", a12.runner().treasure_inventory) == 1 | (待填) | | | +| 918 | 296 | "刷新双计数器——X 优先消耗(x1y2 → x0y2)" | a12.stop().refresh_curio() and a12.refresh_bank().refresh_y() == 2 and a12.refresh_bank().free_left() == 2 + a12.refresh_bank().refresh_stock() | (待填) | | | +| 919 | 298 | "刷新双计数器——X 耗尽转 Y 池、池竭拒绝(x0y2 → y0 → 第四次 false 零副作用)" | a12.stop().refresh_curio() and a12.stop().refresh_curio() and a12.refresh_bank().refresh_y() == 0 and a12.stop().refresh_curio() == false | (待填) | | | +| 920 | 302 | "刷新 X 每碰三选一重置(不结转;新目录无 bonus_refresh 行源——X 机制保留恒 0 空转,存量加仓走 _refresh_stock=刷新练习 V8)〔秘宝批换代〕" | a12.refresh_bank().free_left() == 0 | (待填) | | | +| 921 | 313 | "开局传参夹具前置:Fork 侧 a 激活 rule 键非空 + Y=Lv2 行表值 2(防断言空转)" | rules_e.has("posture_first_strike") and mp_e.attune_refresh_charges() == 2 | (待填) | | | +| 922 | 317 | "主菜单开新冒险参数面:同调装配 = 存档装配集(营地装了什么开局就带什么)" | (p_new.get("attunement", []) as Array) == (sv_e.meta_state.attune_state().equipped as Array) | (待填) | | | +| 923 | 319 | "主菜单开新冒险参数面:Y 刷新池 = 当前等级行表值(营地升几档开局就给几次)" | int(p_new.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges() | (待填) | | | +| 924 | 321 | "主菜单开新冒险参数面:树 rule 键集 = Fork 激活侧 effects(点哪侧带哪侧)" | (p_new.get("tree_rules", []) as Array) == rules_e | (待填) | | | +| 925 | 323 | "主菜单开新冒险参数面:入口特有键(treasure_unlocked/shop_mode)随 extra 原样并入" | bool((p_new.get("treasure_unlocked", {}) as Dictionary).get("wide_scout_band", false)) and String(p_new.get("shop_mode", "")) == "pool" | (待填) | | | +| 926 | 327 | "主菜单继续上局参数面:三键与开新冒险同源同值(续玩不吃白板开局)" | (p_resume.get("attunement", []) as Array) == (sv_e.meta_state.attune_state().equipped as Array) and int(p_resume.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges() and (p_resume.get("tree_rules", []) as Array) == rules_e | (待填) | | | +| 927 | 338 | "继续上局受理面:resume_adventure 受理三键参数面(尾裁微批b 受理闭环——受理不再吃白板)" | adr_res.resume_adventure(bp_min, p_resume) | (待填) | | | +| 928 | 340 | "继续上局受理面: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) | (待填) | | | -## S116 _meta_injection(行 353–383 · 10 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) +## S116 _meta_injection(行 354–384 · 10 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 938 | 353 | "生命淬炼 2 级 → hero_override().max_hp == Σ 节点 effects×等级(验收4 换代〔天赋树批 V12〕)" | int(mp.hero_override().get("max_hp", -1)) == 2 * int(_db.tree_node("sp_hp").get("effects", {}).get("max_hp", 0)) | (待填) | | | -| 939 | 355 | "攻/AC 树零来源(契约 9——树表零 atk/armor_class 行,override 二轴恒 0+预留位 0)" | int(mp.hero_override().get("atk", -1)) == 0 and int(mp.hero_override().get("armor_class", -1)) == 0 | (待填) | | | -| 940 | 357 | "命中线在账未注入(契约 6——override 无 hit 字段〔天赋树批 V13〕)" | not mp.hero_override().has("hit") | (待填) | | | -| 941 | 360 | "注入参数冒险启动(start_adventure params 通道)" | adr.start_adventure(920251, {"hero_override": mp.hero_override()}) | (待填) | | | -| 942 | 364 | "首战 hero_atk = 表值 + 永久强化 flat(begin_battle 同表达式——下一局能用到强化)" | int(r.effective_atk()) == int(_db.hero.atk) + int(ov.get("atk", 0)) | (待填) | | | -| 943 | 366 | "首战 hero_max_hp 与开局生命账本 = 表值 + 注入档" | int(r.effective_max_hp()) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)) and int(adr.stats().hero_hp) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)) | (待填) | | | -| 944 | 371 | "白板冒险启动(无 params 实参——B1 空参数路径)" | adr_w.start_adventure(920251) | (待填) | | | -| 945 | 374 | "零强化参数冒险启动(新档 hero_override 全零)" | adr_z.start_adventure(920251, {"hero_override": load("res://core/meta_progression.gd").new(_db, sv_white).hero_override()}) | (待填) | | | -| 946 | 378 | "白板成立:空参数 = 零强化参数 = 纯表值(atk/max_hp/ac 三轴逐位一致——验收 2/3)" | int(rw.effective_atk()) == int(_db.hero.atk) and int(rw.effective_max_hp()) == int(_db.hero.max_hp) and int(rz.effective_atk()) == int(rw.effective_atk()) and int(rz.effective_max_hp()) == int(rw.effective_max_hp()) and int(rz.effective_ac()) == int(rw.effective_ac()) | (待填) | | | -| 947 | 382 | "ac 轴:白板恒表值、未换强化时注入零位移" | int(rw.effective_ac()) == int(_db.hero.armor_class) and int(r.effective_ac()) == int(_db.hero.armor_class) | (待填) | | | +| 929 | 354 | "生命淬炼 2 级 → hero_override().max_hp == Σ 节点 effects×等级(验收4 换代〔天赋树批 V12〕)" | int(mp.hero_override().get("max_hp", -1)) == 2 * int(_db.crossrun.meta_tree.row_by_id("sp_hp").get("effects", {}).get("max_hp", 0)) | (待填) | | | +| 930 | 356 | "攻/AC 树零来源(契约 9——树表零 atk/armor_class 行,override 二轴恒 0+预留位 0)" | int(mp.hero_override().get("atk", -1)) == 0 and int(mp.hero_override().get("armor_class", -1)) == 0 | (待填) | | | +| 931 | 358 | "命中线在账未注入(契约 6——override 无 hit 字段〔天赋树批 V13〕)" | not mp.hero_override().has("hit") | (待填) | | | +| 932 | 361 | "注入参数冒险启动(start_adventure params 通道)" | adr.start_adventure(920251, {"hero_override": mp.hero_override()}) | (待填) | | | +| 933 | 365 | "首战 hero_atk = 表值 + 永久强化 flat(begin_battle 同表达式——下一局能用到强化)" | int(r.effective_atk()) == int(_db.combat.hero.atk) + int(ov.get("atk", 0)) | (待填) | | | +| 934 | 367 | "首战 hero_max_hp 与开局生命账本 = 表值 + 注入档" | int(r.effective_max_hp()) == int(_db.combat.hero.max_hp) + int(ov.get("max_hp", 0)) and int(adr.stats().hero_hp) == int(_db.combat.hero.max_hp) + int(ov.get("max_hp", 0)) | (待填) | | | +| 935 | 372 | "白板冒险启动(无 params 实参——B1 空参数路径)" | adr_w.start_adventure(920251) | (待填) | | | +| 936 | 375 | "零强化参数冒险启动(新档 hero_override 全零)" | adr_z.start_adventure(920251, {"hero_override": load("res://core/meta_progression.gd").new(_db, sv_white).hero_override()}) | (待填) | | | +| 937 | 379 | "白板成立:空参数 = 零强化参数 = 纯表值(atk/max_hp/ac 三轴逐位一致——验收 2/3)" | int(rw.effective_atk()) == int(_db.combat.hero.atk) and int(rw.effective_max_hp()) == int(_db.combat.hero.max_hp) and int(rz.effective_atk()) == int(rw.effective_atk()) and int(rz.effective_max_hp()) == int(rw.effective_max_hp()) and int(rz.effective_ac()) == int(rw.effective_ac()) | (待填) | | | +| 938 | 383 | "ac 轴:白板恒表值、未换强化时注入零位移" | int(rw.effective_ac()) == int(_db.combat.hero.armor_class) and int(r.effective_ac()) == int(_db.combat.hero.armor_class) | (待填) | | | -## S117 _smoke_forge_axes(行 392–540 · 33 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) +## S117 _smoke_forge_axes(行 393–541 · 33 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 948 | 392 | "hero.tres 无 attack_type/armor_type 字段(契约 2——静态表两轴删除)〔锻造工坊批 V5〕" | not ("attack_type" in _db.hero) and not ("armor_type" in _db.hero) | (待填) | | | -| 949 | 395 | "锻造配置默认值 attack_type=normal / armor_type=light(常驻持有)〔锻造工坊批 V5〕" | _db.forge_default_attack_type() == "normal" and _db.forge_default_armor_type() == "light" | (待填) | | | -| 950 | 399 | "spawn 受理(三参形态——两轴缺省回退面)" | uc.spawn(1, 0, int(_db.hero.max_hp)) | (待填) | | | -| 951 | 400 | "主角单位快照两轴 = 锻造配置默认值 normal/light(零注入回退——行为连续)〔锻造工坊批 V5〕" | String(uc.hero_unit().get("attack_type", "")) == "normal" and String(uc.hero_unit().get("armor_type", "")) == "light" | (待填) | | | -| 952 | 410 | "长剑锻造首购:扣 20 钥石、等级 0→1(Q1——第一级=获得装备=首个 +1)〔锻造工坊批 V2〕" | mp2.forge_buy_level("sword") and sv2.forge_route_level("sword") == 1 and sv2.keystone_balance() == 80 | (待填) | | | -| 953 | 414 | "购买持久(reload 后等级与余额不变——即时写盘)〔锻造工坊批 V2〕" | sv2r.forge_route_level("sword") == 1 and sv2r.keystone_balance() == 80 | (待填) | | | -| 954 | 417 | "长剑二级购:扣 30、等级→2;轻甲独立零扰动(六路线各练各的)〔锻造工坊批 V2〕" | mp2.forge_buy_level("sword") and sv2.forge_route_level("sword") == 2 and sv2.keystone_balance() == 50 and sv2.forge_route_level("light") == 0 | (待填) | | | -| 955 | 424 | "余额不足购买拒绝且零副作用(0 余额——三域零变化)〔锻造工坊批 V2〕" | not mp2d.forge_buy_level("sword") and sv2d.keystone_balance() == 0 and sv2d.forge_routes_snapshot() == fr_dry | (待填) | | | -| 956 | 429 | "已满级拒购/未解锁拒购(cap 闸+解锁闸——零副作用)〔锻造工坊批 V2〕" | not mp2.forge_buy_level("sword") and not mp2d.forge_buy_level("spear") | (待填) | | | -| 957 | 432 | "通道一:长剑练满→长枪/法杖派生解锁;轻甲未练满→重甲/魔法护甲仍锁〔锻造工坊批 V3〕" | mp2.forge_route_unlocked("spear") and mp2.forge_route_unlocked("staff") and not mp2.forge_route_unlocked("heavy") and not mp2.forge_route_unlocked("ward") | (待填) | | | -| 958 | 435 | "派生解锁后长枪可购(首购 20——余额 50−20)〔锻造工坊批 V3〕" | mp2.forge_buy_level("spear") and sv2.forge_route_level("spear") == 1 and sv2.keystone_balance() == 30 | (待填) | | | -| 959 | 442 | "通道二:魔法护甲一次性解锁价 200 直购开(轻甲未练满也可买)〔锻造工坊批 V3〕" | not mp3.forge_route_unlocked("ward") and mp3.forge_buy_unlock("ward") and sv3.keystone_balance() == 300 and mp3.forge_route_unlocked("ward") | (待填) | | | -| 960 | 447 | "直购后练满不重复收(已解锁=拒绝零收费——两通道互不影响)〔锻造工坊批 V3〕" | mp3.forge_route_unlocked("ward") and not mp3.forge_buy_unlock("ward") and sv3.keystone_balance() == 300 | (待填) | | | -| 961 | 452 | "不设串联门槛(法杖直购开——长枪零状态不阻)〔锻造工坊批 V3〕" | mp2d.forge_buy_unlock("staff") and mp2d.forge_route_unlocked("staff") and sv2d.forge_route_level("spear") == 0 and not mp2d.forge_route_unlocked("spear") | (待填) | | | -| 962 | 457 | "两币分工:购买只扣钥石(余额差=当级价 20——GP 零触碰、无材料计数)〔锻造工坊批 V4〕" | mp3.forge_buy_level("ward") and sv3.keystone_balance() == ks4 - 20 and sv3.forge_route_level("ward") == 1 | (待填) | | | -| 963 | 462 | "选中切换免费(余额零动——写存档域即盘)〔锻造工坊批 V6〕" | mp3.forge_select_armor("ward") and sv3.forge_armor() == "ward" and sv3.keystone_balance() == ks6 | (待填) | | | -| 964 | 465 | "切换后旧路线练度保留(轻甲 5 级不丢——回家待命只不生效)〔锻造工坊批 V6〕" | sv3.forge_route_level("light") == 5 | (待填) | | | -| 965 | 468 | "未解锁路线拒选零副作用〔锻造工坊批 V6〕" | not mp2d.forge_select_weapon("spear") and sv2d.forge_weapon() == "sword" | (待填) | | | -| 966 | 471 | "类型轴当前生效值随选中走(ward;未解锁档回退默认 normal/light)〔锻造工坊批 V5/V6〕" | mp3.hero_armor_type() == "ward" and mp2d.hero_attack_type() == "normal" and mp2d.hero_armor_type() == "light" | (待填) | | | -| 967 | 480 | "build_adventure_params 注入默认值 normal/light(树钥匙未点亮——新档初始持有态)〔锻造工坊批 V5〕" | String(bp_params.get("attack_type", "")) == "normal" and String(bp_params.get("armor_type", "")) == "light" | (待填) | | | -| 968 | 484 | "长枪直购解锁+首购+选中(注入面夹具——200+20 钥石)" | mp4.forge_buy_unlock("spear") and mp4.forge_buy_level("spear") and mp4.forge_select_weapon("spear") | (待填) | | | -| 969 | 487 | "build_adventure_params 读存档选中覆盖(选中长枪→pierce)〔锻造工坊批 V5〕" | String(bp_sel.get("attack_type", "")) == "pierce" and String(bp_sel.get("armor_type", "")) == "light" | (待填) | | | -| 970 | 491 | "hero_override=锻造路线加值(选中长枪 Lv1→atk +1;甲 0 级→AC +0;树零分量;无 hit 键)〔锻造工坊批 V7〕" | int(mp4.hero_override().get("atk", -1)) == 1 and int(mp4.hero_override().get("max_hp", -1)) == 0 and int(mp4.hero_override().get("armor_class", -1)) == 0 and not mp4.hero_override().has("hit") | (待填) | | | -| 971 | 497 | "换当前武器→攻击加值随切换(长剑 3 级待命不生效——只认选中)〔锻造工坊批 V7〕" | int(mp4.hero_override().get("atk", -1)) == 1 | (待填) | | | -| 972 | 499 | "切回长剑→加值=3 级量(旧路线练度保留且生效)〔锻造工坊批 V7〕" | mp4.forge_select_weapon("sword") and int(mp4.hero_override().get("atk", -1)) == 3 | (待填) | | | -| 973 | 505 | "重甲直购解锁+首购+选中(三层 AC 夹具)" | mp5.forge_buy_unlock("heavy") and mp5.forge_buy_level("heavy") and mp5.forge_select_armor("heavy") | (待填) | | | -| 974 | 509 | "注入参数冒险启动(两轴+override 同面)" | adr5.start_adventure(77, p5) | (待填) | | | -| 975 | 511 | "入战 AC 三层:甲型基础 12(重甲)+路线加值 1+局内 0〔锻造工坊批 V7〕" | int(r5.effective_ac()) == 12 + 1 | (待填) | | | -| 976 | 513 | "参数面快照入 runner(attack_type=normal/armor_type=heavy)〔锻造工坊批 V6〕" | adr5.stats().attack_type == "normal" and adr5.stats().armor_type == "heavy" | (待填) | | | -| 977 | 516 | "冒险开始后锁死:改存档选中不影响参数面快照与生效值(契约 8)〔锻造工坊批 V6〕" | mp5.forge_select_armor("light") and adr5.stats().armor_type == "heavy" and int(r5.effective_ac()) == 12 + 1 | (待填) | | | -| 978 | 525 | "enc 两轴直达 spawn 英雄快照(hero_attack 行携带 pierce 键——注入链末端)〔锻造工坊批 V5〕" | pierce_rows >= 1 | (待填) | | | -| 979 | 529 | "spawn 显式两轴注入(magic/heavy 直入英雄快照)〔锻造工坊批 V5〕" | uc2.spawn(1, 0, int(_db.hero.max_hp), [], -1, -1, [], "magic", "heavy") and String(uc2.hero_unit().get("attack_type", "")) == "magic" and String(uc2.hero_unit().get("armor_type", "")) == "heavy" | (待填) | | | -| 980 | 539 | "树零攻防来源复核(12 行 effects 键集无 atk/armor_class——契约 1)〔锻造工坊批 V10〕" | not tree_atk_ac | (待填) | | | +| 939 | 393 | "hero.tres 无 attack_type/armor_type 字段(契约 2——静态表两轴删除)〔锻造工坊批 V5〕" | not ("attack_type" in _db.combat.hero) and not ("armor_type" in _db.combat.hero) | (待填) | | | +| 940 | 396 | "锻造配置默认值 attack_type=normal / armor_type=light(常驻持有)〔锻造工坊批 V5〕" | _db.crossrun.forge.default_attack_type == "normal" and _db.crossrun.forge.default_armor_type == "light" | (待填) | | | +| 941 | 400 | "spawn 受理(三参形态——两轴缺省回退面)" | uc.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}) | (待填) | | | +| 942 | 401 | "主角单位快照两轴 = 锻造配置默认值 normal/light(零注入回退——行为连续)〔锻造工坊批 V5〕" | String(uc.hero_unit().get("attack_type", "")) == "normal" and String(uc.hero_unit().get("armor_type", "")) == "light" | (待填) | | | +| 943 | 411 | "长剑锻造首购:扣 20 钥石、等级 0→1(Q1——第一级=获得装备=首个 +1)〔锻造工坊批 V2〕" | mp2.forge_buy_level("sword") and sv2.meta_state.forge_state().levels.get("sword", 0) == 1 and sv2.meta_state.keystone_balance() == 80 | (待填) | | | +| 944 | 415 | "购买持久(reload 后等级与余额不变——即时写盘)〔锻造工坊批 V2〕" | sv2r.meta_state.forge_state().levels.get("sword", 0) == 1 and sv2r.meta_state.keystone_balance() == 80 | (待填) | | | +| 945 | 418 | "长剑二级购:扣 30、等级→2;轻甲独立零扰动(六路线各练各的)〔锻造工坊批 V2〕" | mp2.forge_buy_level("sword") and sv2.meta_state.forge_state().levels.get("sword", 0) == 2 and sv2.meta_state.keystone_balance() == 50 and sv2.meta_state.forge_state().levels.get("light", 0) == 0 | (待填) | | | +| 946 | 425 | "余额不足购买拒绝且零副作用(0 余额——三域零变化)〔锻造工坊批 V2〕" | not mp2d.forge_buy_level("sword") and sv2d.meta_state.keystone_balance() == 0 and sv2d.meta_state.forge_state() == fr_dry | (待填) | | | +| 947 | 430 | "已满级拒购/未解锁拒购(cap 闸+解锁闸——零副作用)〔锻造工坊批 V2〕" | not mp2.forge_buy_level("sword") and not mp2d.forge_buy_level("spear") | (待填) | | | +| 948 | 433 | "通道一:长剑练满→长枪/法杖派生解锁;轻甲未练满→重甲/魔法护甲仍锁〔锻造工坊批 V3〕" | mp2._forge_route_unlocked("spear") and mp2._forge_route_unlocked("staff") and not mp2._forge_route_unlocked("heavy") and not mp2._forge_route_unlocked("ward") | (待填) | | | +| 949 | 436 | "派生解锁后长枪可购(首购 20——余额 50−20)〔锻造工坊批 V3〕" | mp2.forge_buy_level("spear") and sv2.meta_state.forge_state().levels.get("spear", 0) == 1 and sv2.meta_state.keystone_balance() == 30 | (待填) | | | +| 950 | 443 | "通道二:魔法护甲一次性解锁价 200 直购开(轻甲未练满也可买)〔锻造工坊批 V3〕" | not mp3._forge_route_unlocked("ward") and mp3.forge_buy_unlock("ward") and sv3.meta_state.keystone_balance() == 300 and mp3._forge_route_unlocked("ward") | (待填) | | | +| 951 | 448 | "直购后练满不重复收(已解锁=拒绝零收费——两通道互不影响)〔锻造工坊批 V3〕" | mp3._forge_route_unlocked("ward") and not mp3.forge_buy_unlock("ward") and sv3.meta_state.keystone_balance() == 300 | (待填) | | | +| 952 | 453 | "不设串联门槛(法杖直购开——长枪零状态不阻)〔锻造工坊批 V3〕" | mp2d.forge_buy_unlock("staff") and mp2d._forge_route_unlocked("staff") and sv2d.meta_state.forge_state().levels.get("spear", 0) == 0 and not mp2d._forge_route_unlocked("spear") | (待填) | | | +| 953 | 458 | "两币分工:购买只扣钥石(余额差=当级价 20——GP 零触碰、无材料计数)〔锻造工坊批 V4〕" | mp3.forge_buy_level("ward") and sv3.meta_state.keystone_balance() == ks4 - 20 and sv3.meta_state.forge_state().levels.get("ward", 0) == 1 | (待填) | | | +| 954 | 463 | "选中切换免费(余额零动——写存档域即盘)〔锻造工坊批 V6〕" | mp3.forge_select("armor", "ward") and sv3.meta_state.forge_state().armor == "ward" and sv3.meta_state.keystone_balance() == ks6 | (待填) | | | +| 955 | 466 | "切换后旧路线练度保留(轻甲 5 级不丢——回家待命只不生效)〔锻造工坊批 V6〕" | sv3.meta_state.forge_state().levels.get("light", 0) == 5 | (待填) | | | +| 956 | 469 | "未解锁路线拒选零副作用〔锻造工坊批 V6〕" | not mp2d.forge_select("weapon", "spear") and sv2d.meta_state.forge_state().weapon == "sword" | (待填) | | | +| 957 | 472 | "类型轴当前生效值随选中走(ward;未解锁档回退默认 normal/light)〔锻造工坊批 V5/V6〕" | mp3.hero_type_axes()["armor"] == "ward" and mp2d.hero_type_axes()["attack"] == "normal" and mp2d.hero_type_axes()["armor"] == "light" | (待填) | | | +| 958 | 481 | "build_adventure_params 注入默认值 normal/light(树钥匙未点亮——新档初始持有态)〔锻造工坊批 V5〕" | String(bp_params.get("attack_type", "")) == "normal" and String(bp_params.get("armor_type", "")) == "light" | (待填) | | | +| 959 | 485 | "长枪直购解锁+首购+选中(注入面夹具——200+20 钥石)" | mp4.forge_buy_unlock("spear") and mp4.forge_buy_level("spear") and mp4.forge_select("weapon", "spear") | (待填) | | | +| 960 | 488 | "build_adventure_params 读存档选中覆盖(选中长枪→pierce)〔锻造工坊批 V5〕" | String(bp_sel.get("attack_type", "")) == "pierce" and String(bp_sel.get("armor_type", "")) == "light" | (待填) | | | +| 961 | 492 | "hero_override=锻造路线加值(选中长枪 Lv1→atk +1;甲 0 级→AC +0;树零分量;无 hit 键)〔锻造工坊批 V7〕" | int(mp4.hero_override().get("atk", -1)) == 1 and int(mp4.hero_override().get("max_hp", -1)) == 0 and int(mp4.hero_override().get("armor_class", -1)) == 0 and not mp4.hero_override().has("hit") | (待填) | | | +| 962 | 498 | "换当前武器→攻击加值随切换(长剑 3 级待命不生效——只认选中)〔锻造工坊批 V7〕" | int(mp4.hero_override().get("atk", -1)) == 1 | (待填) | | | +| 963 | 500 | "切回长剑→加值=3 级量(旧路线练度保留且生效)〔锻造工坊批 V7〕" | mp4.forge_select("weapon", "sword") and int(mp4.hero_override().get("atk", -1)) == 3 | (待填) | | | +| 964 | 506 | "重甲直购解锁+首购+选中(三层 AC 夹具)" | mp5.forge_buy_unlock("heavy") and mp5.forge_buy_level("heavy") and mp5.forge_select("armor", "heavy") | (待填) | | | +| 965 | 510 | "注入参数冒险启动(两轴+override 同面)" | adr5.start_adventure(77, p5) | (待填) | | | +| 966 | 512 | "入战 AC 三层:甲型基础 12(重甲)+路线加值 1+局内 0〔锻造工坊批 V7〕" | int(r5.effective_ac()) == 12 + 1 | (待填) | | | +| 967 | 514 | "参数面快照入 runner(attack_type=normal/armor_type=heavy)〔锻造工坊批 V6〕" | adr5.stats().attack_type == "normal" and adr5.stats().armor_type == "heavy" | (待填) | | | +| 968 | 517 | "冒险开始后锁死:改存档选中不影响参数面快照与生效值(契约 8)〔锻造工坊批 V6〕" | mp5.forge_select("armor", "light") and adr5.stats().armor_type == "heavy" and int(r5.effective_ac()) == 12 + 1 | (待填) | | | +| 969 | 526 | "enc 两轴直达 spawn 英雄快照(hero_attack 行携带 pierce 键——注入链末端)〔锻造工坊批 V5〕" | pierce_rows >= 1 | (待填) | | | +| 970 | 530 | "spawn 显式两轴注入(magic/heavy 直入英雄快照)〔锻造工坊批 V5〕" | uc2.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_attack_type": "magic", "hero_armor_type": "heavy"}) and String(uc2.hero_unit().get("attack_type", "")) == "magic" and String(uc2.hero_unit().get("armor_type", "")) == "heavy" | (待填) | | | +| 971 | 540 | "树零攻防来源复核(12 行 effects 键集无 atk/armor_class——契约 1)〔锻造工坊批 V10〕" | not tree_atk_ac | (待填) | | | -## S118 _smoke_tree(行 548–621 · 25 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) +## S118 _smoke_tree(行 549–622 · 25 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 981 | 548 | "meta_tree 已加载(12 节点=脊柱 2〔生命/命中淬炼一线一节点 cap4〕+锻造 2〔钥匙+彩蛋——彩蛋含在锻造 2 内非第 13 行〕+钥石 2〔秘宝匣/刷新升级〕+同调 6〔socket 3+Fork 3 双态〕——v0.8 终态)〔天赋树批 V1〕" | _db.meta_tree != null and (_db.meta_tree.rows as Array).size() == 12 | (待填) | | | -| 982 | 550 | "MetaTreeTable.validate 零违例(脉/类型/币词表出域即拒/Fork 三组每组恰一且持 a/b 双效果键/彩蛋 ≤3/effects 键封闭/零 archetype)" | _db.meta_tree.validate().is_empty() | (待填) | | | -| 983 | 558 | "easter_ley effects 键集 ⊆ {max_hp, hit}(攻/护分量已摘除——契约 9)〔天赋树批 V8 落点〕" | ley_ok | (待填) | | | -| 984 | 566 | "schema 反例:vein 出域被拒" | not t2.validate().is_empty() | (待填) | | | -| 985 | 571 | "schema 反例:fork_group 出域(fg_mastery 已退役组)被拒" | not t2.validate().is_empty() | (待填) | | | -| 986 | 576 | "schema 反例:effects.archetype 加载致命(🔴 层级纪律护栏面)〔天赋树批 V4〕" | not t2.validate().is_empty() | (待填) | | | -| 987 | 581 | "schema 反例:requires 悬空被拒" | not t2.validate().is_empty() | (待填) | | | -| 988 | 586 | "树购买受理(脊柱生命 L1——树全钥石单币)" | mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 1 | (待填) | | | -| 989 | 588 | "行门槛:第 2 级不可购(总等级 1 < 4——一线一节点主语,买第 N 级需全树总投入 ≥4×(N−1))〔天赋树批 V2〕" | not mp.tree_can_buy("sp_hp") | (待填) | | | -| 990 | 590 | "命中 L1/锻造钥匙(free 无前置=初始零购零币可点亮)/同调位 1(根节点无前置)受理(总等级 1→4)" | mp.tree_buy("sp_hit") and mp.tree_buy("forge_workshop") and mp.tree_buy("att_socket1") | (待填) | | | -| 991 | 592 | "锻造工坊钥匙零币(free)" | int(_db.tree_node("forge_workshop").get("price", -1)) == 0 | (待填) | | | -| 992 | 593 | "行门槛恰达开放(总等级 4 ≥ 4×1)" | mp.tree_can_buy("sp_hp") | (待填) | | | -| 993 | 594 | "第 2 级购买受理(门槛恰达当刻开放)" | mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 2 | (待填) | | | -| 994 | 598 | "Fork 购买=解锁+默认 A 态(一节点双态——单行承载)" | mp.tree_buy_fork("fork_posture") and sv.tree_level("fork_posture") == 1 and sv.fork_side("fg_posture") == "a" | (待填) | | | -| 995 | 600 | "已解锁 Fork 重复购买拒绝(cap 1)" | not mp.tree_can_buy("fork_posture") | (待填) | | | -| 996 | 601 | "同态重复切换零操作(false)" | not mp.tree_toggle_fork("fg_posture", "a") | (待填) | | | -| 997 | 602 | "未解锁 Fork 切换拒绝(解锁唯一入口=购买面)" | not mp.tree_toggle_fork("fg_sense", "b") | (待填) | | | -| 998 | 604 | "零币切态 b(购买解锁后 A/B 可切)" | mp.tree_toggle_fork("fg_posture", "b") and sv.fork_side("fg_posture") == "b" | (待填) | | | -| 999 | 606 | "切态零消费(钥石余额差分不动)" | int(sv.keystone_balance()) == bal_pre_toggle | (待填) | | | -| 1000 | 607 | "规则键投影=激活态效果键(B 态稳健拓荒——tree_rule_keys 随切态换键集)" | (mp.tree_rule_keys() as Array).has("posture_bulwark") and not (mp.tree_rule_keys() as Array).has("posture_first_strike") | (待填) | | | -| 1001 | 609 | "切回 A 态键集随换(六键词表在册——V11 校验面归单元段)" | mp.tree_toggle_fork("fg_posture", "a") and (mp.tree_rule_keys() as Array).has("posture_first_strike") | (待填) | | | -| 1002 | 612 | "彩蛋不经购买(手动购亮=红——觉醒唯一入口=特化判定)" | not mp.tree_can_buy("easter_ley") | (待填) | | | -| 1003 | 613 | "觉醒前置未满足不亮(两线未拉满)" | sv.tree_level("easter_ley") == 0 | (待填) | | | -| 1004 | 618 | "两线拉满+同调位全解锁 → 地脉共鸣自动点亮(零币特化判定)" | sv.tree_level("easter_ley") == 1 | (待填) | | | -| 1005 | 621 | "觉醒零消费(钥石余额不动)" | int(sv.keystone_balance()) == bal_now | (待填) | | | +| 972 | 549 | "meta_tree 已加载(12 节点=脊柱 2〔生命/命中淬炼一线一节点 cap4〕+锻造 2〔钥匙+彩蛋——彩蛋含在锻造 2 内非第 13 行〕+钥石 2〔秘宝匣/刷新升级〕+同调 6〔socket 3+Fork 3 双态〕——v0.8 终态)〔天赋树批 V1〕" | _db.crossrun.meta_tree != null and (_db.crossrun.meta_tree.rows as Array).size() == 12 | (待填) | | | +| 973 | 551 | "MetaTreeTable.validate 零违例(脉/类型/币词表出域即拒/Fork 三组每组恰一且持 a/b 双效果键/彩蛋 ≤3/effects 键封闭/零 archetype)" | _db.crossrun.meta_tree.validate().is_empty() | (待填) | | | +| 974 | 559 | "easter_ley effects 键集 ⊆ {max_hp, hit}(攻/护分量已摘除——契约 9)〔天赋树批 V8 落点〕" | ley_ok | (待填) | | | +| 975 | 567 | "schema 反例:vein 出域被拒" | not t2.validate().is_empty() | (待填) | | | +| 976 | 572 | "schema 反例:fork_group 出域(fg_mastery 已退役组)被拒" | not t2.validate().is_empty() | (待填) | | | +| 977 | 577 | "schema 反例:effects.archetype 加载致命(🔴 层级纪律护栏面)〔天赋树批 V4〕" | not t2.validate().is_empty() | (待填) | | | +| 978 | 582 | "schema 反例:requires 悬空被拒" | not t2.validate().is_empty() | (待填) | | | +| 979 | 587 | "树购买受理(脊柱生命 L1——树全钥石单币)" | mp.tree_buy("sp_hp") and sv.meta_state.tree_state().nodes.get("sp_hp", 0) == 1 | (待填) | | | +| 980 | 589 | "行门槛:第 2 级不可购(总等级 1 < 4——一线一节点主语,买第 N 级需全树总投入 ≥4×(N−1))〔天赋树批 V2〕" | not mp.tree_can_buy("sp_hp") | (待填) | | | +| 981 | 591 | "命中 L1/锻造钥匙(free 无前置=初始零购零币可点亮)/同调位 1(根节点无前置)受理(总等级 1→4)" | mp.tree_buy("sp_hit") and mp.tree_buy("forge_workshop") and mp.tree_buy("att_socket1") | (待填) | | | +| 982 | 593 | "锻造工坊钥匙零币(free)" | int(_db.crossrun.meta_tree.row_by_id("forge_workshop").get("price", -1)) == 0 | (待填) | | | +| 983 | 594 | "行门槛恰达开放(总等级 4 ≥ 4×1)" | mp.tree_can_buy("sp_hp") | (待填) | | | +| 984 | 595 | "第 2 级购买受理(门槛恰达当刻开放)" | mp.tree_buy("sp_hp") and sv.meta_state.tree_state().nodes.get("sp_hp", 0) == 2 | (待填) | | | +| 985 | 599 | "Fork 购买=解锁+默认 A 态(一节点双态——单行承载)" | mp.tree_buy_fork("fork_posture") and sv.meta_state.tree_state().nodes.get("fork_posture", 0) == 1 and sv.meta_state.tree_state().fork_sides.get("fg_posture", "") == "a" | (待填) | | | +| 986 | 601 | "已解锁 Fork 重复购买拒绝(cap 1)" | not mp.tree_can_buy("fork_posture") | (待填) | | | +| 987 | 602 | "同态重复切换零操作(false)" | not mp.tree_toggle_fork("fg_posture", "a") | (待填) | | | +| 988 | 603 | "未解锁 Fork 切换拒绝(解锁唯一入口=购买面)" | not mp.tree_toggle_fork("fg_sense", "b") | (待填) | | | +| 989 | 605 | "零币切态 b(购买解锁后 A/B 可切)" | mp.tree_toggle_fork("fg_posture", "b") and sv.meta_state.tree_state().fork_sides.get("fg_posture", "") == "b" | (待填) | | | +| 990 | 607 | "切态零消费(钥石余额差分不动)" | int(sv.meta_state.keystone_balance()) == bal_pre_toggle | (待填) | | | +| 991 | 608 | "规则键投影=激活态效果键(B 态稳健拓荒——tree_rule_keys 随切态换键集)" | (mp.tree_rule_keys() as Array).has("posture_bulwark") and not (mp.tree_rule_keys() as Array).has("posture_first_strike") | (待填) | | | +| 992 | 610 | "切回 A 态键集随换(六键词表在册——V11 校验面归单元段)" | mp.tree_toggle_fork("fg_posture", "a") and (mp.tree_rule_keys() as Array).has("posture_first_strike") | (待填) | | | +| 993 | 613 | "彩蛋不经购买(手动购亮=红——觉醒唯一入口=特化判定)" | not mp.tree_can_buy("easter_ley") | (待填) | | | +| 994 | 614 | "觉醒前置未满足不亮(两线未拉满)" | sv.meta_state.tree_state().nodes.get("easter_ley", 0) == 0 | (待填) | | | +| 995 | 619 | "两线拉满+同调位全解锁 → 地脉共鸣自动点亮(零币特化判定)" | sv.meta_state.tree_state().nodes.get("easter_ley", 0) == 1 | (待填) | | | +| 996 | 622 | "觉醒零消费(钥石余额不动)" | int(sv.meta_state.keystone_balance()) == bal_now | (待填) | | | -## S119 _smoke_tree_rules(行 633–758 · 21 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) +## S119 _smoke_tree_rules(行 634–761 · 21 条 · 源 game/tests/unit/core/meta_progression_forge_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1006 | 633 | "先手压制:每关首战首轮窗命中(battle_index=1 且 round=1 → grant_ap queue)〔天赋树批 V10〕" | (fx.dispatch(ctx_p, "round_start", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1 | (待填) | | | -| 1007 | 635 | "先手压制:首轮窗外地不触发(第二轮/第二关首战首轮——「每战首轮」旧语义换代件)" | (fx.dispatch(ctx_p, "round_start", {"battle_index": 1, "round_index": 2}, Callable()) as Array).is_empty() and (fx.dispatch(ctx_p, "round_start", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty() | (待填) | | | -| 1008 | 638 | "稳健拓荒:首轮窗内受击出盾、窗外不出(shield damage_taken 同窗条件)" | (fx.dispatch(ctx_p, "damage_taken", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1 and (fx.dispatch(ctx_p, "damage_taken", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty() | (待填) | | | -| 1009 | 654 | "锋:踩雷减半命中+限次记账(counter 0<2 闸开——damage_mult+counter_add 双行同预评估态)" | got_mult and got_add | (待填) | | | -| 1010 | 657 | "锋:限次耗尽全额(counter 2 不<2 闸关——每关限次 provisional 2)〔无状态化 V5〕" | (fz.dispatch(ctx_fz, "mine_hit", {}, Callable()) as Array).is_empty() | (待填) | | | -| 1011 | 663 | "锋:限次跨战持久(level 桶共享引用——替代旧副本搬运链)〔无状态化 V4〕" | (fz2.dispatch(ctx_fz2, "mine_hit", {}, Callable()) as Array).is_empty() | (待填) | | | -| 1012 | 685 | "锋:惊巢反制 mine_hit 减半分支受理(wa12 前提——halve_pct=50 效果行落 step_row)〔wa12〕" | nc_hit != "" | (待填) | | | -| 1013 | 687 | "惊巢反制效果行==「惊巢反制——危险格挖掘惩罚减半(6→3)」零「挖雷」(u13 换代)〔wa12〕" | nc_hit == "惊巢反制——危险格挖掘惩罚减半(6→3)" and not nc_hit.contains("挖雷") | (待填) | | | -| 1014 | 691 | "守侧 rig:战斗编排启动(tree_rules=sense_guard 携带)" | orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "feats": [], "tree_rules": ["sense_guard"], "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"}) | (待填) | | | -| 1015 | 703 | "守侧 rig:安全格定位(非危险格=标错负样本)" | safe.x >= 0 | (待填) | | | -| 1016 | 704 | "守侧 rig:首轮后 queue 干净(开局 posture queue 已兑现)" | int(orch.action_economy.pending_ap_delta()) == 0 | (待填) | | | -| 1017 | 708 | "守①:当回合标错且封印在场 → 下轮 AP+1(queue 排队——本轮 AP 逐位零变化)" | int(orch.action_economy.pending_ap_delta()) == 1 | (待填) | | | -| 1018 | 713 | "守③④:旧封印重判零触发+同格最多一次(_seal_miss_seen 去重闸=「当回合标错动作」判别器)" | int(orch.action_economy.pending_ap_delta()) == 1 | (待填) | | | -| 1019 | 717 | "守②负样本:标中/被挖封印零触发(标错事件只在 miss 分支发射——发射面过滤承载)" | int(orch.action_economy.pending_ap_delta()) == 1 | (待填) | | | -| 1020 | 721 | "守⑤ rig:次战编排启动(新战新队列实例)" | orch2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "feats": [], "tree_rules": ["sense_guard"], "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"}) | (待填) | | | -| 1021 | 727 | "守⑤:末轮判出不返(queue 只在 notify_round_start 兑现、战亡即弃——无跨战泄漏)" | int(orch2.action_economy.pending_ap_delta()) == 0 | (待填) | | | -| 1022 | 733 | "散未激活:商品标价原价(无 economy_spend 键)" | int(lr._spend_discounted(100)) == 100 | (待填) | | | -| 1023 | 736 | "散激活:商品购买价 ×80%(provisional 折率——floor 偏向玩家、下限 1;刷新价不折=另一计价点零触碰)" | int(lr._spend_discounted(100)) == 80 and int(lr._spend_discounted(1)) == 1 | (待填) | | | -| 1024 | 740 | "聚 rig:基线编排启动(无增益侧)" | orch3.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "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"}) | (待填) | | | -| 1025 | 749 | "聚 rig:增益编排启动(tree_rules=economy_hoard)" | orch4.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "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"}) | (待填) | | | -| 1026 | 757 | "聚:战斗掉落乘算 ×1.5(provisional——同种子配对 %d→%d,入账前=结算框显示实得)" | gp_hoard == int(round(float(gp_base) * 1.5)) and gp_hoard >= gp_base | (待填) | | | +| 997 | 634 | "先手压制:每关首战首轮窗命中(battle_index=1 且 round=1 → grant_ap queue)〔天赋树批 V10〕" | (fx.dispatch(ctx_p, "round_start", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1 | (待填) | | | +| 998 | 636 | "先手压制:首轮窗外地不触发(第二轮/第二关首战首轮——「每战首轮」旧语义换代件)" | (fx.dispatch(ctx_p, "round_start", {"battle_index": 1, "round_index": 2}, Callable()) as Array).is_empty() and (fx.dispatch(ctx_p, "round_start", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty() | (待填) | | | +| 999 | 639 | "稳健拓荒:首轮窗内受击出盾、窗外不出(shield damage_taken 同窗条件)" | (fx.dispatch(ctx_p, "damage_taken", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1 and (fx.dispatch(ctx_p, "damage_taken", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty() | (待填) | | | +| 1000 | 655 | "锋:踩雷减半命中+限次记账(counter 0<2 闸开——damage_mult+counter_add 双行同预评估态)" | got_mult and got_add | (待填) | | | +| 1001 | 658 | "锋:限次耗尽全额(counter 2 不<2 闸关——每关限次 provisional 2)〔无状态化 V5〕" | (fz.dispatch(ctx_fz, "mine_hit", {}, Callable()) as Array).is_empty() | (待填) | | | +| 1002 | 664 | "锋:限次跨战持久(level 桶共享引用——替代旧副本搬运链)〔无状态化 V4〕" | (fz2.dispatch(ctx_fz2, "mine_hit", {}, Callable()) as Array).is_empty() | (待填) | | | +| 1003 | 686 | "锋:惊巢反制 mine_hit 减半分支受理(wa12 前提——halve_pct=50 效果行落 step_row)〔wa12〕" | nc_hit != "" | (待填) | | | +| 1004 | 688 | "惊巢反制效果行==「惊巢反制——危险格挖掘惩罚减半(6→3)」零「挖雷」(u13 换代)〔wa12〕" | nc_hit == "惊巢反制——危险格挖掘惩罚减半(6→3)" and not nc_hit.contains("挖雷") | (待填) | | | +| 1005 | 692 | "守侧 rig:战斗编排启动(tree_rules=sense_guard 携带)" | orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "feats": [], "tree_rules": ["sense_guard"], "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"}) | (待填) | | | +| 1006 | 704 | "守侧 rig:安全格定位(非危险格=标错负样本)" | safe.x >= 0 | (待填) | | | +| 1007 | 705 | "守侧 rig:首轮后 queue 干净(开局 posture queue 已兑现)" | int(orch.action_economy.pending_ap_delta()) == 0 | (待填) | | | +| 1008 | 709 | "守①:当回合标错且封印在场 → 下轮 AP+1(queue 排队——本轮 AP 逐位零变化)" | int(orch.action_economy.pending_ap_delta()) == 1 | (待填) | | | +| 1009 | 714 | "守③④:旧封印重判零触发+同格最多一次(_seal_miss_seen 去重闸=「当回合标错动作」判别器)" | int(orch.action_economy.pending_ap_delta()) == 1 | (待填) | | | +| 1010 | 718 | "守②负样本:标中/被挖封印零触发(标错事件只在 miss 分支发射——发射面过滤承载)" | int(orch.action_economy.pending_ap_delta()) == 1 | (待填) | | | +| 1011 | 722 | "守⑤ rig:次战编排启动(新战新队列实例)" | orch2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "feats": [], "tree_rules": ["sense_guard"], "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"}) | (待填) | | | +| 1012 | 728 | "守⑤:末轮判出不返(queue 只在 notify_round_start 兑现、战亡即弃——无跨战泄漏)" | int(orch2.action_economy.pending_ap_delta()) == 0 | (待填) | | | +| 1013 | 734 | "散未激活:商品标价原价(无 economy_spend 键)" | int(lr.current_price({"base_price": 100, "price_increment": 0})) == 100 + int(_db.run.shop.price_level_bonus_at(0)) | (待填) | | | +| 1014 | 738 | "散激活:商品购买价 ×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)))) | (待填) | | | +| 1015 | 743 | "聚 rig:基线编排启动(无增益侧)" | orch3.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "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"}) | (待填) | | | +| 1016 | 752 | "聚 rig:增益编排启动(tree_rules=economy_hoard)" | orch4.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, "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"}) | (待填) | | | +| 1017 | 760 | "聚:战斗掉落乘算 ×1.5(provisional——同种子配对 %d→%d,入账前=结算框显示实得)" | gp_hoard == int(round(float(gp_base) * 1.5)) and gp_hoard >= gp_base | (待填) | | | ## S120 _init(行 22–30 · 2 条 · 源 game/tests/unit/core/minesweeper_rules_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1027 | 22 | "数值表加载" | false | (待填) | | | -| 1028 | 30 | "生成返回棋盘(前置)" | false | (待填) | | | +| 1018 | 22 | "数值表加载" | false | (待填) | | | +| 1019 | 30 | "生成返回棋盘(前置)" | false | (待填) | | | ## S121 _test_install_invariants(行 48–56 · 4 条 · 源 game/tests/unit/core/minesweeper_rules_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1029 | 48 | "装板后 current_board 即所装棋盘" | _rules.current_board() == _board | (待填) | | | -| 1030 | 52 | "装板带初始扩张揭示(%d 格,非零且未铺满安全格 %d)" ↩ % [opened, safe_total] | opened > 0 and opened < safe_total | (待填) | | | -| 1031 | 54 | "安全格余量 = 安全格总数 − 已揭示数(%d − %d)" ↩ % [safe_total, opened] | _rules.safe_remaining() == safe_total - opened | (待填) | | | -| 1032 | 56 | "装板后仍有未揭示格" | _rules.has_unrevealed() | (待填) | | | +| 1020 | 48 | "装板后 current_board 即所装棋盘" | _rules.current_board() == _board | (待填) | | | +| 1021 | 52 | "装板带初始扩张揭示(%d 格,非零且未铺满安全格 %d)" ↩ % [opened, safe_total] | opened > 0 and opened < safe_total | (待填) | | | +| 1022 | 54 | "安全格余量 = 安全格总数 − 已揭示数(%d − %d)" ↩ % [safe_total, opened] | _rules.safe_remaining() == safe_total - opened | (待填) | | | +| 1023 | 56 | "装板后仍有未揭示格" | _rules.has_unrevealed() | (待填) | | | ## S122 _test_dig_face_rules(行 73–80 · 2 条 · 源 game/tests/unit/core/minesweeper_rules_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1033 | 73 | "挖已揭示格被拒(board_face=revealed,禁重复挖)——载荷 %s" ↩ % str(o.get("sub_reason", "")) | String(o.get("outcome", "")) == "rejected" and String(o.get("sub_reason", "")) == "target_revealed" | (待填) | | | -| 1034 | 79 | "挖非相邻格被拒(邻接规则)——载荷 %s" ↩ % str(o2.get("sub_reason", "")) | String(o2.get("outcome", "")) == "rejected" | (待填) | | | +| 1024 | 73 | "挖已揭示格被拒(board_face=revealed,禁重复挖)——载荷 %s" ↩ % str(o.get("sub_reason", "")) | String(o.get("outcome", "")) == "rejected" and String(o.get("sub_reason", "")) == "target_revealed" | (待填) | | | +| 1025 | 79 | "挖非相邻格被拒(邻接规则)——载荷 %s" ↩ % str(o2.get("sub_reason", "")) | String(o2.get("outcome", "")) == "rejected" | (待填) | | | ## S123 _test_reveal_advances(行 86–93 · 4 条 · 源 game/tests/unit/core/minesweeper_rules_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1035 | 86 | "存在未揭示安全格(揭示推进前置)" | false | (待填) | | | -| 1036 | 90 | "专长揭示受理(fx_reveal 不受邻接约束)" | ok | (待填) | | | -| 1037 | 91 | "fx_reveal 后该格已揭示" | _rules.is_revealed(c) | (待填) | | | -| 1038 | 92 | "fx_reveal 后安全格余量递减(%d → %d)" ↩ % [before, _rules.safe_remaining()] | _rules.safe_remaining() < before | (待填) | | | +| 1026 | 86 | "存在未揭示安全格(揭示推进前置)" | false | (待填) | | | +| 1027 | 90 | "专长揭示受理(fx_reveal 不受邻接约束)" | ok | (待填) | | | +| 1028 | 91 | "fx_reveal 后该格已揭示" | _rules.is_revealed(c) | (待填) | | | +| 1029 | 92 | "fx_reveal 后安全格余量递减(%d → %d)" ↩ % [before, _rules.safe_remaining()] | _rules.safe_remaining() < before | (待填) | | | ## S124 _test_seal_place(行 99–105 · 3 条 · 源 game/tests/unit/core/minesweeper_rules_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1039 | 99 | "存在未揭示安全格(封印前置)" | false | (待填) | | | -| 1040 | 104 | "封印后该格在封印态" | _rules.has_seal(c) | (待填) | | | -| 1041 | 105 | "封印数计数 = 1" | _rules.seal_count() == 1 | (待填) | | | +| 1030 | 99 | "存在未揭示安全格(封印前置)" | false | (待填) | | | +| 1031 | 104 | "封印后该格在封印态" | _rules.has_seal(c) | (待填) | | | +| 1032 | 105 | "封印数计数 = 1" | _rules.seal_count() == 1 | (待填) | | | ## S125 _test_reject_out_of_catalog(行 111–112 · 1 条 · 源 game/tests/unit/core/minesweeper_rules_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1042 | 111 | "目录外意图被拒(不静默吞掉)——实际载荷 %s" ↩ % str(o).substr(0, 80) | out.size() == 1 and String(o.get("outcome", o.get("result", ""))) == "rejected" | (待填) | | | +| 1033 | 111 | "目录外意图被拒(不静默吞掉)——实际载荷 %s" ↩ % str(o).substr(0, 80) | out.size() == 1 and String(o.get("outcome", o.get("result", ""))) == "rejected" | (待填) | | | ## S126 _test_output_ok(行 88–89 · 2 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1043 | 88 | "三档×20 种子产出图过产出约束五条+结构铁律(自证零错误)〔fv1〕" | all_ok | (待填) | | | -| 1044 | 89 | "入口数/终点数 = 参数档 entry_count/exit_count〔fv1〕" | entry_exit_ok | (待填) | | | +| 1034 | 88 | "三档×20 种子产出图过产出约束五条+结构铁律(自证零错误)〔fv1〕" | all_ok | (待填) | | | +| 1035 | 89 | "入口数/终点数 = 参数档 entry_count/exit_count〔fv1〕" | entry_exit_ok | (待填) | | | ## S127 _test_reproducible(行 99–119 · 3 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1045 | 99 | "同(种子,参数档)两次产图逐位同图〔fv3〕" | same | (待填) | | | -| 1046 | 109 | "counter 无关(不同号位态同种子同图)〔fv3〕" | _sig(gen_a.generate(_params(2), 42)) == _sig(gen_b.generate(_params(2), 42)) and _sig(gen_a.generate(_params(3), 7)) == _sig(gen_b.generate(_params(3), 7)) | (待填) | | | -| 1047 | 119 | "同参数档不同种子产出异图(10 种子去重数 ≥ 8)〔fv3〕" | distinct >= 8 | (待填) | | | +| 1036 | 99 | "同(种子,参数档)两次产图逐位同图〔fv3〕" | same | (待填) | | | +| 1037 | 109 | "counter 无关(不同号位态同种子同图)〔fv3〕" | _sig(gen_a.generate(_params(2), 42)) == _sig(gen_b.generate(_params(2), 42)) and _sig(gen_a.generate(_params(3), 7)) == _sig(gen_b.generate(_params(3), 7)) | (待填) | | | +| 1038 | 119 | "同参数档不同种子产出异图(10 种子去重数 ≥ 8)〔fv3〕" | distinct >= 8 | (待填) | | | ## S128 _test_fork_rules(行 163–164 · 2 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1048 | 163 | "任意岔口各去路下一节点类型互不相同〔fv5〕" | next_hop_ok | (待填) | | | -| 1049 | 164 | "任意岔口各支路类型构成互异且各有独有类型〔fv5〕" | comp_ok | (待填) | | | +| 1039 | 163 | "任意岔口各去路下一节点类型互不相同〔fv5〕" | next_hop_ok | (待填) | | | +| 1040 | 164 | "任意岔口各支路类型构成互异且各有独有类型〔fv5〕" | comp_ok | (待填) | | | ## S129 _test_shop_and_attribution(行 228–230 · 3 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1050 | 228 | "任意入口→终点路径恰过一家店〔fv6〕" | paths_ok | (待填) | | | -| 1051 | 229 | "每终点前一位恰一店+商店总数=终点数(归属锁定)〔fv6〕" | lock_ok | (待填) | | | -| 1052 | 230 | "多终点落在互异节点深度(L2/L3)〔fv6〕" | depth_ok | (待填) | | | +| 1041 | 228 | "任意入口→终点路径恰过一家店〔fv6〕" | paths_ok | (待填) | | | +| 1042 | 229 | "每终点前一位恰一店+商店总数=终点数(归属锁定)〔fv6〕" | lock_ok | (待填) | | | +| 1043 | 230 | "多终点落在互异节点深度(L2/L3)〔fv6〕" | depth_ok | (待填) | | | ## S130 _test_battle_floor(行 257–257 · 1 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1053 | 257 | "任意合法产出战斗节点数 ≥ battle_floor〔fv7〕" | ok | (待填) | | | +| 1044 | 257 | "任意合法产出战斗节点数 ≥ battle_floor〔fv7〕" | ok | (待填) | | | ## S131 _test_exit_structure(行 285–287 · 3 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1054 | 285 | "三关参数档生成图各含 ≥1 岔口节点(第一关即含)〔fv9〕" | fork_all | (待填) | | | -| 1055 | 286 | "L1 生成图终点恰 1(出口恒 1)〔fv9〕" | l1_single | (待填) | | | -| 1056 | 287 | "L2/L3 含分区域大岔口(一节点可达 ≥2 终点且去路数=出口数)〔fv9〕" | big_fork_ok | (待填) | | | +| 1045 | 285 | "三关参数档生成图各含 ≥1 岔口节点(第一关即含)〔fv9〕" | fork_all | (待填) | | | +| 1046 | 286 | "L1 生成图终点恰 1(出口恒 1)〔fv9〕" | l1_single | (待填) | | | +| 1047 | 287 | "L2/L3 含分区域大岔口(一节点可达 ≥2 终点且去路数=出口数)〔fv9〕" | big_fork_ok | (待填) | | | -## S132 _test_rng_single_point(行 313–318 · 3 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) +## S132 _test_rng_single_point(行 315–317 · 2 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1057 | 313 | "pure_stream 不推全局计数器(产图前后 counter 同值)〔fv10〕" | svc.counter() == before | (待填) | | | -| 1058 | 316 | "pure_stream 同 (seed,scope) 同流〔fv10〕" | a.randf() == b.randf() and a.randi() == b.randi() | (待填) | | | -| 1059 | 317 | "pure_stream 不推计数器且产图非空〔fv10〕" | svc.counter() == before and (g["nodes"] as Array).size() > 0 | (待填) | | | +| 1048 | 315 | "pure_stream 同 (seed,scope) 同流〔fv10〕" | a.randf() == b.randf() and a.randi() == b.randi() | (待填) | | | +| 1049 | 316 | "pure_stream 不推计数器且产图非空〔fv10〕" | svc.counter() == before and (g["nodes"] as Array).size() > 0 | (待填) | | | -## S133 _test_negative_fixtures(行 328–412 · 13 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) +## S133 _test_negative_fixtures(行 327–411 · 13 条 · 源 game/tests/unit/core/node_map_generator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1060 | 328 | "句柄重复夹具报 dup_key〔fv1〕" | _has_cat(_gen.validate_output(dup), "dup_key") | (待填) | | | -| 1061 | 332 | "悬空出边夹具报 dangling_next〔fv1〕" | _has_cat(_gen.validate_output(dang), "dangling_next") | (待填) | | | -| 1062 | 336 | "类型出域夹具报 enum〔fv1〕" | _has_cat(_gen.validate_output(badty), "enum") | (待填) | | | -| 1063 | 343 | "战斗节点低于 battle_floor 的合法形状夹具命中「低于保底」判据〔fv7〕" | _has_msg(_gen.validate_output(thin_battle, {"battle_floor": 4}), "低于保底") | (待填) | | | -| 1064 | 345 | "同夹具在保底 1 下自证零错误(判据不误伤)〔fv7〕" | _gen.validate_output(thin_battle, {"battle_floor": 1}).is_empty() | (待填) | | | -| 1065 | 350 | "非 Boss 终点夹具命中「终点(无出边)非 Boss」判据〔fv1〕" | _has_msg(_gen.validate_output(noBoss), "终点(无出边)非 Boss") | (待填) | | | -| 1066 | 361 | "合法合流图(第二入口直插主链,深度取最长路 3)自证零错误〔fv1〕" | _gen.validate_output(conf_legal).is_empty() | (待填) | | | -| 1067 | 370 | "声明深度高于最长路径步数被拒(深度=自入口最长路径步数)〔fv1〕" | _has_msg(_gen.validate_output(conf_high), "自入口最长路径步数") | (待填) | | | -| 1068 | 377 | "回边夹具报前进方向违例(structural)〔fv1〕" | _has_cat(_gen.validate_output(loop), "structural") | (待填) | | | -| 1069 | 387 | "多终点同深度夹具报 structural〔fv6〕" | _has_cat(_gen.validate_output(twin), "structural") | (待填) | | | -| 1070 | 397 | "双终点异深+区域类型互异合法夹具自证零错误〔fv6〕" | _gen.validate_output(twin_ok).is_empty() | (待填) | | | -| 1071 | 404 | "终点前驱非店夹具报 structural(必经恰一店/商店数)〔fv6〕" | _has_cat(cats, "structural") | (待填) | | | -| 1072 | 412 | "岔口同类型下一跳夹具报 structural〔fv5〕" | _has_cat(_gen.validate_output(sameHop), "structural") | (待填) | | | +| 1050 | 327 | "句柄重复夹具报 dup_key〔fv1〕" | _has_cat(_gen.validate_output(dup), "dup_key") | (待填) | | | +| 1051 | 331 | "悬空出边夹具报 dangling_next〔fv1〕" | _has_cat(_gen.validate_output(dang), "dangling_next") | (待填) | | | +| 1052 | 335 | "类型出域夹具报 enum〔fv1〕" | _has_cat(_gen.validate_output(badty), "enum") | (待填) | | | +| 1053 | 342 | "战斗节点低于 battle_floor 的合法形状夹具命中「低于保底」判据〔fv7〕" | _has_msg(_gen.validate_output(thin_battle, {"battle_floor": 4}), "低于保底") | (待填) | | | +| 1054 | 344 | "同夹具在保底 1 下自证零错误(判据不误伤)〔fv7〕" | _gen.validate_output(thin_battle, {"battle_floor": 1}).is_empty() | (待填) | | | +| 1055 | 349 | "非 Boss 终点夹具命中「终点(无出边)非 Boss」判据〔fv1〕" | _has_msg(_gen.validate_output(noBoss), "终点(无出边)非 Boss") | (待填) | | | +| 1056 | 360 | "合法合流图(第二入口直插主链,深度取最长路 3)自证零错误〔fv1〕" | _gen.validate_output(conf_legal).is_empty() | (待填) | | | +| 1057 | 369 | "声明深度高于最长路径步数被拒(深度=自入口最长路径步数)〔fv1〕" | _has_msg(_gen.validate_output(conf_high), "自入口最长路径步数") | (待填) | | | +| 1058 | 376 | "回边夹具报前进方向违例(structural)〔fv1〕" | _has_cat(_gen.validate_output(loop), "structural") | (待填) | | | +| 1059 | 386 | "多终点同深度夹具报 structural〔fv6〕" | _has_cat(_gen.validate_output(twin), "structural") | (待填) | | | +| 1060 | 396 | "双终点异深+区域类型互异合法夹具自证零错误〔fv6〕" | _gen.validate_output(twin_ok).is_empty() | (待填) | | | +| 1061 | 403 | "终点前驱非店夹具报 structural(必经恰一店/商店数)〔fv6〕" | _has_cat(cats, "structural") | (待填) | | | +| 1062 | 411 | "岔口同类型下一跳夹具报 structural〔fv5〕" | _has_cat(_gen.validate_output(sameHop), "structural") | (待填) | | | ## S134 _test_growth(行 57–58 · 2 条 · 源 game/tests/unit/core/node_map_growth_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1073 | 57 | "生长递增:三档最长路径步数逐关变大〔fv8〕" | steps_ok | (待填) | | | -| 1074 | 58 | "生长递增:节点总数逐关变大且增幅变大〔fv8〕" | count_ok | (待填) | | | +| 1063 | 57 | "生长递增:三档最长路径步数逐关变大〔fv8〕" | steps_ok | (待填) | | | +| 1064 | 58 | "生长递增:节点总数逐关变大且增幅变大〔fv8〕" | count_ok | (待填) | | | ## S135 _test_fail_fast_static(行 64–74 · 3 条 · 源 game/tests/unit/core/node_map_growth_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1075 | 64 | "生产三档参数档可行性判定零错误〔fv4〕" | NodeMapGenerator.check_params(db.level_gen_params_row(1)).is_empty() and NodeMapGenerator.check_params(db.level_gen_params_row(3)).is_empty() | (待填) | | | -| 1076 | 70 | "层深 6 放三出口无解 → check_params 拦截〔fv4〕" | not NodeMapGenerator.check_params(unsolvable).is_empty() | (待填) | | | -| 1077 | 74 | "战斗保底超可放数无解 → check_params 拦截〔fv4〕" | not NodeMapGenerator.check_params(overfloor).is_empty() | (待填) | | | +| 1065 | 64 | "生产三档参数档可行性判定零错误〔fv4〕" | NodeMapGenerator.check_params(db.run.level_gen_params.row_by_level_index(1)).is_empty() and NodeMapGenerator.check_params(db.run.level_gen_params.row_by_level_index(3)).is_empty() | (待填) | | | +| 1066 | 70 | "层深 6 放三出口无解 → check_params 拦截〔fv4〕" | not NodeMapGenerator.check_params(unsolvable).is_empty() | (待填) | | | +| 1067 | 74 | "战斗保底超可放数无解 → check_params 拦截〔fv4〕" | not NodeMapGenerator.check_params(overfloor).is_empty() | (待填) | | | ## S136 _test_fatal_subprocess(行 82–91 · 2 条 · 源 game/tests/unit/core/node_map_growth_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1078 | 82 | "未找到引擎二进制,致命子进程通道未跑(如实标注)〔fv4〕" | false | (待填) | | | -| 1079 | 91 | "无解参数档置致命封印并由组合根停机(退出码恰 1,无残图交付)〔fv4〕" | code == 1 | (待填) | | | +| 1068 | 82 | "未找到引擎二进制,致命子进程通道未跑(如实标注)〔fv4〕" | false | (待填) | | | +| 1069 | 91 | "无解参数档置致命封印并由组合根停机(退出码恰 1,无残图交付)〔fv4〕" | code == 1 | (待填) | | | ## S137 _smoke_save(行 81–329 · 60 条 · 源 game/tests/unit/core/save_manager_domain_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1080 | 81 | "apply 原语入账(钥石 7+3、关卡完成次数 1/1/0)" | w.keystone_balance() == 10 and w.clear_count(1) == 1 and w.clear_count(2) == 1 and w.clear_count(3) == 0 | (待填) | | | -| 1081 | 83 | "首杀标记随关卡完成置位(1/2 有、3 无)" | w.boss_first_kill_marked(1) and w.boss_first_kill_marked(2) and not w.boss_first_kill_marked(3) | (待填) | | | -| 1082 | 85 | "生涯终身计数随 apply 自增恰一(中止局:总局数 1、冒险成功局数 0——迭代 18 卡④)" | w.adventure_total() == 1 and w.adventure_cleared() == 0 | (待填) | | | -| 1083 | 87 | "写盘成功" | w.write_to_disk() | (待填) | | | -| 1084 | 89 | "读回成功" | r.load_from_disk() | (待填) | | | -| 1085 | 90 | "钥石余额往返相等(10)" | r.keystone_balance() == 10 | (待填) | | | -| 1086 | 91 | "树域往返相等(tree_nodes/fork_sides——迭代 21 换代承载)" | r.tree_level("sp_hp") == 2 and r.tree_level("sp_hit") == 1 and r.fork_side("fg_posture") == "b" | (待填) | | | -| 1087 | 93 | "锻造工坊域往返相等(levels/unlocked/weapon/armor——迭代 23 T5)〔锻造工坊批 V8〕" | r.forge_route_level("spear") == 3 and r.forge_route_level("sword") == 0 and r.forge_unlocked_purchases() == ["spear"] and r.forge_weapon() == "spear" and r.forge_armor() == "ward" | (待填) | | | -| 1088 | 97 | "道具解锁往返相等" | r.item_unlocked("probe_compass") and not r.item_unlocked("rope") | (待填) | | | -| 1089 | 98 | "关卡完成次数表往返相等" | r.clear_count(1) == 1 and r.clear_count(2) == 1 and r.clear_count(3) == 0 | (待填) | | | -| 1090 | 99 | "首杀标记往返相等" | r.boss_first_kill_marked(1) and r.boss_first_kill_marked(2) and not r.boss_first_kill_marked(3) | (待填) | | | -| 1091 | 101 | "生涯终身计数往返相等(1/0)" | r.adventure_total() == 1 and r.adventure_cleared() == 0 | (待填) | | | -| 1092 | 103 | "冒险摘要往返相等(1 条,seed 99)" | r.recent_summaries().size() == 1 and int(s0.get("master_seed", -1)) == 99 and int(s0.get("keystone_total", -1)) == 3 | (待填) | | | -| 1093 | 108 | "新档合法态(load true + 全零默认)" | f2.load_from_disk() and f2.keystone_balance() == 0 and f2.tree_level("sp_hp") == 0 and not f2.item_unlocked("anything") and f2.clear_count(1) == 0 and f2.recent_summaries().is_empty() | (待填) | | | -| 1094 | 111 | "新档生涯终身计数缺省全零(迭代 18 卡④两新域——白板存档完整可玩)" | f2.adventure_total() == 0 and f2.adventure_cleared() == 0 | (待填) | | | -| 1095 | 117 | "余额 5 花 8 拒绝且余额不动" | not f3.spend_keystone(8) and f3.keystone_balance() == 5 | (待填) | | | -| 1096 | 118 | "余额内消费成功(5→2)" | f3.spend_keystone(3) and f3.keystone_balance() == 2 | (待填) | | | -| 1097 | 126 | "高版本拒读(load false)" | not f5.load_from_disk() | (待填) | | | -| 1098 | 127 | "拒读后原文件字节不动" | FileAccess.open(paths[3], FileAccess.READ).get_as_text() == hv_text | (待填) | | | -| 1099 | 128 | "拒读态写盘拒绝且文件仍字节不动" | not f5.write_to_disk() and FileAccess.open(paths[3], FileAccess.READ).get_as_text() == hv_text | (待填) | | | -| 1100 | 137 | "损坏档兜底(默认数据继续)" | f6.load_from_disk() and f6.keystone_balance() == 0 and f6.recent_summaries().is_empty() | (待填) | | | -| 1101 | 139 | "坏档备份 .corrupt.bak 存在" | FileAccess.file_exists(paths[4] + ".corrupt.bak") | (待填) | | | -| 1102 | 140 | "备份内容 = 原坏档字节" | FileAccess.open(paths[4] + ".corrupt.bak", FileAccess.READ).get_as_text() == junk | (待填) | | | -| 1103 | 151 | "v0 旧档经迁移链入账(keystone_old 12 → 余额 12)" | f7.load_from_disk() and f7.keystone_balance() == 12 | (待填) | | | -| 1104 | 153 | "迁移链 0→1(测试钩)→7(生产) 写盘 version == 7(v7=锻造工坊域,迭代 23 T5——v6=生涯终身计数域迭代 18 卡④)" | f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 7 | (待填) | | | -| 1105 | 165 | "域界干净——meta 域无局内资源键(发现 %s)" ↩ % str(leaked) | leaked.is_empty() | (待填) | | | -| 1106 | 173 | "摘要 cap 10(12 份 → 10 条)" | arr.size() == 10 | (待填) | | | -| 1107 | 174 | "最新在前(头 seed 11 / 尾 seed 2)" | int(arr[0].get("master_seed", -1)) == 11 and int(arr[9].get("master_seed", -1)) == 2 | (待填) | | | -| 1108 | 182 | "断点:新档无断点(合法空态——「继续上局」隐藏)" | not fbp.has_breakpoint() | (待填) | | | -| 1109 | 193 | "断点:写入+写盘成功" | fbp.has_breakpoint() and fbp.write_to_disk() | (待填) | | | -| 1110 | 195 | "断点:读回在档" | rb.load_from_disk() and rb.has_breakpoint() | (待填) | | | -| 1111 | 197 | "断点七字段往返:关卡序号/节点位/生命" | int(bp.get("level_index", -1)) == 2 and int(bp.get("node_id", -1)) == 5 and int(bp.get("hero_hp", -1)) == 13 | (待填) | | | -| 1112 | 201 | "断点七字段往返:属性加成区" | int(bp_bonus.get("atk", -1)) == 2 and int(bp_bonus.get("max_hp", -1)) == 5 and int(bp_bonus.get("armor_class", -1)) == 1 | (待填) | | | -| 1113 | 204 | "断点七字段往返:GP 钱包" | int(bp.get("gp", -1)) == 17 | (待填) | | | -| 1114 | 206 | "断点七字段往返:商店已购档" | int(bp_shop.get("hp_small", -1)) == 2 and int(bp_shop.get("atk_small", -1)) == 1 | (待填) | | | -| 1115 | 208 | "断点七字段往返:生成图代差标记键" | int(bp.get("graph_gen_v2", 0)) == 1 | (待填) | | | -| 1116 | 209 | "断点附随件往返:主种子/计数器/钥石累计" | int(bp.get("master_seed", -1)) == 424242 and int(bp.get("rng_counter", -1)) == 38 and int(bp.get("keystone", -1)) == 6 | (待填) | | | -| 1117 | 213 | "断点附随件往返:已到节点序" | bp_visited.size() == 3 and int(bp_visited[0]) == 0 and int(bp_visited[2]) == 5 | (待填) | | | -| 1118 | 216 | "断点附随件往返:道具库存" | bp_treasure.size() == 1 and String(bp_treasure[0].get("item_id", "")) == "aux_scout" | (待填) | | | -| 1119 | 219 | "断点读面值拷贝(改读面不渗档)" | int((rb.breakpoint_snapshot() as Dictionary).get("hero_hp", -1)) == 13 | (待填) | | | -| 1120 | 222 | "断点:清除+写盘(三结局收束口径)" | not rb.has_breakpoint() and rb.write_to_disk() | (待填) | | | -| 1121 | 224 | "断点:清除后读回无断点" | rc.load_from_disk() and not rc.has_breakpoint() | (待填) | | | -| 1122 | 229 | "v1 旧档迁移 v7:断点域补空(合法空态,无断点域不炸)" | fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint() | (待填) | | | -| 1123 | 231 | "v1 旧档迁移后写盘 version == 7(v1→v7 链式——锻造工坊域 2026-10-12)" | fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 7 | (待填) | | | -| 1124 | 243 | "v2 旧档迁移 v3:断点域与 meta 域原样保留(缺新附随件=合法空态)" | fv2.load_from_disk() and fv2.has_breakpoint() and fv2.keystone_balance() == 7 | (待填) | | | -| 1125 | 245 | "v2 旧断点字段零丢失(向后兼容——breakpoint 内容不因 schema 升版重写)" | int((fv2.breakpoint_snapshot() as Dictionary).get("master_seed", 0)) == 777 | (待填) | | | -| 1126 | 248 | "v2 旧断点续玩受理(缺 bonus 附随件走常规重建路径——g 裁 semver 口径)" | adr_v2.resume_adventure(fv2.breakpoint_snapshot(), {"hero_override": {}}) | (待填) | | | -| 1127 | 257 | "v5 旧档升 v7:计数域缺省补零且既有余额原样(迁移只增字段不改值——v5→v6→v7 链式)" | f13.load_from_disk() and f13.adventure_total() == 0 and f13.adventure_cleared() == 0 and f13.keystone_balance() == 9 | (待填) | | | -| 1128 | 260 | "升档幂等:v7 档写盘再载零漂移(重复迁移零副作用——锻造工坊域同批补齐)" | f13.write_to_disk() and f13.load_from_disk() and int(JSON.parse_string(FileAccess.open(paths[11], FileAccess.READ).get_as_text()).get("version", -1)) == 7 and f13.adventure_total() == 0 and f13.adventure_cleared() == 0 | (待填) | | | -| 1129 | 268 | "收束自增恰一·冒险成功局(三关全通 cleared=true:总局数+1 且冒险成功局数+1)" | f13b.adventure_total() == 1 and f13b.adventure_cleared() == 1 | (待填) | | | -| 1130 | 272 | "收束自增恰一·中止局(放弃局:仅总局数+1,冒险成功局数不动)" | f13b.adventure_total() == 2 and f13b.adventure_cleared() == 1 | (待填) | | | -| 1131 | 283 | "换代前结构存档:树域废弃重置(旧节点 id/fork 侧/forge_levels 旧域残留清退)、钥石余额零折算零变动" | f15.load_from_disk() and f15.keystone_balance() == 33 and f15.tree_nodes().is_empty() and f15.fork_sides().is_empty() | (待填) | | | -| 1132 | 288 | "废弃重置后后续购买正常受理(无残留脏节点)" | mp16.tree_buy("sp_hp") and f15.tree_level("sp_hp") == 1 | (待填) | | | -| 1133 | 297 | "v6 旧档升 v7:既有域逐位不动(钥石/树/生涯计数零折算——迁移只增不改)〔锻造工坊批 V8〕" | f16.load_from_disk() and f16.keystone_balance() == 21 and f16.tree_level("sp_hp") == 1 and f16.adventure_total() == 4 and f16.adventure_cleared() == 1 | (待填) | | | -| 1134 | 300 | "v6 旧档缺键补齐=初始持有态(六路线 0 级/直购清单空/选中=sword+light)〔锻造工坊批 V8〕" | f16.forge_route_level("sword") == 0 and f16.forge_route_level("spear") == 0 and f16.forge_route_level("light") == 0 and f16.forge_route_level("ward") == 0 and f16.forge_unlocked_purchases().is_empty() and f16.forge_weapon() == "sword" and f16.forge_armor() == "light" | (待填) | | | -| 1135 | 307 | "forge_routes 域键集恰四项(无特效域——满级特效本期不做,契约 7)〔锻造工坊批 V8/V11〕" | fr_keys == ["armor", "levels", "unlocked", "weapon"] | (待填) | | | -| 1136 | 311 | "levels 六路线键集恰全(sword/spear/staff/light/heavy/ward)〔锻造工坊批 V8〕" | lv_keys == ["heavy", "light", "spear", "staff", "sword", "ward"] | (待填) | | | -| 1137 | 316 | "forge_routes_snapshot 值拷贝出户(改快照不渗档)〔锻造工坊批 V8〕" | f16.forge_route_level("sword") == 0 | (待填) | | | -| 1138 | 324 | "收束链自增·中止局(settle_adventure 真实挂点:总局数+1、冒险成功局数不动)" | f13b.adventure_total() == 3 and f13b.adventure_cleared() == 1 | (待填) | | | -| 1139 | 328 | "收束链自增·冒险成功局(三关全通:总局数+1 且冒险成功局数+1——全链恰一)" | f13b.adventure_total() == 4 and f13b.adventure_cleared() == 2 | (待填) | | | +| 1070 | 81 | "apply 原语入账(钥石 7+3、关卡完成次数 1/1/0)" | w.meta_state.keystone_balance() == 10 and w.meta_state.career_state().clear_counts.get(str(1), 0) == 1 and w.meta_state.career_state().clear_counts.get(str(2), 0) == 1 and w.meta_state.career_state().clear_counts.get(str(3), 0) == 0 | (待填) | | | +| 1071 | 83 | "首杀标记随关卡完成置位(1/2 有、3 无)" | w.meta_state.career_state().boss_first_kill.has(str(1)) and w.meta_state.career_state().boss_first_kill.has(str(2)) and not w.meta_state.career_state().boss_first_kill.has(str(3)) | (待填) | | | +| 1072 | 85 | "生涯终身计数随 apply 自增恰一(中止局:总局数 1、冒险成功局数 0——迭代 18 卡④)" | w.meta_state.career_state().adventure_total == 1 and w.meta_state.career_state().adventure_cleared == 0 | (待填) | | | +| 1073 | 87 | "写盘成功" | w.write_to_disk() | (待填) | | | +| 1074 | 89 | "读回成功" | r.load_from_disk() | (待填) | | | +| 1075 | 90 | "钥石余额往返相等(10)" | r.meta_state.keystone_balance() == 10 | (待填) | | | +| 1076 | 91 | "树域往返相等(tree_nodes/fork_sides——迭代 21 换代承载)" | r.meta_state.tree_state().nodes.get("sp_hp", 0) == 2 and r.meta_state.tree_state().nodes.get("sp_hit", 0) == 1 and r.meta_state.tree_state().fork_sides.get("fg_posture", "") == "b" | (待填) | | | +| 1077 | 93 | "锻造工坊域往返相等(levels/unlocked/weapon/armor——迭代 23 T5)〔锻造工坊批 V8〕" | r.meta_state.forge_state().levels.get("spear", 0) == 3 and r.meta_state.forge_state().levels.get("sword", 0) == 0 and r.meta_state.forge_state().unlocked == ["spear"] and r.meta_state.forge_state().weapon == "spear" and r.meta_state.forge_state().armor == "ward" | (待填) | | | +| 1078 | 97 | "道具解锁往返相等" | r.meta_state.item_unlocked("probe_compass") and not r.meta_state.item_unlocked("rope") | (待填) | | | +| 1079 | 98 | "关卡完成次数表往返相等" | r.meta_state.career_state().clear_counts.get(str(1), 0) == 1 and r.meta_state.career_state().clear_counts.get(str(2), 0) == 1 and r.meta_state.career_state().clear_counts.get(str(3), 0) == 0 | (待填) | | | +| 1080 | 99 | "首杀标记往返相等" | r.meta_state.career_state().boss_first_kill.has(str(1)) and r.meta_state.career_state().boss_first_kill.has(str(2)) and not r.meta_state.career_state().boss_first_kill.has(str(3)) | (待填) | | | +| 1081 | 101 | "生涯终身计数往返相等(1/0)" | r.meta_state.career_state().adventure_total == 1 and r.meta_state.career_state().adventure_cleared == 0 | (待填) | | | +| 1082 | 103 | "冒险摘要往返相等(1 条,seed 99)" | r.meta_state.career_state().recent_summaries.size() == 1 and int(s0.get("master_seed", -1)) == 99 and int(s0.get("keystone_total", -1)) == 3 | (待填) | | | +| 1083 | 108 | "新档合法态(load true + 全零默认)" | f2.load_from_disk() and f2.meta_state.keystone_balance() == 0 and f2.meta_state.tree_state().nodes.get("sp_hp", 0) == 0 and not f2.meta_state.item_unlocked("anything") and f2.meta_state.career_state().clear_counts.get(str(1), 0) == 0 and f2.meta_state.career_state().recent_summaries.is_empty() | (待填) | | | +| 1084 | 111 | "新档生涯终身计数缺省全零(迭代 18 卡④两新域——白板存档完整可玩)" | f2.meta_state.career_state().adventure_total == 0 and f2.meta_state.career_state().adventure_cleared == 0 | (待填) | | | +| 1085 | 117 | "余额 5 花 8 拒绝且余额不动" | not f3.meta_state.spend_keystone(8) and f3.meta_state.keystone_balance() == 5 | (待填) | | | +| 1086 | 118 | "余额内消费成功(5→2)" | f3.meta_state.spend_keystone(3) and f3.meta_state.keystone_balance() == 2 | (待填) | | | +| 1087 | 126 | "高版本拒读(load false)" | not f5.load_from_disk() | (待填) | | | +| 1088 | 127 | "拒读后原文件字节不动" | FileAccess.open(paths[3], FileAccess.READ).get_as_text() == hv_text | (待填) | | | +| 1089 | 128 | "拒读态写盘拒绝且文件仍字节不动" | not f5.write_to_disk() and FileAccess.open(paths[3], FileAccess.READ).get_as_text() == hv_text | (待填) | | | +| 1090 | 137 | "损坏档兜底(默认数据继续)" | f6.load_from_disk() and f6.meta_state.keystone_balance() == 0 and f6.meta_state.career_state().recent_summaries.is_empty() | (待填) | | | +| 1091 | 139 | "坏档备份 .corrupt.bak 存在" | FileAccess.file_exists(paths[4] + ".corrupt.bak") | (待填) | | | +| 1092 | 140 | "备份内容 = 原坏档字节" | FileAccess.open(paths[4] + ".corrupt.bak", FileAccess.READ).get_as_text() == junk | (待填) | | | +| 1093 | 151 | "v0 旧档经迁移链入账(keystone_old 12 → 余额 12)" | f7.load_from_disk() and f7.meta_state.keystone_balance() == 12 | (待填) | | | +| 1094 | 153 | "迁移链 0→1(测试钩)→7(生产) 写盘 version == 7(v7=锻造工坊域,迭代 23 T5——v6=生涯终身计数域迭代 18 卡④)" | f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 7 | (待填) | | | +| 1095 | 165 | "域界干净——meta 域无局内资源键(发现 %s)" ↩ % str(leaked) | leaked.is_empty() | (待填) | | | +| 1096 | 173 | "摘要 cap 10(12 份 → 10 条)" | arr.size() == 10 | (待填) | | | +| 1097 | 174 | "最新在前(头 seed 11 / 尾 seed 2)" | int(arr[0].get("master_seed", -1)) == 11 and int(arr[9].get("master_seed", -1)) == 2 | (待填) | | | +| 1098 | 182 | "断点:新档无断点(合法空态——「继续上局」隐藏)" | not fbp.has_breakpoint() | (待填) | | | +| 1099 | 193 | "断点:写入+写盘成功" | fbp.has_breakpoint() and fbp.write_to_disk() | (待填) | | | +| 1100 | 195 | "断点:读回在档" | rb.load_from_disk() and rb.has_breakpoint() | (待填) | | | +| 1101 | 197 | "断点七字段往返:关卡序号/节点位/生命" | int(bp.get("level_index", -1)) == 2 and int(bp.get("node_id", -1)) == 5 and int(bp.get("hero_hp", -1)) == 13 | (待填) | | | +| 1102 | 201 | "断点七字段往返:属性加成区" | int(bp_bonus.get("atk", -1)) == 2 and int(bp_bonus.get("max_hp", -1)) == 5 and int(bp_bonus.get("armor_class", -1)) == 1 | (待填) | | | +| 1103 | 204 | "断点七字段往返:GP 钱包" | int(bp.get("gp", -1)) == 17 | (待填) | | | +| 1104 | 206 | "断点七字段往返:商店已购档" | int(bp_shop.get("hp_small", -1)) == 2 and int(bp_shop.get("atk_small", -1)) == 1 | (待填) | | | +| 1105 | 208 | "断点七字段往返:生成图代差标记键" | int(bp.get("graph_gen_v2", 0)) == 1 | (待填) | | | +| 1106 | 209 | "断点附随件往返:主种子/计数器/钥石累计" | int(bp.get("master_seed", -1)) == 424242 and int(bp.get("rng_counter", -1)) == 38 and int(bp.get("keystone", -1)) == 6 | (待填) | | | +| 1107 | 213 | "断点附随件往返:已到节点序" | bp_visited.size() == 3 and int(bp_visited[0]) == 0 and int(bp_visited[2]) == 5 | (待填) | | | +| 1108 | 216 | "断点附随件往返:道具库存" | bp_treasure.size() == 1 and String(bp_treasure[0].get("item_id", "")) == "aux_scout" | (待填) | | | +| 1109 | 219 | "断点读面值拷贝(改读面不渗档)" | int((rb.breakpoint_snapshot() as Dictionary).get("hero_hp", -1)) == 13 | (待填) | | | +| 1110 | 222 | "断点:清除+写盘(三结局收束口径)" | not rb.has_breakpoint() and rb.write_to_disk() | (待填) | | | +| 1111 | 224 | "断点:清除后读回无断点" | rc.load_from_disk() and not rc.has_breakpoint() | (待填) | | | +| 1112 | 229 | "v1 旧档迁移 v7:断点域补空(合法空态,无断点域不炸)" | fo.load_from_disk() and fo.meta_state.keystone_balance() == 4 and not fo.has_breakpoint() | (待填) | | | +| 1113 | 231 | "v1 旧档迁移后写盘 version == 7(v1→v7 链式——锻造工坊域 2026-10-12)" | fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 7 | (待填) | | | +| 1114 | 243 | "v2 旧档迁移 v3:断点域与 meta 域原样保留(缺新附随件=合法空态)" | fv2.load_from_disk() and fv2.has_breakpoint() and fv2.meta_state.keystone_balance() == 7 | (待填) | | | +| 1115 | 245 | "v2 旧断点字段零丢失(向后兼容——breakpoint 内容不因 schema 升版重写)" | int((fv2.breakpoint_snapshot() as Dictionary).get("master_seed", 0)) == 777 | (待填) | | | +| 1116 | 248 | "v2 旧断点续玩受理(缺 bonus 附随件走常规重建路径——g 裁 semver 口径)" | adr_v2.resume_adventure(fv2.breakpoint_snapshot(), {"hero_override": {}}) | (待填) | | | +| 1117 | 257 | "v5 旧档升 v7:计数域缺省补零且既有余额原样(迁移只增字段不改值——v5→v6→v7 链式)" | f13.load_from_disk() and f13.meta_state.career_state().adventure_total == 0 and f13.meta_state.career_state().adventure_cleared == 0 and f13.meta_state.keystone_balance() == 9 | (待填) | | | +| 1118 | 260 | "升档幂等:v7 档写盘再载零漂移(重复迁移零副作用——锻造工坊域同批补齐)" | f13.write_to_disk() and f13.load_from_disk() and int(JSON.parse_string(FileAccess.open(paths[11], FileAccess.READ).get_as_text()).get("version", -1)) == 7 and f13.meta_state.career_state().adventure_total == 0 and f13.meta_state.career_state().adventure_cleared == 0 | (待填) | | | +| 1119 | 268 | "收束自增恰一·冒险成功局(三关全通 cleared=true:总局数+1 且冒险成功局数+1)" | f13b.meta_state.career_state().adventure_total == 1 and f13b.meta_state.career_state().adventure_cleared == 1 | (待填) | | | +| 1120 | 272 | "收束自增恰一·中止局(放弃局:仅总局数+1,冒险成功局数不动)" | f13b.meta_state.career_state().adventure_total == 2 and f13b.meta_state.career_state().adventure_cleared == 1 | (待填) | | | +| 1121 | 283 | "换代前结构存档:树域废弃重置(旧节点 id/fork 侧/forge_levels 旧域残留清退)、钥石余额零折算零变动" | f15.load_from_disk() and f15.meta_state.keystone_balance() == 33 and f15.meta_state.tree_state().nodes.is_empty() and f15.meta_state.tree_state().fork_sides.is_empty() | (待填) | | | +| 1122 | 288 | "废弃重置后后续购买正常受理(无残留脏节点)" | mp16.tree_buy("sp_hp") and f15.meta_state.tree_state().nodes.get("sp_hp", 0) == 1 | (待填) | | | +| 1123 | 297 | "v6 旧档升 v7:既有域逐位不动(钥石/树/生涯计数零折算——迁移只增不改)〔锻造工坊批 V8〕" | f16.load_from_disk() and f16.meta_state.keystone_balance() == 21 and f16.meta_state.tree_state().nodes.get("sp_hp", 0) == 1 and f16.meta_state.career_state().adventure_total == 4 and f16.meta_state.career_state().adventure_cleared == 1 | (待填) | | | +| 1124 | 300 | "v6 旧档缺键补齐=初始持有态(六路线 0 级/直购清单空/选中=sword+light)〔锻造工坊批 V8〕" | f16.meta_state.forge_state().levels.get("sword", 0) == 0 and f16.meta_state.forge_state().levels.get("spear", 0) == 0 and f16.meta_state.forge_state().levels.get("light", 0) == 0 and f16.meta_state.forge_state().levels.get("ward", 0) == 0 and f16.meta_state.forge_state().unlocked.is_empty() and f16.meta_state.forge_state().weapon == "sword" and f16.meta_state.forge_state().armor == "light" | (待填) | | | +| 1125 | 307 | "forge_routes 域键集恰四项(无特效域——满级特效本期不做,契约 7)〔锻造工坊批 V8/V11〕" | fr_keys == ["armor", "levels", "unlocked", "weapon"] | (待填) | | | +| 1126 | 311 | "levels 六路线键集恰全(sword/spear/staff/light/heavy/ward)〔锻造工坊批 V8〕" | lv_keys == ["heavy", "light", "spear", "staff", "sword", "ward"] | (待填) | | | +| 1127 | 316 | "forge_routes_snapshot 值拷贝出户(改快照不渗档)〔锻造工坊批 V8〕" | f16.meta_state.forge_state().levels.get("sword", 0) == 0 | (待填) | | | +| 1128 | 324 | "收束链自增·中止局(settle_adventure 真实挂点:总局数+1、冒险成功局数不动)" | f13b.meta_state.career_state().adventure_total == 3 and f13b.meta_state.career_state().adventure_cleared == 1 | (待填) | | | +| 1129 | 328 | "收束链自增·冒险成功局(三关全通:总局数+1 且冒险成功局数+1——全链恰一)" | f13b.meta_state.career_state().adventure_total == 4 and f13b.meta_state.career_state().adventure_cleared == 2 | (待填) | | | ## S138 _smoke_save_slots(行 359–499 · 31 条 · 源 game/tests/unit/core/save_manager_domain_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1140 | 359 | "sv5 进档受理·槽 %d(激活+读盘)〔卡① sv5〕" ↩ % i | t.activate_slot(i) | (待填) | | | -| 1141 | 362 | "sv3 激活态写盘受理·槽 %d〔卡① sv1〕" ↩ % i | t.write_to_disk() | (待填) | | | -| 1142 | 366 | "sv1 三槽往返一致·槽 %d(异值按位读回)〔卡① sv1〕" ↩ % i | t2.activate_slot(i) and t2.keystone_balance() == 100 * i and t2.adventure_total() == 1 and t2.adventure_cleared() == (1 if i == 1 else 0) | (待填) | | | -| 1143 | 372 | "sv3 写盘受理·槽 1 加值后〔卡① sv1〕" | t.write_to_disk() | (待填) | | | -| 1144 | 373 | "sv1 位间隔离:改槽 1 槽 2 字节不动〔卡① sv1〕" | FileAccess.get_file_as_bytes(slot_tpl % 2) == slot2_before | (待填) | | | -| 1145 | 377 | "sv5 切激活至槽 3(删激活槽夹具前置)〔卡① sv5〕" | t.activate_slot(3) | (待填) | | | -| 1146 | 378 | "sv6 删档受理·激活槽 3〔卡① sv6〕" | t.delete_slot(3) | (待填) | | | -| 1147 | 379 | "sv6 删后文件不在盘+无 .tmp 残留〔卡① sv6〕" | not FileAccess.file_exists(slot_tpl % 3) and not FileAccess.file_exists(slot_tpl % 3 + ".tmp") | (待填) | | | -| 1148 | 381 | "sv6 删激活槽激活指针连清除(该槽不再可写)〔卡① sv6〕" | t.active_slot() == 0 and not t.has_write_target() | (待填) | | | -| 1149 | 385 | "sv6 删非激活槽受理·槽 2〔卡① sv6〕" | t.delete_slot(2) | (待填) | | | -| 1150 | 386 | "sv6 删非激活槽:文件不在盘+激活指针保持〔卡① sv6〕" | not FileAccess.file_exists(slot_tpl % 2) and t.active_slot() == 1 | (待填) | | | -| 1151 | 388 | "sv6 删槽 2 槽 1 字节不动(位间互隔离)〔卡① sv6〕" | FileAccess.get_file_as_bytes(slot_tpl % 1) == slot1_before | (待填) | | | -| 1152 | 397 | "sv4 新档受理·槽 3(创建即激活)〔卡① sv4〕" | t.create_slot(3) | (待填) | | | -| 1153 | 398 | "sv4 新档=全零可玩合法白板态(旧内容 999/5 零带入——钥石/总局数/断点全零)〔卡① sv4〕" | t.active_slot() == 3 and t.keystone_balance() == 0 and not t.has_breakpoint() and t.adventure_total() == 0 | (待填) | | | -| 1154 | 402 | "sv4 新档落盘=白板内容(盘面核验非 999 旧值)〔卡① sv4〕" | int((created.get("meta", {}) as Dictionary).get("keystone_balance", -1)) == 0 | (待填) | | | -| 1155 | 411 | "sv5 进档槽 1 读回(107)〔卡① sv10〕" | hv.activate_slot(1) and hv.keystone_balance() == 107 | (待填) | | | -| 1156 | 414 | "sv9 高版本槽进档拒绝(拒读禁写链按槽)〔卡① sv9〕" | not hv.activate_slot(2) | (待填) | | | -| 1157 | 415 | "sv10 激活失败恢复切换前三态(路径/激活号/数据归槽 1)〔卡① sv10〕" | hv.active_slot() == 1 and hv.keystone_balance() == 107 | (待填) | | | -| 1158 | 418 | "sv10 恢复后写盘落激活槽 1(高版本槽 2 零误写)〔卡① sv10〕" | hv.write_to_disk() and FileAccess.get_file_as_bytes(slot_tpl % 2) == slot2_hv | (待填) | | | -| 1159 | 429 | "sv9 迁移链按槽成立·槽 %d(v5→v7 各槽独立+写盘 version=7)〔卡① sv9〕" ↩ % i | mi.activate_slot(i) and mi.keystone_balance() == i * 11 and mi.write_to_disk() | (待填) | | | -| 1160 | 444 | "sv7 窥视三项逐槽正确·槽 %d〔卡① sv7〕" ↩ % i | bool(ps.get("readable", false)) and int(ps.get("keystone", -1)) == 200 * i and int(ps.get("cleared", -1)) == i and int(ps.get("total", -1)) == 5 * i | (待填) | | | -| 1161 | 447 | "sv6 删槽 3 备空槽窥视〔卡① sv6〕" | pv.delete_slot(3) | (待填) | | | -| 1162 | 449 | "sv7 空槽窥视=零值合法态〔卡① sv7〕" | bool(pe.get("readable", false)) and int(pe.get("keystone", -1)) == 0 and int(pe.get("cleared", -1)) == 0 and int(pe.get("total", -1)) == 0 | (待填) | | | -| 1163 | 458 | "sv8 坏槽窥视=显式损坏标记〔卡① sv8〕" | not bool(pb.get("readable", true)) | (待填) | | | -| 1164 | 459 | "sv8 坏槽窥视零副作用(字节+mtime 双比对不变+零 .corrupt.bak 改名)〔卡① sv8〕" | FileAccess.get_file_as_bytes(slot_tpl % 1) == bad_bytes and FileAccess.get_modified_time(slot_tpl % 1) == bad_mtime and not FileAccess.file_exists(slot_tpl % 1 + ".corrupt.bak") | (待填) | | | -| 1165 | 469 | "sv8 高版本槽窥视=不可读标记〔卡① sv8〕" | not bool(ph.get("readable", true)) | (待填) | | | -| 1166 | 470 | "sv8 高版本窥视零副作用(字节+mtime 不变+零改名)〔卡① sv8〕" | FileAccess.get_file_as_bytes(slot_tpl % 2) == hv_bytes and FileAccess.get_modified_time(slot_tpl % 2) == hv_mtime | (待填) | | | -| 1167 | 483 | "sv12 旧档字节迁入槽 1(逐域相等——余额 1234)〔卡① sv12〕" | int((migrated.get("meta", {}) as Dictionary).get("keystone_balance", -1)) == 1234 | (待填) | | | -| 1168 | 485 | "sv12 迁移后源改名留痕(.migrated.bak 在盘、原路径不在)〔卡① sv12〕" | not FileAccess.file_exists(legacy_src) and FileAccess.file_exists(legacy_src + ".migrated.bak") | (待填) | | | -| 1169 | 494 | "sv12 幂等:槽 1 已存在不重迁(预置 777 零覆盖、源保留)〔卡① sv12〕" | int((slot1_now.get("meta", {}) as Dictionary).get("keystone_balance", -1)) == 1234 and FileAccess.file_exists(legacy_src) | (待填) | | | -| 1170 | 497 | "sv12 迁移他槽零触碰(槽 2/3 字节不动)〔卡① sv12〕" | FileAccess.get_file_as_bytes(slot_tpl % 2) == s2_before and FileAccess.get_file_as_bytes(slot_tpl % 3) == s3_before | (待填) | | | +| 1130 | 359 | "sv5 进档受理·槽 %d(激活+读盘)〔卡① sv5〕" ↩ % i | t.slots.activate_slot(i) | (待填) | | | +| 1131 | 362 | "sv3 激活态写盘受理·槽 %d〔卡① sv1〕" ↩ % i | t.write_to_disk() | (待填) | | | +| 1132 | 366 | "sv1 三槽往返一致·槽 %d(异值按位读回)〔卡① sv1〕" ↩ % i | t2.slots.activate_slot(i) and t2.meta_state.keystone_balance() == 100 * i and t2.meta_state.career_state().adventure_total == 1 and t2.meta_state.career_state().adventure_cleared == (1 if i == 1 else 0) | (待填) | | | +| 1133 | 372 | "sv3 写盘受理·槽 1 加值后〔卡① sv1〕" | t.write_to_disk() | (待填) | | | +| 1134 | 373 | "sv1 位间隔离:改槽 1 槽 2 字节不动〔卡① sv1〕" | FileAccess.get_file_as_bytes(slot_tpl % 2) == slot2_before | (待填) | | | +| 1135 | 377 | "sv5 切激活至槽 3(删激活槽夹具前置)〔卡① sv5〕" | t.slots.activate_slot(3) | (待填) | | | +| 1136 | 378 | "sv6 删档受理·激活槽 3〔卡① sv6〕" | t.slots.delete_slot(3) | (待填) | | | +| 1137 | 379 | "sv6 删后文件不在盘+无 .tmp 残留〔卡① sv6〕" | not FileAccess.file_exists(slot_tpl % 3) and not FileAccess.file_exists(slot_tpl % 3 + ".tmp") | (待填) | | | +| 1138 | 381 | "sv6 删激活槽激活指针连清除(该槽不再可写)〔卡① sv6〕" | t.slots.active_slot() == 0 and not t.slots.has_write_target() | (待填) | | | +| 1139 | 385 | "sv6 删非激活槽受理·槽 2〔卡① sv6〕" | t.slots.delete_slot(2) | (待填) | | | +| 1140 | 386 | "sv6 删非激活槽:文件不在盘+激活指针保持〔卡① sv6〕" | not FileAccess.file_exists(slot_tpl % 2) and t.slots.active_slot() == 1 | (待填) | | | +| 1141 | 388 | "sv6 删槽 2 槽 1 字节不动(位间互隔离)〔卡① sv6〕" | FileAccess.get_file_as_bytes(slot_tpl % 1) == slot1_before | (待填) | | | +| 1142 | 397 | "sv4 新档受理·槽 3(创建即激活)〔卡① sv4〕" | t.slots.create_slot(3) | (待填) | | | +| 1143 | 398 | "sv4 新档=全零可玩合法白板态(旧内容 999/5 零带入——钥石/总局数/断点全零)〔卡① sv4〕" | t.slots.active_slot() == 3 and t.meta_state.keystone_balance() == 0 and not t.has_breakpoint() and t.meta_state.career_state().adventure_total == 0 | (待填) | | | +| 1144 | 402 | "sv4 新档落盘=白板内容(盘面核验非 999 旧值)〔卡① sv4〕" | int((created.get("meta", {}) as Dictionary).get("keystone_balance", -1)) == 0 | (待填) | | | +| 1145 | 411 | "sv5 进档槽 1 读回(107)〔卡① sv10〕" | hv.slots.activate_slot(1) and hv.meta_state.keystone_balance() == 107 | (待填) | | | +| 1146 | 414 | "sv9 高版本槽进档拒绝(拒读禁写链按槽)〔卡① sv9〕" | not hv.slots.activate_slot(2) | (待填) | | | +| 1147 | 415 | "sv10 激活失败恢复切换前三态(路径/激活号/数据归槽 1)〔卡① sv10〕" | hv.slots.active_slot() == 1 and hv.meta_state.keystone_balance() == 107 | (待填) | | | +| 1148 | 418 | "sv10 恢复后写盘落激活槽 1(高版本槽 2 零误写)〔卡① sv10〕" | hv.write_to_disk() and FileAccess.get_file_as_bytes(slot_tpl % 2) == slot2_hv | (待填) | | | +| 1149 | 429 | "sv9 迁移链按槽成立·槽 %d(v5→v7 各槽独立+写盘 version=7)〔卡① sv9〕" ↩ % i | mi.slots.activate_slot(i) and mi.meta_state.keystone_balance() == i * 11 and mi.write_to_disk() | (待填) | | | +| 1150 | 444 | "sv7 窥视三项逐槽正确·槽 %d〔卡① sv7〕" ↩ % i | bool(ps.get("readable", false)) and int(ps.get("keystone", -1)) == 200 * i and int(ps.get("cleared", -1)) == i and int(ps.get("total", -1)) == 5 * i | (待填) | | | +| 1151 | 447 | "sv6 删槽 3 备空槽窥视〔卡① sv6〕" | pv.slots.delete_slot(3) | (待填) | | | +| 1152 | 449 | "sv7 空槽窥视=零值合法态〔卡① sv7〕" | bool(pe.get("readable", false)) and int(pe.get("keystone", -1)) == 0 and int(pe.get("cleared", -1)) == 0 and int(pe.get("total", -1)) == 0 | (待填) | | | +| 1153 | 458 | "sv8 坏槽窥视=显式损坏标记〔卡① sv8〕" | not bool(pb.get("readable", true)) | (待填) | | | +| 1154 | 459 | "sv8 坏槽窥视零副作用(字节+mtime 双比对不变+零 .corrupt.bak 改名)〔卡① sv8〕" | FileAccess.get_file_as_bytes(slot_tpl % 1) == bad_bytes and FileAccess.get_modified_time(slot_tpl % 1) == bad_mtime and not FileAccess.file_exists(slot_tpl % 1 + ".corrupt.bak") | (待填) | | | +| 1155 | 469 | "sv8 高版本槽窥视=不可读标记〔卡① sv8〕" | not bool(ph.get("readable", true)) | (待填) | | | +| 1156 | 470 | "sv8 高版本窥视零副作用(字节+mtime 不变+零改名)〔卡① sv8〕" | FileAccess.get_file_as_bytes(slot_tpl % 2) == hv_bytes and FileAccess.get_modified_time(slot_tpl % 2) == hv_mtime | (待填) | | | +| 1157 | 483 | "sv12 旧档字节迁入槽 1(逐域相等——余额 1234)〔卡① sv12〕" | int((migrated.get("meta", {}) as Dictionary).get("keystone_balance", -1)) == 1234 | (待填) | | | +| 1158 | 485 | "sv12 迁移后源改名留痕(.migrated.bak 在盘、原路径不在)〔卡① sv12〕" | not FileAccess.file_exists(legacy_src) and FileAccess.file_exists(legacy_src + ".migrated.bak") | (待填) | | | +| 1159 | 494 | "sv12 幂等:槽 1 已存在不重迁(预置 777 零覆盖、源保留)〔卡① sv12〕" | int((slot1_now.get("meta", {}) as Dictionary).get("keystone_balance", -1)) == 1234 and FileAccess.file_exists(legacy_src) | (待填) | | | +| 1160 | 497 | "sv12 迁移他槽零触碰(槽 2/3 字节不动)〔卡① sv12〕" | FileAccess.get_file_as_bytes(slot_tpl % 2) == s2_before and FileAccess.get_file_as_bytes(slot_tpl % 3) == s3_before | (待填) | | | ## S139 _init(行 14–15 · 2 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1171 | 14 | "全部表加载 + 校验通过(含跨表护栏零 error 诊断)〔fv6〕" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | -| 1172 | 15 | "is_ready 通道随加载置位" | db.is_ready | (待填) | | | +| 1161 | 14 | "全部表加载 + 校验通过(含跨表护栏零 error 诊断)〔fv6〕" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | +| 1162 | 15 | "is_ready 通道随加载置位" | db.is_ready | (待填) | | | ## S140 _test_progress_resolution(行 55–83 · 7 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1173 | 55 | "进度解析精确命中返回原行(board_gen 首行)" | int(got.get("level_index", -99)) == int(probe_row.level_index) and int(got.get("node_depth", -99)) == int(probe_row.node_depth) and String(got.get("board_type", "")) == String(probe_row.get("board_type", "")) | (待填) | | | -| 1174 | 63 | "enemy 行集同坐标解析非空(双表精确行契约——battle 行类)" | not en_got.is_empty() | (待填) | | | -| 1175 | 68 | "深度越最大行 clamp 取该关最大深度行" | int(deep.get("level_index", -99)) == min_l and int(deep.get("node_depth", -99)) == int(min_depths[-1]) | (待填) | | | -| 1176 | 70 | "clamp 解析发警告诊断(resolve_depth_clamp)" | db.diagnostics_count("warning") >= 1 | (待填) | | | -| 1177 | 75 | "警告类别 = resolve_depth_clamp" | found_clamp | (待填) | | | -| 1178 | 78 | "同键重复请求警告去重(四元组注册表)" | db.diagnostics_count("warning") == warn_before | (待填) | | | -| 1179 | 83 | "关卡越最大行 clamp 取最大关卡行" | int(high.get("level_index", -99)) == max_l | (待填) | | | +| 1163 | 55 | "进度解析精确命中返回原行(board_gen 首行)" | int(got.get("level_index", -99)) == int(probe_row.level_index) and int(got.get("node_depth", -99)) == int(probe_row.node_depth) and String(got.get("board_type", "")) == String(probe_row.get("board_type", "")) | (待填) | | | +| 1164 | 63 | "enemy 行集同坐标解析非空(双表精确行契约——battle 行类)" | not en_got.is_empty() | (待填) | | | +| 1165 | 68 | "深度越最大行 clamp 取该关最大深度行" | int(deep.get("level_index", -99)) == min_l and int(deep.get("node_depth", -99)) == int(min_depths[-1]) | (待填) | | | +| 1166 | 70 | "clamp 解析发警告诊断(resolve_depth_clamp)" | db.diagnostics_count("warning") >= 1 | (待填) | | | +| 1167 | 75 | "警告类别 = resolve_depth_clamp" | found_clamp | (待填) | | | +| 1168 | 78 | "同键重复请求警告去重(四元组注册表)" | db.diagnostics_count("warning") == warn_before | (待填) | | | +| 1169 | 83 | "关卡越最大行 clamp 取最大关卡行" | int(high.get("level_index", -99)) == max_l | (待填) | | | ## S141 _test_variant_pool_gate(行 108–118 · 6 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1180 | 108 | "变异池 unlock 前不含未解锁行(level=unlock−1)" | not _rows_contain(before, gated_row) | (待填) | | | -| 1181 | 109 | "变异池 unlock 当级含该行" | _rows_contain(after, gated_row) | (待填) | | | -| 1182 | 113 | "∅ board_types 行通配(任一 board_type 命中)" | _rows_contain(any_pool, wildcard_row) | (待填) | | | -| 1183 | 116 | "global 行 boss 节点入池(board_types ∅ 通配)" | _rows_contain(db.variant_pool(u3, "nonexistent_board_type_tag", "boss"), global_row) | (待填) | | | -| 1184 | 117 | "global 行缺省双参调用跳过(既有调用口径零扰动)" | not _rows_contain(db.variant_pool(u3, "nonexistent_board_type_tag"), global_row) | (待填) | | | -| 1185 | 118 | "global 行 battle 节点静默跳过(门控兜底零报错)" | not _rows_contain(db.variant_pool(u3, "nonexistent_board_type_tag", "battle"), global_row) | (待填) | | | +| 1170 | 108 | "变异池 unlock 前不含未解锁行(level=unlock−1)" | not _rows_contain(before, gated_row) | (待填) | | | +| 1171 | 109 | "变异池 unlock 当级含该行" | _rows_contain(after, gated_row) | (待填) | | | +| 1172 | 113 | "∅ board_types 行通配(任一 board_type 命中)" | _rows_contain(any_pool, wildcard_row) | (待填) | | | +| 1173 | 116 | "global 行 boss 节点入池(board_types ∅ 通配)" | _rows_contain(db.combat.variation.variant_pool(u3, "nonexistent_board_type_tag", "boss"), global_row) | (待填) | | | +| 1174 | 117 | "global 行缺省双参调用跳过(既有调用口径零扰动)" | not _rows_contain(db.combat.variation.variant_pool(u3, "nonexistent_board_type_tag"), global_row) | (待填) | | | +| 1175 | 118 | "global 行 battle 节点静默跳过(门控兜底零报错)" | not _rows_contain(db.combat.variation.variant_pool(u3, "nonexistent_board_type_tag", "battle"), global_row) | (待填) | | | ## S142 _test_shop_accessors(行 133–152 · 8 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1186 | 133 | "ShopTable products 行族非空(表已已入库前提)" | false | (待填) | | | -| 1187 | 135 | "ShopTable products 行族非空" | true | (待填) | | | -| 1188 | 138 | "shop_product_row 按 product_id 精确命中首行" | got == first | (待填) | | | -| 1189 | 139 | "shop_product_row 未知 id 返回空字典(容错通道)" | db.shop_product_row("no_such_product_id__").is_empty() | (待填) | | | -| 1190 | 143 | "shop_gp_drop_row 按 tier 命中" | not db.shop_gp_drop_row(tier0).is_empty() | (待填) | | | -| 1191 | 147 | "shop_level_gp_bonus 按关卡序号命中行值" | db.shop_level_gp_bonus(blv) == int(bonus_rows[0].bonus) | (待填) | | | -| 1192 | 148 | "shop_level_gp_bonus 未登记关卡返回 0(兜底通道)" | db.shop_level_gp_bonus(-1) == 0 | (待填) | | | -| 1193 | 152 | "shop_price_level_bonus 按关卡序号命中行值" | db.shop_price_level_bonus(plv) == int(price_rows[0].bonus) | (待填) | | | +| 1176 | 133 | "ShopTable products 行族非空(表已已入库前提)" | false | (待填) | | | +| 1177 | 135 | "ShopTable products 行族非空" | true | (待填) | | | +| 1178 | 138 | "shop_product_row 按 product_id 精确命中首行" | got == first | (待填) | | | +| 1179 | 139 | "shop_product_row 未知 id 返回空字典(容错通道)" | db.run.shop.product_row("no_such_product_id__").is_empty() | (待填) | | | +| 1180 | 143 | "shop_gp_drop_row 按 tier 命中" | not db.run.shop.gp_drop_row(tier0).is_empty() | (待填) | | | +| 1181 | 147 | "shop_level_gp_bonus 按关卡序号命中行值" | db.run.shop.level_gp_bonus_at(blv) == int(bonus_rows[0].bonus) | (待填) | | | +| 1182 | 148 | "shop_level_gp_bonus 未登记关卡返回 0(兜底通道)" | db.run.shop.level_gp_bonus_at(-1) == 0 | (待填) | | | +| 1183 | 152 | "shop_price_level_bonus 按关卡序号命中行值" | db.run.shop.price_level_bonus_at(plv) == int(price_rows[0].bonus) | (待填) | | | -## S143 _test_meta_accessors(行 162–166 · 2 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) +## S143 _test_meta_accessors(行 162–171 · 2 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1194 | 162 | "meta_boss_keystone_row 按关卡序号命中" | db.meta_boss_keystone_row(klv) == key_rows[0] | (待填) | | | -| 1195 | 166 | "meta_conversion_row 按 attr 命中" | db.meta_conversion_row(cattr) == conv_rows[0] | (待填) | | | +| 1184 | 162 | "meta_boss_keystone_row 按关卡序号命中" | db.crossrun.meta.boss_keystone_row(klv) == key_rows[0] | (待填) | | | +| 1185 | 171 | "meta.conversion 行集按 attr 命中(访问器退役改直扫同判据)" | conv_hit == conv_rows[0] | (待填) | | | -## S144 _test_manifest_dir_sync(行 188–188 · 1 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) +## S144 _test_manifest_dir_sync(行 193–193 · 1 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1196 | 188 | "TABLE_MANIFEST 与 data/tables 目录逐文件同步(无缺登/无悬登)" | synced | (待填) | | | +| 1186 | 193 | "TABLE_MANIFEST 与 data/tables 目录逐文件同步(无缺登/无悬登)" | synced | (待填) | | | -## S145 _test_rowset_discipline(行 208–273 · 8 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) +## S145 _test_rowset_discipline(行 213–278 · 8 条 · 源 game/tests/unit/data_config/config_db_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1197 | 208 | "混编行集加载通过(同带键两行不同敌种相邻——行集恰一敌种校验退役的正向面)" | errs.is_empty() | (待填) | | | -| 1198 | 214 | "Boss 带混编行集加载通过(P4 乙——Boss 多行合法,无硬闸)" | errs.is_empty() | (待填) | | | -| 1199 | 225 | "行序相邻校验:同带键行被隔断报 rowset_rule 且定位带键〔fv9〕〔fv20〕" | gap_hit | (待填) | | | -| 1200 | 235 | "同带键同敌种二行被 dup_key 新键拦(纪律①——enemy_id 唯一)〔fv8〕" | dup_hit | (待填) | | | -| 1201 | 246 | "单行数量域 [1,3] 域闸不回归(UNIT_COUNT_MAX 语义 = 单种数)" | dom_hit | (待填) | | | -| 1202 | 256 | "非法 row_kind 被拒(封闭枚举 {battle, boss})〔fv7〕" | kind_hit | (待填) | | | -| 1203 | 265 | "缺 (battle, band 0) 锚行报 anchor〔fv10〕" | no_anchor_hit | (待填) | | | -| 1204 | 273 | "缺 Boss 行集报 anchor(每关 Boss 行集 ≥1)〔fv10〕" | no_boss_hit | (待填) | | | +| 1187 | 213 | "混编行集加载通过(同带键两行不同敌种相邻——行集恰一敌种校验退役的正向面)" | errs.is_empty() | (待填) | | | +| 1188 | 219 | "Boss 带混编行集加载通过(P4 乙——Boss 多行合法,无硬闸)" | errs.is_empty() | (待填) | | | +| 1189 | 230 | "行序相邻校验:同带键行被隔断报 rowset_rule 且定位带键〔fv9〕〔fv20〕" | gap_hit | (待填) | | | +| 1190 | 240 | "同带键同敌种二行被 dup_key 新键拦(纪律①——enemy_id 唯一)〔fv8〕" | dup_hit | (待填) | | | +| 1191 | 251 | "单行数量域 [1,3] 域闸不回归(UNIT_COUNT_MAX 语义 = 单种数)" | dom_hit | (待填) | | | +| 1192 | 261 | "非法 row_kind 被拒(封闭枚举 {battle, boss})〔fv7〕" | kind_hit | (待填) | | | +| 1193 | 270 | "缺 (battle, band 0) 锚行报 anchor〔fv10〕" | no_anchor_hit | (待填) | | | +| 1194 | 278 | "缺 Boss 行集报 anchor(每关 Boss 行集 ≥1)〔fv10〕" | no_boss_hit | (待填) | | | ## S146 _init(行 17–17 · 1 条 · 源 game/tests/unit/data_config/forge_table_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1205 | 17 | "生产 forge.tres 随全表加载 + validate 零违例〔锻造工坊批 V1〕" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | +| 1195 | 17 | "生产 forge.tres 随全表加载 + validate 零违例〔锻造工坊批 V1〕" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | -## S147 _test_production_rows(行 30–81 · 13 条 · 源 game/tests/unit/data_config/forge_table_test.gd · 去向:待分类回填) +## S147 _test_production_rows(行 30–86 · 13 条 · 源 game/tests/unit/data_config/forge_table_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1206 | 30 | "六路线行族规模恰 6(三武器+三甲)〔锻造工坊批 V1〕" | rows.size() == 6 | (待填) | | | -| 1207 | 35 | "路线 id 全集 = {sword, spear, staff, light, heavy, ward}〔锻造工坊批 V1〕" | ids == ["heavy", "light", "spear", "staff", "sword", "ward"] | (待填) | | | -| 1208 | 46 | "六行字段键集恰 = ROW_KEYS 闭集(无特效参数位)〔锻造工坊批 V11〕" | keys_closed | (待填) | | | -| 1209 | 53 | "初始持有恰 sword+light〔锻造工坊批 V1〕" | initial == ["light", "sword"] | (待填) | | | -| 1210 | 55 | "默认攻击类型轴 = normal(常驻持有)〔锻造工坊批 V1〕" | db.forge_default_attack_type() == "normal" | (待填) | | | -| 1211 | 56 | "默认甲型轴 = light(常驻持有)〔锻造工坊批 V1〕" | db.forge_default_armor_type() == "light" | (待填) | | | -| 1212 | 58 | "长枪/法杖 ← 长剑练满(并联双通道①)〔锻造工坊批 V1〕" | String(db.forge_route("spear").get("prereq_route", "")) == "sword" and String(db.forge_route("staff").get("prereq_route", "")) == "sword" | (待填) | | | -| 1213 | 61 | "重甲/魔法护甲 ← 轻甲练满(并联双通道①)〔锻造工坊批 V1〕" | String(db.forge_route("heavy").get("prereq_route", "")) == "light" and String(db.forge_route("ward").get("prereq_route", "")) == "light" | (待填) | | | -| 1214 | 64 | "四待解锁路线一次性解锁价 = 200〔锻造工坊批 V1〕" | int(db.forge_route("spear").get("unlock_price", 0)) == 200 and int(db.forge_route("staff").get("unlock_price", 0)) == 200 and int(db.forge_route("heavy").get("unlock_price", 0)) == 200 and int(db.forge_route("ward").get("unlock_price", 0)) == 200 | (待填) | | | -| 1215 | 70 | "甲型基础 AC = 轻 10 / 重 12 / 魔法护甲 9〔锻造工坊批 V1〕" | int(db.forge_route("light").get("base_ac", 0)) == 10 and int(db.forge_route("heavy").get("base_ac", 0)) == 12 and int(db.forge_route("ward").get("base_ac", 0)) == 9 | (待填) | | | -| 1216 | 75 | "forge_armor_route_by_type(ward) = 魔法护甲锻造〔锻造工坊批 V1〕" | String(db.forge_armor_route_by_type("ward").get("route_id", "")) == "ward" | (待填) | | | -| 1217 | 77 | "forge_weapon_route_by_type(pierce) = 长枪锻造〔锻造工坊批 V1〕" | String(db.forge_weapon_route_by_type("pierce").get("route_id", "")) == "spear" | (待填) | | | -| 1218 | 80 | "未知 route_id / 未知类型轴 = 空字典〔锻造工坊批 V1〕" | db.forge_route("blade").is_empty() and db.forge_armor_route_by_type("plate").is_empty() | (待填) | | | +| 1196 | 30 | "六路线行族规模恰 6(三武器+三甲)〔锻造工坊批 V1〕" | rows.size() == 6 | (待填) | | | +| 1197 | 35 | "路线 id 全集 = {sword, spear, staff, light, heavy, ward}〔锻造工坊批 V1〕" | ids == ["heavy", "light", "spear", "staff", "sword", "ward"] | (待填) | | | +| 1198 | 46 | "六行字段键集恰 = ROW_KEYS 闭集(无特效参数位)〔锻造工坊批 V11〕" | keys_closed | (待填) | | | +| 1199 | 53 | "初始持有恰 sword+light〔锻造工坊批 V1〕" | initial == ["light", "sword"] | (待填) | | | +| 1200 | 55 | "默认攻击类型轴 = normal(常驻持有)〔锻造工坊批 V1〕" | db.crossrun.forge.default_attack_type == "normal" | (待填) | | | +| 1201 | 56 | "默认甲型轴 = light(常驻持有)〔锻造工坊批 V1〕" | db.crossrun.forge.default_armor_type == "light" | (待填) | | | +| 1202 | 58 | "长枪/法杖 ← 长剑练满(并联双通道①)〔锻造工坊批 V1〕" | String(db.crossrun.forge.route("spear").get("prereq_route", "")) == "sword" and String(db.crossrun.forge.route("staff").get("prereq_route", "")) == "sword" | (待填) | | | +| 1203 | 61 | "重甲/魔法护甲 ← 轻甲练满(并联双通道①)〔锻造工坊批 V1〕" | String(db.crossrun.forge.route("heavy").get("prereq_route", "")) == "light" and String(db.crossrun.forge.route("ward").get("prereq_route", "")) == "light" | (待填) | | | +| 1204 | 64 | "四待解锁路线一次性解锁价 = 200〔锻造工坊批 V1〕" | int(db.crossrun.forge.route("spear").get("unlock_price", 0)) == 200 and int(db.crossrun.forge.route("staff").get("unlock_price", 0)) == 200 and int(db.crossrun.forge.route("heavy").get("unlock_price", 0)) == 200 and int(db.crossrun.forge.route("ward").get("unlock_price", 0)) == 200 | (待填) | | | +| 1205 | 70 | "甲型基础 AC = 轻 10 / 重 12 / 魔法护甲 9〔锻造工坊批 V1〕" | int(db.crossrun.forge.route("light").get("base_ac", 0)) == 10 and int(db.crossrun.forge.route("heavy").get("base_ac", 0)) == 12 and int(db.crossrun.forge.route("ward").get("base_ac", 0)) == 9 | (待填) | | | +| 1206 | 75 | "forge_armor_route_by_type(ward) = 魔法护甲锻造〔锻造工坊批 V1〕" | String(db.crossrun.forge.armor_route_by_type("ward").get("route_id", "")) == "ward" | (待填) | | | +| 1207 | 82 | "forge route_rows 按轴反查武器路线(pierce)=长枪锻造〔锻造工坊批 V1——访问器退役改直扫同判据〕" | String(wpn_hit.get("route_id", "")) == "spear" | (待填) | | | +| 1208 | 85 | "未知 route_id / 未知类型轴 = 空字典〔锻造工坊批 V1〕" | db.crossrun.forge.route("blade").is_empty() and db.crossrun.forge.armor_route_by_type("plate").is_empty() | (待填) | | | -## S148 _test_price_ladder(行 91–94 · 2 条 · 源 game/tests/unit/data_config/forge_table_test.gd · 去向:待分类回填) +## S148 _test_price_ladder(行 96–99 · 2 条 · 源 game/tests/unit/data_config/forge_table_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1219 | 91 | "长剑各级价格 = 20/30/45/67/101(基础 20×150%^已购级数)〔锻造工坊批 V2〕" | prices == [20, 30, 45, 67, 101] | (待填) | | | -| 1220 | 93 | "已满级(owned=5)再购价照算 151(cap 闸归写口,价格函数纯几何)〔锻造工坊批 V2〕" | ForgeTableScript.level_price(row, 5) == 151 | (待填) | | | +| 1209 | 96 | "长剑各级价格 = 20/30/45/67/101(基础 20×150%^已购级数)〔锻造工坊批 V2〕" | prices == [20, 30, 45, 67, 101] | (待填) | | | +| 1210 | 98 | "已满级(owned=5)再购价照算 151(cap 闸归写口,价格函数纯几何)〔锻造工坊批 V2〕" | ForgeTableScript.level_price(row, 5) == 151 | (待填) | | | -## S149 _test_negative_cases(行 133–189 · 2 条 · 源 game/tests/unit/data_config/forge_table_test.gd · 去向:待分类回填) +## S149 _test_negative_cases(行 138–194 · 2 条 · 源 game/tests/unit/data_config/forge_table_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1221 | 133 | "负向夹具(合法六行)validate 零违例〔锻造工坊批 V1〕" | t_ok.validate().is_empty() | (待填) | | | -| 1222 | 189 | "锻造 schema 负向:%s〔锻造工坊批 V1〕" ↩ % label | _has_cat(errs, expect_cat) | (待填) | | | +| 1211 | 138 | "负向夹具(合法六行)validate 零违例〔锻造工坊批 V1〕" | t_ok.validate().is_empty() | (待填) | | | +| 1212 | 194 | "锻造 schema 负向:%s〔锻造工坊批 V1〕" ↩ % label | _has_cat(errs, expect_cat) | (待填) | | | -## S150 _test_effect_kinds_closed(行 197–200 · 2 条 · 源 game/tests/unit/data_config/forge_table_test.gd · 去向:待分类回填) +## S150 _test_effect_kinds_closed(行 202–205 · 2 条 · 源 game/tests/unit/data_config/forge_table_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1223 | 197 | "效果引擎效果类型闭集 32 种(锻造工坊批零新增口径随迭代 31 卡② 扩签 forgive_seal_miss 换代)〔锻造工坊批 V11〕" | EffectEngineScript.EFFECT_KINDS.size() == 32 | (待填) | | | -| 1224 | 199 | "效果引擎事件词表在册 20 种(本批零扩签参照面)〔锻造工坊批 V11〕" | EffectEngineScript.EVENTS.size() == 20 | (待填) | | | +| 1213 | 202 | "效果引擎效果类型闭集 32 种(锻造工坊批零新增口径随迭代 31 卡② 扩签 forgive_seal_miss 换代)〔锻造工坊批 V11〕" | EffectEngineScript.EFFECT_KINDS.size() == 32 | (待填) | | | +| 1214 | 204 | "效果引擎事件词表在册 20 种(本批零扩签参照面)〔锻造工坊批 V11〕" | EffectEngineScript.EVENTS.size() == 20 | (待填) | | | ## S151 _init(行 16–17 · 1 条 · 源 game/tests/unit/data_config/level_gen_params_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1225 | 16 | "生产 level_gen_params.tres 随全表加载 + validate 零违例〔fv2〕" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | +| 1215 | 16 | "生产 level_gen_params.tres 随全表加载 + validate 零违例〔fv2〕" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | ## S152 _test_production(行 47–55 · 4 条 · 源 game/tests/unit/data_config/level_gen_params_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1226 | 47 | "访问器读回 L1 行五旋钮齐备〔fv2〕" | int(r1.get("layers", 0)) == 6 and is_equal_approx(float(r1.get("branch_density", 0.0)), 0.3) and int(r1.get("exit_count", 0)) == 1 and int(r1.get("entry_count", 0)) == 1 | (待填) | | | -| 1227 | 50 | "访问器读回 L2/L3 行层深 9/13 递增〔fv2〕" | int(r2.get("layers", 0)) == 9 and int(r3.get("layers", 0)) == 13 | (待填) | | | -| 1228 | 52 | "三行出口 = 1/2/3、战斗保底 = 4/6/8 与 §8.1 表对上〔fv2〕" | int(r1.get("battle_floor", 0)) == 4 and int(r2.get("exit_count", 0)) == 2 and int(r3.get("exit_count", 0)) == 3 and int(r3.get("battle_floor", 0)) == 8 | (待填) | | | -| 1229 | 55 | "生产表 validate 零错误〔fv2〕" | db.level_gen_params.validate().is_empty() | (待填) | | | +| 1216 | 47 | "访问器读回 L1 行五旋钮齐备〔fv2〕" | int(r1.get("layers", 0)) == 6 and is_equal_approx(float(r1.get("branch_density", 0.0)), 0.3) and int(r1.get("exit_count", 0)) == 1 and int(r1.get("entry_count", 0)) == 1 | (待填) | | | +| 1217 | 50 | "访问器读回 L2/L3 行层深 9/13 递增〔fv2〕" | int(r2.get("layers", 0)) == 9 and int(r3.get("layers", 0)) == 13 | (待填) | | | +| 1218 | 52 | "三行出口 = 1/2/3、战斗保底 = 4/6/8 与 §8.1 表对上〔fv2〕" | int(r1.get("battle_floor", 0)) == 4 and int(r2.get("exit_count", 0)) == 2 and int(r3.get("exit_count", 0)) == 3 and int(r3.get("battle_floor", 0)) == 8 | (待填) | | | +| 1219 | 55 | "生产表 validate 零错误〔fv2〕" | db.run.level_gen_params.validate().is_empty() | (待填) | | | ## S153 _test_accessor(行 62–62 · 1 条 · 源 game/tests/unit/data_config/level_gen_params_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1230 | 62 | "row_by_level_index 缺行返回空字典〔fv2〕" | t.row_by_level_index(9).is_empty() | (待填) | | | +| 1220 | 62 | "row_by_level_index 缺行返回空字典〔fv2〕" | t.row_by_level_index(9).is_empty() | (待填) | | | ## S154 _test_negative(行 72–105 · 11 条 · 源 game/tests/unit/data_config/level_gen_params_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1231 | 72 | "合法三行域校验零错误(夹具自证)〔fv2〕" | _table(base).validate().is_empty() | (待填) | | | -| 1232 | 75 | "L1 出口=2 被拒(出口 L1 恒 1 已锁)〔fv2〕" | not _table(bad_l1_exit).validate().is_empty() | (待填) | | | -| 1233 | 78 | "配比合计 0.5 被拒(四类合计 1 已锁)〔fv2〕" | not _table(bad_ratio).validate().is_empty() | (待填) | | | -| 1234 | 81 | "层深 5<6 被拒(层深下限已锁)〔fv2〕" | not _table(bad_layers).validate().is_empty() | (待填) | | | -| 1235 | 85 | "层深增幅倒挂被拒(递增且增幅变大)〔fv2〕" | not _table(bad_growth).validate().is_empty() | (待填) | | | -| 1236 | 88 | "出口 4 越域 [1,3] 被拒〔fv2〕" | not _table(bad_exit).validate().is_empty() | (待填) | | | -| 1237 | 92 | "岔口去路数 (2,4) 被拒(恒 2/3 已锁)〔fv2〕" | not _table([bad_fork_row, base[1], base[2]]).validate().is_empty() | (待填) | | | -| 1238 | 97 | "字段键集非 ROW_KEYS 闭集被拒〔fv2〕" | not _table([bad_keys_row, base[1], base[2]]).validate().is_empty() | (待填) | | | -| 1239 | 100 | "键重复行被拒〔fv2〕" | not _table([base[0], base[0]]).validate().is_empty() | (待填) | | | -| 1240 | 102 | "战斗保底 -1 被拒(≥0)〔fv2〕" | not _table([_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, -1), base[1], base[2]]).validate().is_empty() | (待填) | | | -| 1241 | 105 | "空表域校验零错误(合法态)〔fv2〕" | _table([]).validate().is_empty() | (待填) | | | +| 1221 | 72 | "合法三行域校验零错误(夹具自证)〔fv2〕" | _table(base).validate().is_empty() | (待填) | | | +| 1222 | 75 | "L1 出口=2 被拒(出口 L1 恒 1 已锁)〔fv2〕" | not _table(bad_l1_exit).validate().is_empty() | (待填) | | | +| 1223 | 78 | "配比合计 0.5 被拒(四类合计 1 已锁)〔fv2〕" | not _table(bad_ratio).validate().is_empty() | (待填) | | | +| 1224 | 81 | "层深 5<6 被拒(层深下限已锁)〔fv2〕" | not _table(bad_layers).validate().is_empty() | (待填) | | | +| 1225 | 85 | "层深增幅倒挂被拒(递增且增幅变大)〔fv2〕" | not _table(bad_growth).validate().is_empty() | (待填) | | | +| 1226 | 88 | "出口 4 越域 [1,3] 被拒〔fv2〕" | not _table(bad_exit).validate().is_empty() | (待填) | | | +| 1227 | 92 | "岔口去路数 (2,4) 被拒(恒 2/3 已锁)〔fv2〕" | not _table([bad_fork_row, base[1], base[2]]).validate().is_empty() | (待填) | | | +| 1228 | 97 | "字段键集非 ROW_KEYS 闭集被拒〔fv2〕" | not _table([bad_keys_row, base[1], base[2]]).validate().is_empty() | (待填) | | | +| 1229 | 100 | "键重复行被拒〔fv2〕" | not _table([base[0], base[0]]).validate().is_empty() | (待填) | | | +| 1230 | 102 | "战斗保底 -1 被拒(≥0)〔fv2〕" | not _table([_row(1, 6, 0.3, [0.5, 0.2, 0.2, 0.1], 1, 1, -1), base[1], base[2]]).validate().is_empty() | (待填) | | | +| 1231 | 105 | "空表域校验零错误(合法态)〔fv2〕" | _table([]).validate().is_empty() | (待填) | | | ## S155 _test_derive_determinism(行 36–40 · 2 条 · 源 game/tests/unit/data_config/rng_service_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1242 | 36 | "同种子两实例 derive_at(0..7) 逐位一致" | same | (待填) | | | -| 1243 | 40 | "derive_at 为纯函数不推计数器" | a2.counter() == before | (待填) | | | +| 1232 | 36 | "同种子两实例 derive_at(0..7) 逐位一致" | same | (待填) | | | +| 1233 | 40 | "derive_at 为纯函数不推计数器" | a2.counter() == before | (待填) | | | ## S156 _test_counter_ledger(行 51–57 · 2 条 · 源 game/tests/unit/data_config/rng_service_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1244 | 51 | "open_stream 逐次推进全局计数器(+1/次)" | c1 == c0 + 1 and c2 == c0 + 2 | (待填) | | | -| 1245 | 57 | "同 scope 异用途标签派生种子一致(标签不参与派生)" | s1 == s2 | (待填) | | | +| 1234 | 51 | "open_stream 逐次推进全局计数器(+1/次)" | c1 == c0 + 1 and c2 == c0 + 2 | (待填) | | | +| 1235 | 57 | "同 scope 异用途标签派生种子一致(标签不参与派生)" | s1 == s2 | (待填) | | | ## S157 _test_stream_draw_determinism(行 71–71 · 1 条 · 源 game/tests/unit/data_config/rng_service_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1246 | 71 | "同种子两实例同标签流取数 12 次逐位一致" | same | (待填) | | | +| 1236 | 71 | "同种子两实例同标签流取数 12 次逐位一致" | same | (待填) | | | ## S158 _test_negative_seed_normalization(行 79–79 · 1 条 · 源 game/tests/unit/data_config/rng_service_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1247 | 79 | "负种子 abs 归一化后派生与正种子一致" | pos.derive_at(0) == neg.derive_at(0) | (待填) | | | +| 1237 | 79 | "负种子 abs 归一化后派生与正种子一致" | pos.derive_at(0) == neg.derive_at(0) | (待填) | | | ## S159 _smoke_initialize(行 28–53 · 13 条 · 源 game/tests/unit/data_config/rng_stream_boot_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1248 | 28 | "全部表加载 + 校验通过(含跨表护栏)" | load_ok | (待填) | | | -| 1249 | 37 | "统一流向量 m=42 scope=0(=旧 board_gen scope0 连续性锚)" | rng.derive_at(0) == 8672514112661456564 | (待填) | | | -| 1250 | 38 | "统一流向量 m=42 scope=1" | rng.derive_at(1) == 8834291164406037215 | (待填) | | | -| 1251 | 39 | "统一流向量 m=42 scope=2" | rng.derive_at(2) == 7714373262647006282 | (待填) | | | -| 1252 | 40 | "统一流向量 m=42 scope=3" | rng.derive_at(3) == 1280102845971471347 | (待填) | | | -| 1253 | 41 | "统一流向量 m=42 scope=4" | rng.derive_at(4) == 82585501446482399 | (待填) | | | -| 1254 | 42 | "统一流向量 m=42 scope=5" | rng.derive_at(5) == 8078924421474470922 | (待填) | | | -| 1255 | 43 | "统一流向量 m=42 scope=6" | rng.derive_at(6) == 6638052740430622424 | (待填) | | | -| 1256 | 44 | "统一流向量 m=42 scope=7" | rng.derive_at(7) == 8264196488417721294 | (待填) | | | -| 1257 | 45 | "derive_at 纯函数恒等" | rng.derive_at(0) == rng.derive_at(0) | (待填) | | | -| 1258 | 49 | "open_stream 全局计数器单调 +1 ×2" | rng.counter() == 2 | (待填) | | | -| 1259 | 50 | "首个取号 = scope 0(顺序确定)" | s0.seed == rng.derive_at(0) and s1.seed == rng.derive_at(1) | (待填) | | | -| 1260 | 53 | "new_adventure 重置全局计数器" | rng.counter() == 1 and s2.seed == rng.derive_at(0) | (待填) | | | +| 1238 | 28 | "全部表加载 + 校验通过(含跨表护栏)" | load_ok | (待填) | | | +| 1239 | 37 | "统一流向量 m=42 scope=0(=旧 board_gen scope0 连续性锚)" | rng.derive_at(0) == 8672514112661456564 | (待填) | | | +| 1240 | 38 | "统一流向量 m=42 scope=1" | rng.derive_at(1) == 8834291164406037215 | (待填) | | | +| 1241 | 39 | "统一流向量 m=42 scope=2" | rng.derive_at(2) == 7714373262647006282 | (待填) | | | +| 1242 | 40 | "统一流向量 m=42 scope=3" | rng.derive_at(3) == 1280102845971471347 | (待填) | | | +| 1243 | 41 | "统一流向量 m=42 scope=4" | rng.derive_at(4) == 82585501446482399 | (待填) | | | +| 1244 | 42 | "统一流向量 m=42 scope=5" | rng.derive_at(5) == 8078924421474470922 | (待填) | | | +| 1245 | 43 | "统一流向量 m=42 scope=6" | rng.derive_at(6) == 6638052740430622424 | (待填) | | | +| 1246 | 44 | "统一流向量 m=42 scope=7" | rng.derive_at(7) == 8264196488417721294 | (待填) | | | +| 1247 | 45 | "derive_at 纯函数恒等" | rng.derive_at(0) == rng.derive_at(0) | (待填) | | | +| 1248 | 49 | "open_stream 全局计数器单调 +1 ×2" | rng.counter() == 2 | (待填) | | | +| 1249 | 50 | "首个取号 = scope 0(顺序确定)" | s0.seed == rng.derive_at(0) and s1.seed == rng.derive_at(1) | (待填) | | | +| 1250 | 53 | "new_adventure 重置全局计数器" | rng.counter() == 1 and s2.seed == rng.derive_at(0) | (待填) | | | ## S160 _smoke_adventure_table(行 92–203 · 18 条 · 源 game/tests/unit/data_config/table_structure_guard_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1261 | 92 | "db.adventure 已加载(adventure.tres 入 manifest)" | _db.adventure != null | (待填) | | | -| 1262 | 95 | "AdventureTable.validate() 零违例(全键域检+档位配额结构锚在案)" | _db.adventure.validate().is_empty() | (待填) | | | -| 1263 | 96 | "clear_heal_pct ∈ [0,100](%d——关卡完成部分回血比例)" ↩ % int(_db.adventure.clear_heal_pct) | int(_db.adventure.clear_heal_pct) >= 0 and int(_db.adventure.clear_heal_pct) <= 100 | (待填) | | | -| 1264 | 99 | "档位配额结构锚(四级 70/25/7/0——迭代 31 卡② 净删两件投放):%s = %d 张" ↩ % [tier, int(_db.adventure.TIER_QUOTAS[tier])] | int(_db.adventure.tier_total(tier)) == int(_db.adventure.TIER_QUOTAS[tier]) | (待填) | | | -| 1265 | 101 | "令牌池总量 = 102 张(四级 普通 70/优秀 25/稀有 7/传说 0——迭代 31 卡② 净删两件投放 104→102)〔凭证裁削批 V5〕" | int(_db.adventure.deck_total()) == 102 | (待填) | | | -| 1266 | 102 | "数值对(四级换代承袭):小GP 20/大GP 50 · 小增伤 +1/大 +2 · 小生命 +5/大 +10 · 小护甲 +1/大 +2" | int(_db.adventure.curio_value("gp", "common")) == 20 and int(_db.adventure.curio_value("gp", "excellent")) == 50 and int(_db.adventure.curio_value("attack", "common")) == 1 and int(_db.adventure.curio_value("attack", "excellent")) == 2 and int(_db.adventure.curio_value("max_hp", "common")) == 5 and int(_db.adventure.curio_value("max_hp", "excellent")) == 10 and int(_db.adventure.curio_value("armor", "common")) == 1 and int(_db.adventure.curio_value("armor", "excellent")) == 2 | (待填) | | | -| 1267 | 107 | "恢复件 +6 当即回血(优秀档承袭)· AP +1(稀有档承袭)〔专长批 V14 换代〕" | int(_db.adventure.curio_value("heal", "excellent")) == 6 and int(_db.adventure.curio_value("ap", "rare")) == 1 | (待填) | | | -| 1268 | 109 | "cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕" | int(_db.adventure.curio_cap("gp")) == 99 and int(_db.adventure.curio_cap("attack")) == 99 | (待填) | | | -| 1269 | 111 | "同调位读数 null 档早退分支求值 0(迭代 21 T4 槽位收编——谓词表已退役)〔天赋树批 V14·核代码增补〕" | Attunement.slots(_db, null) == 0 | (待填) | | | -| 1270 | 113 | "同调位读数源=树同调位节点点亮数(非空档——socket1 点亮 1 件计 1)〔天赋树批 V14·核代码增补〕" | Attunement.slots(_db, _TreeSaveMock.new()) == 1 | (待填) | | | -| 1271 | 120 | "新目录 12 件全量落表(旧 12 行删除;装配词表=活行 12 件零重复)〔秘宝批 V1〕" | trows.size() == 12 and tid_set.size() == 12 | (待填) | | | -| 1272 | 131 | "命名锁定:12 件 item_id 与 display_name 逐件=正式名(C2 代号即名)〔秘宝批 V2 初筛〕" | names_ok | (待填) | | | -| 1273 | 159 | "逐行解锁方式与契约 8 分配一致(钥石 8 件带价格、条件 4 件带谓词行互斥)+三档 7/3/2〔秘宝批 V1〕" | alloc_ok and tier_map["common"] == 7 and tier_map["excellent"] == 3 and tier_map["rare"] == 2 | (待填) | | | -| 1274 | 162 | "装配→战斗贯通:enc.attunement=[morning_order] 起战 attune_has=true(注入面+只读出口)〔秘宝批换主语〕" | orch_l.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "attunement": ["morning_order"]}) and orch_l.attune_has("morning_order") | (待填) | | | -| 1275 | 166 | "空装配基线:不带 attunement 起战 attune_has=false(off 态零扰动)〔秘宝批换主语〕" | orch_e.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}) and not orch_e.attune_has("morning_order") | (待填) | | | -| 1276 | 181 | "新 12 行效果词零棋盘改写类(危险格/安全格分布同池冻结——真视/开格动作外无写面)〔秘宝批 V9〕" | v9_ok | (待填) | | | -| 1277 | 188 | "效果三方向覆盖在册(信息/行动资源/操作能力=plan·抉择刷新——12 件目录结构对齐设计权威)〔秘宝批 V10 覆盖半〕" | tag_set.has("info") and tag_set.has("action") and tag_set.has("plan") | (待填) | | | -| 1278 | 202 | "装配面空目录合法态:白板档 12 件全未解锁且 mode 分派正确(空目录=合法可玩态)〔秘宝批 V18〕" | v18_ok and locked_n == 12 | (待填) | | | +| 1251 | 92 | "db.adventure 已加载(adventure.tres 入 manifest)" | _db.run.adventure != null | (待填) | | | +| 1252 | 95 | "AdventureTable.validate() 零违例(全键域检+档位配额结构锚在案)" | _db.run.adventure.validate().is_empty() | (待填) | | | +| 1253 | 96 | "clear_heal_pct ∈ [0,100](%d——关卡完成部分回血比例)" ↩ % int(_db.run.adventure.clear_heal_pct) | int(_db.run.adventure.clear_heal_pct) >= 0 and int(_db.run.adventure.clear_heal_pct) <= 100 | (待填) | | | +| 1254 | 99 | "档位配额结构锚(四级 70/25/7/0——迭代 31 卡② 净删两件投放):%s = %d 张" ↩ % [tier, int(_db.run.adventure.TIER_QUOTAS[tier])] | int(_db.run.adventure.tier_total(tier)) == int(_db.run.adventure.TIER_QUOTAS[tier]) | (待填) | | | +| 1255 | 101 | "令牌池总量 = 102 张(四级 普通 70/优秀 25/稀有 7/传说 0——迭代 31 卡② 净删两件投放 104→102)〔凭证裁削批 V5〕" | int(_db.run.adventure.deck_total()) == 102 | (待填) | | | +| 1256 | 102 | "数值对(四级换代承袭):小GP 20/大GP 50 · 小增伤 +1/大 +2 · 小生命 +5/大 +10 · 小护甲 +1/大 +2" | int(_db.run.adventure.curio_value("gp", "common")) == 20 and int(_db.run.adventure.curio_value("gp", "excellent")) == 50 and int(_db.run.adventure.curio_value("attack", "common")) == 1 and int(_db.run.adventure.curio_value("attack", "excellent")) == 2 and int(_db.run.adventure.curio_value("max_hp", "common")) == 5 and int(_db.run.adventure.curio_value("max_hp", "excellent")) == 10 and int(_db.run.adventure.curio_value("armor", "common")) == 1 and int(_db.run.adventure.curio_value("armor", "excellent")) == 2 | (待填) | | | +| 1257 | 107 | "恢复件 +6 当即回血(优秀档承袭)· AP +1(稀有档承袭)〔专长批 V14 换代〕" | int(_db.run.adventure.curio_value("heal", "excellent")) == 6 and int(_db.run.adventure.curio_value("ap", "rare")) == 1 | (待填) | | | +| 1258 | 109 | "cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕" | int(_db.run.adventure.curio_cap("gp")) == 99 and int(_db.run.adventure.curio_cap("attack")) == 99 | (待填) | | | +| 1259 | 111 | "同调位读数 null 档早退分支求值 0(迭代 21 T4 槽位收编——谓词表已退役)〔天赋树批 V14·核代码增补〕" | Attunement.slots(_db, null) == 0 | (待填) | | | +| 1260 | 113 | "同调位读数源=树同调位节点点亮数(非空档——socket1 点亮 1 件计 1)〔天赋树批 V14·核代码增补〕" | Attunement.slots(_db, _TreeSaveMock.new()) == 1 | (待填) | | | +| 1261 | 120 | "新目录 12 件全量落表(旧 12 行删除;装配词表=活行 12 件零重复)〔秘宝批 V1〕" | trows.size() == 12 and tid_set.size() == 12 | (待填) | | | +| 1262 | 131 | "命名锁定:12 件 item_id 与 display_name 逐件=正式名(C2 代号即名)〔秘宝批 V2 初筛〕" | names_ok | (待填) | | | +| 1263 | 159 | "逐行解锁方式与契约 8 分配一致(钥石 8 件带价格、条件 4 件带谓词行互斥)+三档 7/3/2〔秘宝批 V1〕" | alloc_ok and tier_map["common"] == 7 and tier_map["excellent"] == 3 and tier_map["rare"] == 2 | (待填) | | | +| 1264 | 162 | "装配→战斗贯通:enc.attunement=[morning_order] 起战 attune_has=true(注入面+只读出口)〔秘宝批换主语〕" | orch_l.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "attunement": ["morning_order"]}) and orch_l.attune_has("morning_order") | (待填) | | | +| 1265 | 166 | "空装配基线:不带 attunement 起战 attune_has=false(off 态零扰动)〔秘宝批换主语〕" | orch_e.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}) and not orch_e.attune_has("morning_order") | (待填) | | | +| 1266 | 181 | "新 12 行效果词零棋盘改写类(危险格/安全格分布同池冻结——真视/开格动作外无写面)〔秘宝批 V9〕" | v9_ok | (待填) | | | +| 1267 | 188 | "效果三方向覆盖在册(信息/行动资源/操作能力=plan·抉择刷新——12 件目录结构对齐设计权威)〔秘宝批 V10 覆盖半〕" | tag_set.has("info") and tag_set.has("action") and tag_set.has("plan") | (待填) | | | +| 1268 | 202 | "装配面空目录合法态:白板档 12 件全未解锁且 mode 分派正确(空目录=合法可玩态)〔秘宝批 V18〕" | v18_ok and locked_n == 12 | (待填) | | | ## S161 _smoke_enemy_bands(行 229–261 · 4 条 · 源 game/tests/unit/data_config/table_structure_guard_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1279 | 229 | "敌人表 28 行且键序 = 覆盖表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕" | _db.enemy.rows.size() == 28 and got_keys == expect_keys | (待填) | | | -| 1280 | 236 | "hobgoblin 行在 (1,battle,3) 留任随迁(缺口行不删不补——补齐归敌种内容批)〔fv12〕〔fv25〕" | int(hob.get("level_index", -1)) == 1 and String(hob.get("row_kind", "")) == "battle" \\ and int(hob.get("depth_band", -1)) == 3 | (待填) | | | -| 1281 | 239 | "hobgoblin 缺口登记随行(表文件注释在位——缺口不静默)〔fv25〕" | FileAccess.get_file_as_string("res://data/tables/enemy.tres").contains("hobgoblin 行为缺口行") | (待填) | | | -| 1282 | 260 | "三关 Boss 行集带键 = 覆盖表(模板终点 4/9/9 在册+生成图终点 6/8/9/11/12/13 扩带)%s〔fv13〕" ↩ % boss_detail | boss_ok | (待填) | | | +| 1269 | 229 | "敌人表 28 行且键序 = 覆盖表序((L,row_kind,depth_band,enemy_id) 逐行比对——文件行序不变)〔fv11〕" | _db.combat.enemy.rows.size() == 28 and got_keys == expect_keys | (待填) | | | +| 1270 | 236 | "hobgoblin 行在 (1,battle,3) 留任随迁(缺口行不删不补——补齐归敌种内容批)〔fv12〕〔fv25〕" | int(hob.get("level_index", -1)) == 1 and String(hob.get("row_kind", "")) == "battle" \\ and int(hob.get("depth_band", -1)) == 3 | (待填) | | | +| 1271 | 239 | "hobgoblin 缺口登记随行(表文件注释在位——缺口不静默)〔fv25〕" | FileAccess.get_file_as_string("res://data/tables/enemy.tres").contains("hobgoblin 行为缺口行") | (待填) | | | +| 1272 | 260 | "三关 Boss 行集带键 = 覆盖表(模板终点 4/9/9 在册+生成图终点 6/8/9/11/12/13 扩带)%s〔fv13〕" ↩ % boss_detail | boss_ok | (待填) | | | ## S162 _smoke_ac_swap_guard(行 272–380 · 13 条 · 源 game/tests/unit/data_config/table_structure_guard_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1283 | 272 | "状态表 defense_up 行已退役(无双轨)" | _db.status_effect_row("defense_up").is_empty() | (待填) | | | -| 1284 | 274 | "ac_up 行在位(hero · +1 ac · 只续时不叠层——2026-09-20 主角增益续时批)" | not ac_row.is_empty() and String(ac_row.get("target_side", "")) == "hero" \\ and String(ac_row.get("unit", "")) == "ac" and int(ac_row.get("magnitude", 0)) == 1 \\ and String(ac_row.get("stack_policy", "")) == "refresh" | (待填) | | | -| 1285 | 282 | "奖励表 defense 正向行 effect_id = ac_up(显示名盾牌加护,权重/tier 不变)" | String(ac_reward.get("effect_id", "")) == "ac_up" and String(ac_reward.get("display_name", "")) == "盾牌加护" | (待填) | | | -| 1286 | 284 | "effect_pool_map 无 defense_up/ac_up 键(减伤不入池——AC 不经乘区)" | not _db.settlement.core.effect_pool_map.has("defense_up") and not _db.settlement.core.effect_pool_map.has("ac_up") | (待填) | | | -| 1287 | 299 | "hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" ↩ % [ wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.settlement.core.pool_clamp_lo)] | wall_ok | (待填) | | | -| 1288 | 305 | "hero_defense 池映射数==0(绊线钉——红=已加映射,去核上方护栏是否已同步激活)" | hd_maps == 0 | (待填) | | | -| 1289 | 307 | "主角 armor_class 在域内(%d——暂定基准随表走)" ↩ % int(_db.hero.armor_class) | int(_db.hero.armor_class) >= 0 and int(_db.hero.armor_class) <= 30 | (待填) | | | -| 1290 | 324 | "敌方 AC 行值在已裁带宽内(杂鱼 0~5·重击手/重装手 8~9·Boss 8~12——迭代 5 返工批 v1 重投,越带行%s)" ↩ % [ ac_detail if ac_detail != "" else "无"] | ac_band | (待填) | | | -| 1291 | 332 | "Boss AC 关卡台阶严格递增(%d < %d < %d)" ↩ % [int(boss_ac.get(1, 0)), int(boss_ac.get(2, 0)), int(boss_ac.get(3, 0))] | int(boss_ac.get(1, 0)) < int(boss_ac.get(2, 0)) and int(boss_ac.get(2, 0)) < int(boss_ac.get(3, 0)) | (待填) | | | -| 1292 | 348 | "熊地精首遇带(1,battle,%d)降档锚定(hp 22 · ac 2 · 池 attack6/charge2/heavy2 零改动——迭代 24 N1;首遇带数据驱动定位)〔fv21〕" ↩ % l12_band | l12_band >= 0 and not l12_row.is_empty() and int(l12_row.get("hp", -1)) == 22 \\ and int(l12_row.get("armor_class", -1)) == 2 \\ and l12_pool == {"attack": 6, "charge": 2, "heavy": 2} | (待填) | | | -| 1293 | 370 | "第一关相邻深度带总量比 ≤ 2.5(12/10/22/35/72——迭代 24 N1 曲线护栏,带键升序;越带%s)" ↩ % [ curve_detail if curve_detail != "" else "无"] | curve_ok | (待填) | | | -| 1294 | 375 | "掷点参数拍板值在位(d20 / nat20 暴击 ×200%% / 下限 1 / attack_bonus 0 / armor_roll 流)" | int(_db.settlement.damage_roll.roll_faces) == 20 and int(_db.settlement.damage_roll.crit_roll) == 20 \\ and int(_db.settlement.damage_roll.crit_multiplier) == 200 and int(_db.settlement.damage_roll.damage_floor) == 1 \\ and int(_db.settlement.damage_roll.attack_bonus) == 0 and String(_db.settlement.damage_roll.armor_roll_stream) == "armor_roll" | (待填) | | | -| 1295 | 379 | "档位表拍板值在位(缺口 1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25——无零档)" | bounds == [3, 7] and discounts == [75, 50, 25] | (待填) | | | +| 1273 | 272 | "状态表 defense_up 行已退役(无双轨)" | _db.combat.status_effects.row_by_id("defense_up").is_empty() | (待填) | | | +| 1274 | 274 | "ac_up 行在位(hero · +1 ac · 只续时不叠层——2026-09-20 主角增益续时批)" | not ac_row.is_empty() and String(ac_row.get("target_side", "")) == "hero" \\ and String(ac_row.get("unit", "")) == "ac" and int(ac_row.get("magnitude", 0)) == 1 \\ and String(ac_row.get("stack_policy", "")) == "refresh" | (待填) | | | +| 1275 | 282 | "奖励表 defense 正向行 effect_id = ac_up(显示名盾牌加护,权重/tier 不变)" | String(ac_reward.get("effect_id", "")) == "ac_up" and String(ac_reward.get("display_name", "")) == "盾牌加护" | (待填) | | | +| 1276 | 284 | "effect_pool_map 无 defense_up/ac_up 键(减伤不入池——AC 不经乘区)" | not _db.combat.settlement.core.effect_pool_map.has("defense_up") and not _db.combat.settlement.core.effect_pool_map.has("ac_up") | (待填) | | | +| 1277 | 299 | "hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" ↩ % [ wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.combat.settlement.core.pool_clamp_lo)] | wall_ok | (待填) | | | +| 1278 | 305 | "hero_defense 池映射数==0(绊线钉——红=已加映射,去核上方护栏是否已同步激活)" | hd_maps == 0 | (待填) | | | +| 1279 | 307 | "主角 armor_class 在域内(%d——暂定基准随表走)" ↩ % int(_db.combat.hero.armor_class) | int(_db.combat.hero.armor_class) >= 0 and int(_db.combat.hero.armor_class) <= 30 | (待填) | | | +| 1280 | 324 | "敌方 AC 行值在已裁带宽内(杂鱼 0~5·重击手/重装手 8~9·Boss 8~12——迭代 5 返工批 v1 重投,越带行%s)" ↩ % [ ac_detail if ac_detail != "" else "无"] | ac_band | (待填) | | | +| 1281 | 332 | "Boss AC 关卡台阶严格递增(%d < %d < %d)" ↩ % [int(boss_ac.get(1, 0)), int(boss_ac.get(2, 0)), int(boss_ac.get(3, 0))] | int(boss_ac.get(1, 0)) < int(boss_ac.get(2, 0)) and int(boss_ac.get(2, 0)) < int(boss_ac.get(3, 0)) | (待填) | | | +| 1282 | 348 | "熊地精首遇带(1,battle,%d)降档锚定(hp 22 · ac 2 · 池 attack6/charge2/heavy2 零改动——迭代 24 N1;首遇带数据驱动定位)〔fv21〕" ↩ % l12_band | l12_band >= 0 and not l12_row.is_empty() and int(l12_row.get("hp", -1)) == 22 \\ and int(l12_row.get("armor_class", -1)) == 2 \\ and l12_pool == {"attack": 6, "charge": 2, "heavy": 2} | (待填) | | | +| 1283 | 370 | "第一关相邻深度带总量比 ≤ 2.5(12/10/22/35/72——迭代 24 N1 曲线护栏,带键升序;越带%s)" ↩ % [ curve_detail if curve_detail != "" else "无"] | curve_ok | (待填) | | | +| 1284 | 375 | "掷点参数拍板值在位(d20 / nat20 暴击 ×200%% / 下限 1 / attack_bonus 0 / armor_roll 流)" | int(_db.combat.settlement.damage_roll.roll_faces) == 20 and int(_db.combat.settlement.damage_roll.crit_roll) == 20 \\ and int(_db.combat.settlement.damage_roll.crit_multiplier) == 200 and int(_db.combat.settlement.damage_roll.damage_floor) == 1 \\ and int(_db.combat.settlement.damage_roll.attack_bonus) == 0 and String(_db.combat.settlement.damage_roll.armor_roll_stream) == "armor_roll" | (待填) | | | +| 1285 | 379 | "档位表拍板值在位(缺口 1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25——无零档)" | bounds == [3, 7] and discounts == [75, 50, 25] | (待填) | | | ## S163 _smoke_variation_framework(行 387–415 · 9 条 · 源 game/tests/unit/data_config/table_structure_guard_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1296 | 387 | "变异清单 6 行(荆棘丛/藤蔓缠绕/岩浆/烬覆 + 落石/矿尘——首批类型定稿批 + 第三关内容批;贫矿脉 2026-10-16 用户令屏蔽删行)" | _db.variation.rows.size() == 6 | (待填) | | | -| 1297 | 389 | "变异表校验零错误(6 行全过域与跨字段互锁——含 scope=global 三互锁负样本)" | _db.variation.validate().is_empty() | (待填) | | | -| 1298 | 394 | "L1 dungeon 候选池 = 荆棘丛+藤蔓缠绕(unlock + board_types 双闸)" | pool1_ids == ["thorn", "vine"] | (待填) | | | -| 1299 | 398 | "L2 underdark 候选池 = 岩浆+烬覆(第二关解锁)" | pool2_ids == ["magma", "ash"] | (待填) | | | -| 1300 | 399 | "unlock 门控:L1 不进 underdark 行(岩浆/烬覆第二关解锁)" | _db.variant_pool(1, "underdark").is_empty() | (待填) | | | -| 1301 | 403 | "board_types 门控:L2 dungeon 板池 = 荆棘丛+藤蔓缠绕(解锁累积;岩浆/烬覆被 board_types ∅ 交集排除)" | pool2d_ids == ["thorn", "vine"] | (待填) | | | -| 1302 | 405 | "清单外 variant_id 返回空(读取通道在位)" | _db.variation.row_by_id("nonexistent").is_empty() | (待填) | | | -| 1303 | 411 | "跨字段互锁负样本①:flat_damage + placement=any 被拒(只出现在未打开的格子)" | _errs_has_cross_field(vt.validate()) | (待填) | | | -| 1304 | 415 | "跨字段互锁负样本②:hide_number + trigger≠on_number_shown 被拒" | _errs_has_cross_field(vt.validate()) | (待填) | | | +| 1286 | 387 | "变异清单 6 行(荆棘丛/藤蔓缠绕/岩浆/烬覆 + 落石/矿尘——首批类型定稿批 + 第三关内容批;贫矿脉 2026-10-16 用户令屏蔽删行)" | _db.combat.variation.rows.size() == 6 | (待填) | | | +| 1287 | 389 | "变异表校验零错误(6 行全过域与跨字段互锁——含 scope=global 三互锁负样本)" | _db.combat.variation.validate().is_empty() | (待填) | | | +| 1288 | 394 | "L1 dungeon 候选池 = 荆棘丛+藤蔓缠绕(unlock + board_types 双闸)" | pool1_ids == ["thorn", "vine"] | (待填) | | | +| 1289 | 398 | "L2 underdark 候选池 = 岩浆+烬覆(第二关解锁)" | pool2_ids == ["magma", "ash"] | (待填) | | | +| 1290 | 399 | "unlock 门控:L1 不进 underdark 行(岩浆/烬覆第二关解锁)" | _db.combat.variation.variant_pool(1, "underdark").is_empty() | (待填) | | | +| 1291 | 403 | "board_types 门控:L2 dungeon 板池 = 荆棘丛+藤蔓缠绕(解锁累积;岩浆/烬覆被 board_types ∅ 交集排除)" | pool2d_ids == ["thorn", "vine"] | (待填) | | | +| 1292 | 405 | "清单外 variant_id 返回空(读取通道在位)" | _db.combat.variation.row_by_id("nonexistent").is_empty() | (待填) | | | +| 1293 | 411 | "跨字段互锁负样本①:flat_damage + placement=any 被拒(只出现在未打开的格子)" | _errs_has_cross_field(vt.validate()) | (待填) | | | +| 1294 | 415 | "跨字段互锁负样本②:hide_number + trigger≠on_number_shown 被拒" | _errs_has_cross_field(vt.validate()) | (待填) | | | ## S164 _smoke_difficulty_table(行 422–444 · 8 条 · 源 game/tests/unit/data_config/table_structure_guard_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1305 | 422 | "DifficultyTable MVP 空表加载(结构位)" | _db.difficulty != null and _db.difficulty.rows.is_empty() | (待填) | | | -| 1306 | 425 | "spawn 受理" | enemy_system.spawn(1, 0, int(_db.hero.max_hp)) | (待填) | | | -| 1307 | 428 | "空表零修饰(hp=%d atk=%d = 行值 %d/%d)" ↩ % [int(unit.hp), int(unit.atk), int(d0.hp), int(d0.atk)] | int(unit.hp) == int(d0.hp) and int(unit.atk) == int(d0.atk) | (待填) | | | -| 1308 | 433 | "修饰后 spawn 受理" | enemy_system_b.spawn(1, 0, int(_db.hero.max_hp)) | (待填) | | | -| 1309 | 435 | "hp 修饰 ×1.5(%d→%d,max_hp 同步)" ↩ % [int(d0.hp), int(g2.hp)] | int(g2.hp) == int(round(float(d0.hp) * 1.5)) and int(g2.max_hp) == int(g2.hp) | (待填) | | | -| 1310 | 437 | "atk 修饰 ×2.0(%d→%d)" ↩ % [int(d0.atk), int(g2.atk)] | int(g2.atk) == int(d0.atk) * 2 | (待填) | | | -| 1311 | 442 | "越域修饰 spawn 受理" | enemy_system_c.spawn(1, 0, int(_db.hero.max_hp)) | (待填) | | | -| 1312 | 443 | "越域乘数 clamp 至 10(%d→%d)" ↩ % [int(d0.hp), int(enemy_system_c.all_enemies()[0].hp)] | int(enemy_system_c.all_enemies()[0].hp) == int(d0.hp) * 10 | (待填) | | | +| 1295 | 422 | "DifficultyTable MVP 空表加载(结构位)" | _db.run.difficulty != null and _db.run.difficulty.rows.is_empty() | (待填) | | | +| 1296 | 425 | "spawn 受理" | enemy_system.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}) | (待填) | | | +| 1297 | 428 | "空表零修饰(hp=%d atk=%d = 行值 %d/%d)" ↩ % [int(unit.hp), int(unit.atk), int(d0.hp), int(d0.atk)] | int(unit.hp) == int(d0.hp) and int(unit.atk) == int(d0.atk) | (待填) | | | +| 1298 | 433 | "修饰后 spawn 受理" | enemy_system_b.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}) | (待填) | | | +| 1299 | 435 | "hp 修饰 ×1.5(%d→%d,max_hp 同步)" ↩ % [int(d0.hp), int(g2.hp)] | int(g2.hp) == int(round(float(d0.hp) * 1.5)) and int(g2.max_hp) == int(g2.hp) | (待填) | | | +| 1300 | 437 | "atk 修饰 ×2.0(%d→%d)" ↩ % [int(d0.atk), int(g2.atk)] | int(g2.atk) == int(d0.atk) * 2 | (待填) | | | +| 1301 | 442 | "越域修饰 spawn 受理" | enemy_system_c.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}) | (待填) | | | +| 1302 | 443 | "越域乘数 clamp 至 10(%d→%d)" ↩ % [int(d0.hp), int(enemy_system_c.all_enemies()[0].hp)] | int(enemy_system_c.all_enemies()[0].hp) == int(d0.hp) * 10 | (待填) | | | ## S165 _smoke_nine_types_gp_stream(行 452–494 · 6 条 · 源 game/tests/unit/data_config/table_structure_guard_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1313 | 452 | "格面资源十型封闭(第九型 gp 九型批 2026-09-20 / 第十型 treasure_item 秘宝内容批 2026-09-29——词表 v1.2)" | RewardTable.CELL_TYPES.size() == 10 and RewardTable.CELL_TYPES.has("gp") and RewardTable.CELL_TYPES.has("treasure_item") | (待填) | | | -| 1314 | 467 | "奖励字典 18 行(九型×正负 18 格——aux 挖格投放四行已退役 2026-10-01 用户拍板甲:战斗/挖格不掉道具,秘宝唯一来路=三选一共享池〔节点/关卡完成〕;清图槽独立批尾不计)" | cell_rows == 18 | (待填) | | | -| 1315 | 474 | "危险格负向直伤 %s power=%d(卡⑧投值)" ↩ % [rid, int(mine_power[rid])] | int(row.get("power", -1)) == int(mine_power[rid]) | (待填) | | | -| 1316 | 487 | "正向 cell 行权重 ≥1 全在池 / 负向行权重恒 0 不入池(正向合计 %d——配置推导)" ↩ % pos_weight | pos_all_in_pool and neg_all_zero | (待填) | | | -| 1317 | 489 | "gp 型两行(GP 袋 gp_gain 正向 weight 2 / 破财 gp_loss 负向 weight 0 不入池)" | gp_pos == 2 and gp_neg == 0 | (待填) | | | -| 1318 | 493 | "derive_at 纯函数不推计数器(统一流全局计数器恒 0——并流后纯函数语义不变,gp_drop 专用向量随分盐作废、统一向量中央断言)" | rng.counter() == 0 | (待填) | | | +| 1303 | 452 | "格面资源十型封闭(第九型 gp 九型批 2026-09-20 / 第十型 treasure_item 秘宝内容批 2026-09-29——词表 v1.2)" | RewardTable.CELL_TYPES.size() == 10 and RewardTable.CELL_TYPES.has("gp") and RewardTable.CELL_TYPES.has("treasure_item") | (待填) | | | +| 1304 | 467 | "奖励字典 18 行(九型×正负 18 格——aux 挖格投放四行已退役 2026-10-01 用户拍板甲:战斗/挖格不掉道具,秘宝唯一来路=三选一共享池〔节点/关卡完成〕;清图槽独立批尾不计)" | cell_rows == 18 | (待填) | | | +| 1305 | 474 | "危险格负向直伤 %s power=%d(卡⑧投值)" ↩ % [rid, int(mine_power[rid])] | int(row.get("power", -1)) == int(mine_power[rid]) | (待填) | | | +| 1306 | 487 | "正向 cell 行权重 ≥1 全在池 / 负向行权重恒 0 不入池(正向合计 %d——配置推导)" ↩ % pos_weight | pos_all_in_pool and neg_all_zero | (待填) | | | +| 1307 | 489 | "gp 型两行(GP 袋 gp_gain 正向 weight 2 / 破财 gp_loss 负向 weight 0 不入池)" | gp_pos == 2 and gp_neg == 0 | (待填) | | | +| 1308 | 493 | "derive_at 纯函数不推计数器(统一流全局计数器恒 0——并流后纯函数语义不变,gp_drop 专用向量随分盐作废、统一向量中央断言)" | rng.counter() == 0 | (待填) | | | ## S166 _smoke_meta(行 502–515 · 4 条 · 源 game/tests/unit/data_config/table_structure_guard_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1319 | 502 | "MetaTable 非空 + validate 零错(三行族词表封闭/唯一/连续/域界/first≥repeat/ratio 全过)" | meta_tbl != null and meta_tbl.validate().is_empty() | (待填) | | | -| 1320 | 504 | "MetaTable 行族规模 3/3/2(Boss/换算恰三/Y 升级两档——forge_products 迭代 21 退役、workshop_items 迭代 22 退役)" | meta_tbl.boss_keystone.size() == 3 and meta_tbl.conversion.size() == 3 and meta_tbl.attune_refresh_upgrade.size() == 2 | (待填) | | | -| 1321 | 510 | "conversion attr 词表封闭恰全(三属性轴——meta_* 访问器可达)" | attrs_ok | (待填) | | | -| 1322 | 515 | "boss_keystone L1–L3 行连续可达且 first_kill ≥ repeat_kill 全行(首杀全量/重复小额)" | boss_ok | (待填) | | | +| 1309 | 502 | "MetaTable 非空 + validate 零错(三行族词表封闭/唯一/连续/域界/first≥repeat/ratio 全过)" | meta_tbl != null and meta_tbl.validate().is_empty() | (待填) | | | +| 1310 | 504 | "MetaTable 行族规模 3/3/2(Boss/换算恰三/Y 升级两档——forge_products 迭代 21 退役、workshop_items 迭代 22 退役)" | meta_tbl.boss_keystone.size() == 3 and meta_tbl.conversion.size() == 3 and meta_tbl.attune_refresh_upgrade.size() == 2 | (待填) | | | +| 1311 | 510 | "conversion attr 词表封闭恰全(三属性轴——meta_* 访问器可达)" | attrs_ok | (待填) | | | +| 1312 | 515 | "boss_keystone L1–L3 行连续可达且 first_kill ≥ repeat_kill 全行(首杀全量/重复小额)" | boss_ok | (待填) | | | ## S167 _init(行 13–14 · 2 条 · 源 game/tests/unit/example_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1323 | 13 | "headless SceneTree 协议可用(quit(code) 通道)" | _test_headless_protocol() | (待填) | | | -| 1324 | 14 | "命名规范可执行:test_[scenario]_[expected]" | _test_naming_convention_followed() | (待填) | | | +| 1313 | 13 | "headless SceneTree 协议可用(quit(code) 通道)" | _test_headless_protocol() | (待填) | | | +| 1314 | 14 | "命名规范可执行:test_[scenario]_[expected]" | _test_naming_convention_followed() | (待填) | | | ## S168 _test_numeric_lines_carry_values(行 31–36 · 3 条 · 源 game/tests/unit/presentation/battle_narrator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1325 | 31 | "休整叙述带入回复量与当前血量(实际「%s」)" ↩ % rest | rest.contains("12") and rest.contains("30") and rest.contains("40") | (待填) | | | -| 1326 | 34 | "商店叙述带入 GP 数(实际「%s」)" ↩ % shop | shop.contains("77") | (待填) | | | -| 1327 | 35 | "奖励行与休整行互异(不是同一句套所有场景)" | NarratorScript.reward_line() != rest | (待填) | | | +| 1315 | 31 | "休整叙述带入回复量与当前血量(实际「%s」)" ↩ % rest | rest.contains("12") and rest.contains("30") and rest.contains("40") | (待填) | | | +| 1316 | 34 | "商店叙述带入 GP 数(实际「%s」)" ↩ % shop | shop.contains("77") | (待填) | | | +| 1317 | 35 | "奖励行与休整行互异(不是同一句套所有场景)" | NarratorScript.reward_line() != rest | (待填) | | | ## S169 _test_end_text_differs_by_result(行 42–48 · 4 条 · 源 game/tests/unit/presentation/battle_narrator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1328 | 42 | "关卡收束态恒为「关卡完成!」(实际「%s」)" ↩ % level | level == "关卡完成!" | (待填) | | | -| 1329 | 45 | "单场胜负文案互异(胜「%s」/负「%s」)" ↩ % [win, lose] | win != lose | (待填) | | | -| 1330 | 46 | "胜利文案非空(实际「%s」)" ↩ % win | win != "" | (待填) | | | -| 1331 | 48 | "未知结果有兜底文案不吐空串(实际「%s」)" ↩ % unknown | unknown != "" | (待填) | | | +| 1318 | 42 | "关卡收束态恒为「关卡完成!」(实际「%s」)" ↩ % level | level == "关卡完成!" | (待填) | | | +| 1319 | 45 | "单场胜负文案互异(胜「%s」/负「%s」)" ↩ % [win, lose] | win != lose | (待填) | | | +| 1320 | 46 | "胜利文案非空(实际「%s」)" ↩ % win | win != "" | (待填) | | | +| 1321 | 48 | "未知结果有兜底文案不吐空串(实际「%s」)" ↩ % unknown | unknown != "" | (待填) | | | ## S170 _test_phase_text_nonempty(行 53–55 · 2 条 · 源 game/tests/unit/presentation/battle_narrator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1332 | 53 | "相位叙述非空(实际「%s」)" ↩ % t | t != "" | (待填) | | | -| 1333 | 55 | "Boss 在场时叙述带入 Boss 名(实际「%s」)" ↩ % boss | boss.contains("灰矮人石君王") | (待填) | | | +| 1322 | 53 | "相位叙述非空(实际「%s」)" ↩ % t | t != "" | (待填) | | | +| 1323 | 55 | "Boss 在场时叙述带入 Boss 名(实际「%s」)" ↩ % boss | boss.contains("灰矮人石君王") | (待填) | | | ## S171 _test_boss_name_uses_boss_rowset(行 82–83 · 2 条 · 源 game/tests/unit/presentation/battle_narrator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1334 | 82 | "Boss 名取自 Boss 行集首行(期望「%s」实际「%s」)〔fv24〕" ↩ % [boss_expect, got] | got == boss_expect | (待填) | | | -| 1335 | 83 | "Boss 名与同深度战斗行集名不同(期望「%s」≠「%s」——行类路由可判别)〔fv24〕" ↩ % [boss_expect, battle_name] | boss_expect != battle_name | (待填) | | | +| 1324 | 82 | "Boss 名取自 Boss 行集首行(期望「%s」实际「%s」)〔fv24〕" ↩ % [boss_expect, got] | got == boss_expect | (待填) | | | +| 1325 | 83 | "Boss 名与同深度战斗行集名不同(期望「%s」≠「%s」——行类路由可判别)〔fv24〕" ↩ % [boss_expect, battle_name] | boss_expect != battle_name | (待填) | | | ## S172 _test_level_clear_wording(行 90–101 · 4 条 · 源 game/tests/unit/presentation/battle_narrator_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1336 | 90 | "关卡完成回血行带入回血量与生命区间(实际「%s」)〔口径换代 V1〕" ↩ % healed | healed.contains("关卡完成回血 +5(生命 10 → 15)") | (待填) | | | -| 1337 | 93 | "满血零回血合法态文案(实际「%s」)〔口径换代 V1〕" ↩ % full_hp | full_hp.contains("生命已满——关卡完成回血 0(合法态)") | (待填) | | | -| 1338 | 96 | "奖励节点头部行渠道句点明关卡完成三选一(实际「%s」)〔口径换代 V2〕" ↩ % node_header | node_header.contains("与关卡完成专长三选一同库分流") | (待填) | | | -| 1339 | 100 | "冒险摘要完成关数行(实际「%s」)〔口径换代 V3〕" ↩ % summary | summary.contains("完成 1 关 · 止步第 2 层") and not summary.contains("通关") | (待填) | | | +| 1326 | 90 | "关卡完成回血行带入回血量与生命区间(实际「%s」)〔口径换代 V1〕" ↩ % healed | healed.contains("关卡完成回血 +5(生命 10 → 15)") | (待填) | | | +| 1327 | 93 | "满血零回血合法态文案(实际「%s」)〔口径换代 V1〕" ↩ % full_hp | full_hp.contains("生命已满——关卡完成回血 0(合法态)") | (待填) | | | +| 1328 | 96 | "奖励节点头部行渠道句点明关卡完成三选一(实际「%s」)〔口径换代 V2〕" ↩ % node_header | node_header.contains("与关卡完成专长三选一同库分流") | (待填) | | | +| 1329 | 100 | "冒险摘要完成关数行(实际「%s」)〔口径换代 V3〕" ↩ % summary | summary.contains("完成 1 关 · 止步第 2 层") and not summary.contains("通关") | (待填) | | | ## S173 _run(行 27–102 · 13 条 · 源 game/tests/unit/presentation/forge_workbench_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1340 | 27 | "ConfigDb 加载(工作间测试夹具前提)" | false | (待填) | | | -| 1341 | 44 | "锁定态:工作间不可见(forge_workshop 钥匙未点亮)〔锻造工坊批 V13〕" | not ov._forge_workbench.visible and ov._forge_lock_l.visible | (待填) | | | -| 1342 | 46 | "锁定态:两区零行板(占位纪律——零购买路径)〔锻造工坊批 V13〕" | ov._forge_zone_weapon.get_child_count() == 0 and ov._forge_zone_armor.get_child_count() == 0 | (待填) | | | -| 1343 | 52 | "解锁态:工作间可见(钥匙点亮展开)〔锻造工坊批 V13〕" | ov._forge_workbench.visible | (待填) | | | -| 1344 | 53 | "两区各三路线(武器锻造区 3 + 护甲锻造区 3)〔锻造工坊批 V13〕" | ov._forge_zone_weapon.get_child_count() == 3 and ov._forge_zone_armor.get_child_count() == 3 | (待填) | | | -| 1345 | 56 | "六路线行册恰全(sword/spear/staff/light/heavy/ward)〔锻造工坊批 V13〕" | rows.has("sword") and rows.has("spear") and rows.has("staff") and rows.has("light") and rows.has("heavy") and rows.has("ward") | (待填) | | | -| 1346 | 63 | "长剑行:购买钮「锻造 20」(0 级位几何价)+选中钮「当前」禁用〔锻造工坊批 V13〕" | rsw.get("buy_btn") != null and String(rsw.buy_btn.text) == "锻造 20" and not rsw.buy_btn.disabled and rsw.get("select_btn") != null and String(rsw.select_btn.text) == "当前" and rsw.select_btn.disabled | (待填) | | | -| 1347 | 66 | "轻甲行:购买钮「锻造 20」+选中钮「当前」禁用〔锻造工坊批 V13〕" | rli.get("buy_btn") != null and String(rli.buy_btn.text) == "锻造 20" and String(rli.select_btn.text) == "当前" and rli.select_btn.disabled | (待填) | | | -| 1348 | 73 | "长枪行:解锁钮「解锁 200」+无购买钮+无选中钮〔锻造工坊批 V13〕" | rsp.get("unlock_btn") != null and String(rsp.unlock_btn.text) == "解锁 200" and not rsp.unlock_btn.disabled and rsp.get("buy_btn") == null and rsp.get("select_btn") == null | (待填) | | | -| 1349 | 76 | "魔法护甲行:解锁钮「解锁 200」+状态含并联双通道文案(练满轻甲锻造 或 200 钥石直购)〔锻造工坊批 V13〕" | rwa.get("unlock_btn") != null and String(rwa.status_l.text).contains("练满轻甲锻造") and String(rwa.status_l.text).contains("200") | (待填) | | | -| 1350 | 84 | "余额 0:长剑购买钮禁用+长枪解锁钮禁用(余额联动常态防线)〔锻造工坊批 V13〕" | rows.sword.buy_btn.disabled and rows.spear.unlock_btn.disabled | (待填) | | | -| 1351 | 92 | "长剑满级:无购买钮+状态「已满级」+长枪派生解锁出解锁钮消失(练满通道)〔锻造工坊批 V13〕" | rows.sword.get("buy_btn") == null and String(rows.sword.status_l.text) == "已满级" and rows.spear.get("unlock_btn") == null and rows.spear.get("buy_btn") != null | (待填) | | | -| 1352 | 101 | "三意图信号外发(buy=spear/unlock=heavy/select=spear——UI 只发意图)〔锻造工坊批 V13〕" | buys == ["spear"] and unlocks == ["heavy"] and selects == ["spear"] | (待填) | | | +| 1330 | 27 | "ConfigDb 加载(工作间测试夹具前提)" | false | (待填) | | | +| 1331 | 44 | "锁定态:工作间不可见(forge_workshop 钥匙未点亮)〔锻造工坊批 V13〕" | not ov._forge_workbench.visible and ov._forge_lock_l.visible | (待填) | | | +| 1332 | 46 | "锁定态:两区零行板(占位纪律——零购买路径)〔锻造工坊批 V13〕" | ov._forge_zone_weapon.get_child_count() == 0 and ov._forge_zone_armor.get_child_count() == 0 | (待填) | | | +| 1333 | 52 | "解锁态:工作间可见(钥匙点亮展开)〔锻造工坊批 V13〕" | ov._forge_workbench.visible | (待填) | | | +| 1334 | 53 | "两区各三路线(武器锻造区 3 + 护甲锻造区 3)〔锻造工坊批 V13〕" | ov._forge_zone_weapon.get_child_count() == 3 and ov._forge_zone_armor.get_child_count() == 3 | (待填) | | | +| 1335 | 56 | "六路线行册恰全(sword/spear/staff/light/heavy/ward)〔锻造工坊批 V13〕" | rows.has("sword") and rows.has("spear") and rows.has("staff") and rows.has("light") and rows.has("heavy") and rows.has("ward") | (待填) | | | +| 1336 | 63 | "长剑行:购买钮「锻造 20」(0 级位几何价)+选中钮「当前」禁用〔锻造工坊批 V13〕" | rsw.get("buy_btn") != null and String(rsw.buy_btn.text) == "锻造 20" and not rsw.buy_btn.disabled and rsw.get("select_btn") != null and String(rsw.select_btn.text) == "当前" and rsw.select_btn.disabled | (待填) | | | +| 1337 | 66 | "轻甲行:购买钮「锻造 20」+选中钮「当前」禁用〔锻造工坊批 V13〕" | rli.get("buy_btn") != null and String(rli.buy_btn.text) == "锻造 20" and String(rli.select_btn.text) == "当前" and rli.select_btn.disabled | (待填) | | | +| 1338 | 73 | "长枪行:解锁钮「解锁 200」+无购买钮+无选中钮〔锻造工坊批 V13〕" | rsp.get("unlock_btn") != null and String(rsp.unlock_btn.text) == "解锁 200" and not rsp.unlock_btn.disabled and rsp.get("buy_btn") == null and rsp.get("select_btn") == null | (待填) | | | +| 1339 | 76 | "魔法护甲行:解锁钮「解锁 200」+状态含并联双通道文案(练满轻甲锻造 或 200 钥石直购)〔锻造工坊批 V13〕" | rwa.get("unlock_btn") != null and String(rwa.status_l.text).contains("练满轻甲锻造") and String(rwa.status_l.text).contains("200") | (待填) | | | +| 1340 | 84 | "余额 0:长剑购买钮禁用+长枪解锁钮禁用(余额联动常态防线)〔锻造工坊批 V13〕" | rows.sword.buy_btn.disabled and rows.spear.unlock_btn.disabled | (待填) | | | +| 1341 | 92 | "长剑满级:无购买钮+状态「已满级」+长枪派生解锁出解锁钮消失(练满通道)〔锻造工坊批 V13〕" | rows.sword.get("buy_btn") == null and String(rows.sword.status_l.text) == "已满级" and rows.spear.get("unlock_btn") == null and rows.spear.get("buy_btn") != null | (待填) | | | +| 1342 | 101 | "三意图信号外发(buy=spear/unlock=heavy/select=spear——UI 只发意图)〔锻造工坊批 V13〕" | buys == ["spear"] and unlocks == ["heavy"] and selects == ["spear"] | (待填) | | | ## S174 _test_accepted_text(行 20–20 · 1 条 · 源 game/tests/unit/presentation/hud_presenter_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1353 | 20 | "受理态文案固定为「执行完成。」(实际「%s」)" ↩ % t | t == "执行完成。" | (待填) | | | +| 1343 | 20 | "受理态文案固定为「执行完成。」(实际「%s」)" ↩ % t | t == "执行完成。" | (待填) | | | ## S175 _test_reject_text_by_reason(行 36–38 · 2 条 · 源 game/tests/unit/presentation/hud_presenter_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1354 | 36 | "棋盘面各拒绝原因文案逐类正确(错 %d 项 %s)" ↩ % [bad.size(), str(bad)] | bad.is_empty() | (待填) | | | -| 1355 | 38 | "资源面拒绝文案为「AP 不足。」(实际「%s」)" ↩ % ap | ap == "AP 不足。" | (待填) | | | +| 1344 | 36 | "棋盘面各拒绝原因文案逐类正确(错 %d 项 %s)" ↩ % [bad.size(), str(bad)] | bad.is_empty() | (待填) | | | +| 1345 | 38 | "资源面拒绝文案为「AP 不足。」(实际「%s」)" ↩ % ap | ap == "AP 不足。" | (待填) | | | ## S176 _test_unknown_reason_fallback(行 43–47 · 2 条 · 源 game/tests/unit/presentation/hud_presenter_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1356 | 43 | "未知拒绝原因兜底为「动作被拒。」而非空串(实际「%s」)" ↩ % t | t == "动作被拒。" | (待填) | | | -| 1357 | 47 | "棋盘面未知子因兜底带前缀可诊断(实际「%s」)" ↩ % sub | sub.begins_with("棋盘面拒绝: ") | (待填) | | | +| 1346 | 43 | "未知拒绝原因兜底为「动作被拒。」而非空串(实际「%s」)" ↩ % t | t == "动作被拒。" | (待填) | | | +| 1347 | 47 | "棋盘面未知子因兜底带前缀可诊断(实际「%s」)" ↩ % sub | sub.begins_with("棋盘面拒绝: ") | (待填) | | | ## S177 _test_fog_hides_type(行 51–56 · 3 条 · 源 game/tests/unit/presentation/level_map_info_fog_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1358 | 51 | "可见集内节点:图标/类型文字均绘制〔fv2〕" | bool(shown["draw_icon"]) and bool(shown["draw_label"]) | (待填) | | | -| 1359 | 54 | "雾下节点:图标与类型文字均不绘制(显隐语义,非灰化)〔fv2〕" | not bool(fogged["draw_icon"]) and not bool(fogged["draw_label"]) | (待填) | | | -| 1360 | 56 | "雾下非 Boss 节点无 Boss 描边〔fv2〕" | not bool(fogged["boss_border"]) | (待填) | | | +| 1348 | 51 | "可见集内节点:图标/类型文字均绘制〔fv2〕" | bool(shown["draw_icon"]) and bool(shown["draw_label"]) | (待填) | | | +| 1349 | 54 | "雾下节点:图标与类型文字均不绘制(显隐语义,非灰化)〔fv2〕" | not bool(fogged["draw_icon"]) and not bool(fogged["draw_label"]) | (待填) | | | +| 1350 | 56 | "雾下非 Boss 节点无 Boss 描边〔fv2〕" | not bool(fogged["boss_border"]) | (待填) | | | ## S178 _test_boss_exempt_and_multi_boss(行 65–73 · 4 条 · 源 game/tests/unit/presentation/level_map_info_fog_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1361 | 65 | "多终点:两个 Boss 节点均带 Boss 描边〔fv3〕" | bool(boss_a["boss_border"]) and bool(boss_b["boss_border"]) | (待填) | | | -| 1362 | 67 | "Boss 不入雾:可见集与图标/文字三通道均在〔fv3〕" | bool(boss_a["draw_icon"]) and bool(boss_a["draw_label"]) and bool(boss_b["draw_icon"]) | (待填) | | | -| 1363 | 70 | "Boss 豁免独立于可见集:不在 type_vis 的 Boss 仍出示类型〔fv3〕" | bool(far_boss["draw_icon"]) and bool(far_boss["draw_label"]) | (待填) | | | -| 1364 | 73 | "Boss 豁免不扩散:雾下非 Boss 仍隐类型〔fv3〕" | not bool(fogged_battle["draw_icon"]) | (待填) | | | +| 1351 | 65 | "多终点:两个 Boss 节点均带 Boss 描边〔fv3〕" | bool(boss_a["boss_border"]) and bool(boss_b["boss_border"]) | (待填) | | | +| 1352 | 67 | "Boss 不入雾:可见集与图标/文字三通道均在〔fv3〕" | bool(boss_a["draw_icon"]) and bool(boss_a["draw_label"]) and bool(boss_b["draw_icon"]) | (待填) | | | +| 1353 | 70 | "Boss 豁免独立于可见集:不在 type_vis 的 Boss 仍出示类型〔fv3〕" | bool(far_boss["draw_icon"]) and bool(far_boss["draw_label"]) | (待填) | | | +| 1354 | 73 | "Boss 豁免不扩散:雾下非 Boss 仍隐类型〔fv3〕" | not bool(fogged_battle["draw_icon"]) | (待填) | | | ## S179 _test_details_never_shown(行 87–89 · 2 条 · 源 game/tests/unit/presentation/level_map_info_fog_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1365 | 87 | "决策面不消费内容键:携带 enemy_id/reward_id 与不带者输出逐键相等〔fv5〕" | same | (待填) | | | -| 1366 | 88 | "label_text 不含内容值(实际「%s」)〔fv5〕" ↩ % String(b["label_text"]) | not String(b["label_text"]).contains("goblin_secret") and not String(b["label_text"]).contains("reward_secret") | (待填) | | | +| 1355 | 87 | "决策面不消费内容键:携带 enemy_id/reward_id 与不带者输出逐键相等〔fv5〕" | same | (待填) | | | +| 1356 | 88 | "label_text 不含内容值(实际「%s」)〔fv5〕" ↩ % String(b["label_text"]) | not String(b["label_text"]).contains("goblin_secret") and not String(b["label_text"]).contains("reward_secret") | (待填) | | | ## S180 _test_label_three_states(行 96–101 · 3 条 · 源 game/tests/unit/presentation/level_map_info_fog_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1367 | 96 | "当前位文案=「当前位置」〔fv2〕" | String(ScreenScript.node_display(node, 3, [3], true, false)["label_text"]) == "当前位置" | (待填) | | | -| 1368 | 98 | "候选文案=「战斗 · 可选」〔fv2〕" | String(ScreenScript.node_display(node, 3, [3], false, true)["label_text"]) == "战斗 · 可选" | (待填) | | | -| 1369 | 100 | "其余文案=类型名「战斗」〔fv2〕" | String(ScreenScript.node_display(node, 3, [3], false, false)["label_text"]) == "战斗" | (待填) | | | +| 1357 | 96 | "当前位文案=「当前位置」〔fv2〕" | String(ScreenScript.node_display(node, 3, [3], true, false)["label_text"]) == "当前位置" | (待填) | | | +| 1358 | 98 | "候选文案=「战斗 · 可选」〔fv2〕" | String(ScreenScript.node_display(node, 3, [3], false, true)["label_text"]) == "战斗 · 可选" | (待填) | | | +| 1359 | 100 | "其余文案=类型名「战斗」〔fv2〕" | String(ScreenScript.node_display(node, 3, [3], false, false)["label_text"]) == "战斗" | (待填) | | | ## S181 _test_big_graph_fit(行 165–191 · 6 条 · 源 game/tests/unit/presentation/level_map_info_fog_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1370 | 165 | "大图夹具规模:≥11 层 × ≥6 槽(实测最大层 %d/最大槽 %d)〔fv8〕" ↩ % [maxl, maxslot] | maxl >= 10 and maxslot >= 5 | (待填) | | | -| 1371 | 171 | "大图拟合:画布 min.x 覆盖最深节点(%.1f ≥ %.1f)〔fv8〕" ↩ % [min_size.x, need_w] | min_size.x >= need_w - 0.01 | (待填) | | | -| 1372 | 173 | "大图拟合:画布 min.y 覆盖最宽层底节点(%.1f ≥ %.1f)〔fv8〕" ↩ % [min_size.y, need_h] | min_size.y >= need_h - 0.01 | (待填) | | | -| 1373 | 181 | "大图布局:全节点方块落在拟合画布内(origin=(%.1f,%.1f) size=(%.1f,%.1f))〔fv8〕" ↩ % [origin.x, origin.y, min_size.x, min_size.y] | all_in | (待填) | | | -| 1374 | 188 | "大图呈现:两个终点 Boss 均有描边标记〔fv8〕" | disp.has(boss_ids[0]) and disp.has(boss_ids[1]) and bool(disp[boss_ids[0]]["boss_border"]) and bool(disp[boss_ids[1]]["boss_border"]) | (待填) | | | -| 1375 | 191 | "大图呈现:入口停点远处战斗节点(句柄 32,层 6)类型入雾〔fv8〕" | not bool(disp[32]["draw_icon"]) | (待填) | | | +| 1360 | 165 | "大图夹具规模:≥11 层 × ≥6 槽(实测最大层 %d/最大槽 %d)〔fv8〕" ↩ % [maxl, maxslot] | maxl >= 10 and maxslot >= 5 | (待填) | | | +| 1361 | 171 | "大图拟合:画布 min.x 覆盖最深节点(%.1f ≥ %.1f)〔fv8〕" ↩ % [min_size.x, need_w] | min_size.x >= need_w - 0.01 | (待填) | | | +| 1362 | 173 | "大图拟合:画布 min.y 覆盖最宽层底节点(%.1f ≥ %.1f)〔fv8〕" ↩ % [min_size.y, need_h] | min_size.y >= need_h - 0.01 | (待填) | | | +| 1363 | 181 | "大图布局:全节点方块落在拟合画布内(origin=(%.1f,%.1f) size=(%.1f,%.1f))〔fv8〕" ↩ % [origin.x, origin.y, min_size.x, min_size.y] | all_in | (待填) | | | +| 1364 | 188 | "大图呈现:两个终点 Boss 均有描边标记〔fv8〕" | disp.has(boss_ids[0]) and disp.has(boss_ids[1]) and bool(disp[boss_ids[0]]["boss_border"]) and bool(disp[boss_ids[1]]["boss_border"]) | (待填) | | | +| 1365 | 191 | "大图呈现:入口停点远处战斗节点(句柄 32,层 6)类型入雾〔fv8〕" | not bool(disp[32]["draw_icon"]) | (待填) | | | ## S182 _test_layout_cache(行 209–226 · 2 条 · 源 game/tests/unit/presentation/level_map_info_fog_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1376 | 209 | "布局缓存:同 nodes 两次查询返回同一实例(第二次命中缓存)〔levelmap批 V1〕" | is_same(p1, p2) | (待填) | | | -| 1377 | 224 | "布局缓存换代:新图查询返回新实例非旧缓存(失效重算生效)〔levelmap批 V2〕" | not is_same(p1, pc1) and is_same(pc1, pc2) and (pc1[chain[0]] as Vector2) != (p1[nodes[0]] as Vector2) | (待填) | | | +| 1366 | 209 | "布局缓存:同 nodes 两次查询返回同一实例(第二次命中缓存)〔levelmap批 V1〕" | is_same(p1, p2) | (待填) | | | +| 1367 | 224 | "布局缓存换代:新图查询返回新实例非旧缓存(失效重算生效)〔levelmap批 V2〕" | not is_same(p1, pc1) and is_same(pc1, pc2) and (pc1[chain[0]] as Vector2) != (p1[nodes[0]] as Vector2) | (待填) | | | ## S183 _init(行 105–149 · 11 条 · 源 game/tests/unit/presentation/pacing_queue_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1378 | 105 | "排空前后核心诊断快照逐位一致(AC⑤ 演出关停零分叉)" | snap_before == snap_after | (待填) | | | -| 1379 | 107 | "结算行已产生(断言前提)" | rows_seen.size() > 0 | (待填) | | | -| 1380 | 108 | "排空后队列恒空(flush 完整性)" | main._row_queue.is_empty() | (待填) | | | -| 1381 | 117 | "末信号行原文在明细带(排空不吞行:『%s』)" ↩ % last_text | last_text != "" and main.log_view.parsed_text().contains(last_text) | (待填) | | | -| 1382 | 123 | "常态段间延迟 >0 且 ≥ 段内(%f/%f)" ↩ % [d_on, d_same] | d_on > 0.0 and d_on >= d_same | (待填) | | | -| 1383 | 125 | "减动效段间延迟归零(行不跳过——同帧直落快路)" | main.pace_delay_for("damage", "heal") == 0.0 | (待填) | | | -| 1384 | 135 | "结算标出按行 target_id 解析站桩(id 与名同传——d12)〔wd12〕" | fstage.calls.size() == 1 and fstage.calls[0] == ["goblin_b", "地精"] | (待填) | | | -| 1385 | 138 | "跨区弹道按行 target_id 解析站桩(id 与名同传——d12)〔wd12〕" | fstage.calls.size() == 2 and fstage.calls[1] == ["goblin_b", "地精"] | (待填) | | | -| 1386 | 140 | "两路解析成功后演出层各接一拍(记参面非空)〔wd12〕" | ffx.calls.size() == 2 | (待填) | | | -| 1387 | 147 | "会话更替夹具已入队(前提:%d 行)〔wr4〕" ↩ % main._row_queue.size() | main._row_queue.size() == 2 | (待填) | | | -| 1388 | 149 | "会话更替后待播队列清零(_begin_session 内机制 flush)〔wr4〕" | main._row_queue.is_empty() | (待填) | | | +| 1368 | 105 | "排空前后核心诊断快照逐位一致(AC⑤ 演出关停零分叉)" | snap_before == snap_after | (待填) | | | +| 1369 | 107 | "结算行已产生(断言前提)" | rows_seen.size() > 0 | (待填) | | | +| 1370 | 108 | "排空后队列恒空(flush 完整性)" | main._row_queue.is_empty() | (待填) | | | +| 1371 | 117 | "末信号行原文在明细带(排空不吞行:『%s』)" ↩ % last_text | last_text != "" and main.log_view.parsed_text().contains(last_text) | (待填) | | | +| 1372 | 123 | "常态段间延迟 >0 且 ≥ 段内(%f/%f)" ↩ % [d_on, d_same] | d_on > 0.0 and d_on >= d_same | (待填) | | | +| 1373 | 125 | "减动效段间延迟归零(行不跳过——同帧直落快路)" | main.pace_delay_for("damage", "heal") == 0.0 | (待填) | | | +| 1374 | 135 | "结算标出按行 target_id 解析站桩(id 与名同传——d12)〔wd12〕" | fstage.calls.size() == 1 and fstage.calls[0] == ["goblin_b", "地精"] | (待填) | | | +| 1375 | 138 | "跨区弹道按行 target_id 解析站桩(id 与名同传——d12)〔wd12〕" | fstage.calls.size() == 2 and fstage.calls[1] == ["goblin_b", "地精"] | (待填) | | | +| 1376 | 140 | "两路解析成功后演出层各接一拍(记参面非空)〔wd12〕" | ffx.calls.size() == 2 | (待填) | | | +| 1377 | 147 | "会话更替夹具已入队(前提:%d 行)〔wr4〕" ↩ % main._row_queue.size() | main._row_queue.size() == 2 | (待填) | | | +| 1378 | 149 | "会话更替后待播队列清零(_begin_session 内机制 flush)〔wr4〕" | main._row_queue.is_empty() | (待填) | | | ## S184 _sweep_hero_panel(行 47–50 · 2 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1389 | 47 | "C 区专长段头==「专长」(u1 换代——段头文案钉值)〔wa1〕" | feat_head != null and feat_head.text == "专长" | (待填) | | | -| 1390 | 49 | "C 区属性值提升段头==「属性值提升」(u2 换代——同屏两段同标撞车消除)〔wa1〕" | curio_head != null and curio_head.text == "属性值提升" | (待填) | | | +| 1379 | 47 | "C 区专长段头==「专长」(u1 换代——段头文案钉值)〔wa1〕" | feat_head != null and feat_head.text == "专长" | (待填) | | | +| 1380 | 49 | "C 区属性值提升段头==「属性值提升」(u2 换代——同屏两段同标撞车消除)〔wa1〕" | curio_head != null and curio_head.text == "属性值提升" | (待填) | | | -## S185 _sweep_narrator(行 57–64 · 7 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) +## S185 _sweep_narrator(行 57–63 · 6 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1391 | 57 | "奖励点头部含「冒险牌库」(u3 机制事实①同库)〔wa4〕" | head.contains("冒险牌库") | (待填) | | | -| 1392 | 58 | "奖励点头部含「属性值提升与补给」(u3 机制事实②渠道分流)〔wa4〕" | head.contains("属性值提升与补给") | (待填) | | | -| 1393 | 59 | "奖励点头部含「同库分流」(u3 机制事实②措辞)〔wa4〕" | head.contains("同库分流") | (待填) | | | -| 1394 | 60 | "奖励点头部零「同权重」旧话(u3 清零)〔wa4〕" | not head.contains("同权重") | (待填) | | | -| 1395 | 61 | "奖励点头部零「同上限」旧话(u3 清零)〔wa4〕" | not head.contains("同上限") | (待填) | | | -| 1396 | 62 | "奖励点头部零「提升池」旧话(u3 清零)〔wa4〕" | not head.contains("提升池") | (待填) | | | -| 1397 | 63 | "专长卡兜底==「专长\\n???」(u11 换代——数据缺行真实兜底路径)〔wa3〕" | String(narrator.feat_card_text({})) == "专长\\n???" | (待填) | | | +| 1381 | 57 | "奖励点头部含「冒险牌库」(u3 机制事实①同库)〔wa4〕" | head.contains("冒险牌库") | (待填) | | | +| 1382 | 58 | "奖励点头部含「属性值提升与补给」(u3 机制事实②渠道分流)〔wa4〕" | head.contains("属性值提升与补给") | (待填) | | | +| 1383 | 59 | "奖励点头部零「同权重」旧话(u3 清零)〔wa4〕" | not head.contains("同权重") | (待填) | | | +| 1384 | 60 | "奖励点头部零「同上限」旧话(u3 清零)〔wa4〕" | not head.contains("同上限") | (待填) | | | +| 1385 | 61 | "奖励点头部零「提升池」旧话(u3 清零)〔wa4〕" | not head.contains("提升池") | (待填) | | | +| 1386 | 62 | "专长卡兜底==「专长\\n???」(u11 换代——数据缺行真实兜底路径)〔wa3〕" | String(narrator.feat_card_text({})) == "专长\\n???" | (待填) | | | -## S186 _sweep_legend(行 75–79 · 5 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) +## S186 _sweep_legend(行 74–77 · 4 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1398 | 75 | "图例封印行存在(wa5 夹具前提)〔wa5〕" | not seal_row.is_empty() | (待填) | | | -| 1399 | 76 | "图例封印行含「标空」(u4——与「标中」成对)〔wa5〕" | String(seal_row[2] if seal_row.size() > 2 else "").contains("标空") | (待填) | | | -| 1400 | 77 | "图例封印行含「受挖掘惩罚」(u4 雷字清零换代)〔wa5〕" | String(seal_row[2] if seal_row.size() > 2 else "").contains("受挖掘惩罚") | (待填) | | | -| 1401 | 78 | "图例封印行零「插错」(u4 旗族遗留词干清零)〔wa5〕" | not String(seal_row[2] if seal_row.size() > 2 else "").contains("插错") | (待填) | | | -| 1402 | 79 | "图例封印行零「挖雷」(u4 雷字清零)〔wa5〕" | not String(seal_row[2] if seal_row.size() > 2 else "").contains("挖雷") | (待填) | | | +| 1387 | 74 | "图例封印行含「标空」(u4——与「标中」成对)〔wa5〕" | String(seal_row[2] if seal_row.size() > 2 else "").contains("标空") | (待填) | | | +| 1388 | 75 | "图例封印行含「受挖掘惩罚」(u4 雷字清零换代)〔wa5〕" | String(seal_row[2] if seal_row.size() > 2 else "").contains("受挖掘惩罚") | (待填) | | | +| 1389 | 76 | "图例封印行零「插错」(u4 旗族遗留词干清零)〔wa5〕" | not String(seal_row[2] if seal_row.size() > 2 else "").contains("插错") | (待填) | | | +| 1390 | 77 | "图例封印行零「挖雷」(u4 雷字清零)〔wa5〕" | not String(seal_row[2] if seal_row.size() > 2 else "").contains("挖雷") | (待填) | | | -## S187 _sweep_source_names(行 92–104 · 4 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) +## S187 _sweep_source_names(行 90–102 · 4 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1403 | 92 | "状态槽 mine_hit 效果行在座(wa6 夹具前提)〔wa6〕" | slot != null | (待填) | | | -| 1404 | 95 | "状态槽来源注记==「危险格」(u5 换代——meta tooltip 消费半)〔wa6〕" | row_view._tip_label.text.contains("来源:危险格") | (待填) | | | -| 1405 | 98 | "结算表 source_label_map[mine_hit]==「危险格」(u6 换代——ConfigDB 真装载)〔wa7〕" | String(_db.settlement.core.source_label_map.get("mine_hit", "")) == "危险格" | (待填) | | | -| 1406 | 103 | "战斗日志 mine_hit 来源行含「(危险格)」(u6 消费半——_format_row 生产路径)〔wa7〕" | row_txt.contains("(危险格)") | (待填) | | | +| 1391 | 90 | "状态槽 mine_hit 效果行在座(wa6 夹具前提)〔wa6〕" | slot != null | (待填) | | | +| 1392 | 93 | "状态槽来源注记==「危险格」(u5 换代——meta tooltip 消费半)〔wa6〕" | row_view._tip_label.text.contains("来源:危险格") | (待填) | | | +| 1393 | 96 | "结算表 source_label_map[mine_hit]==「危险格」(u6 换代——ConfigDB 真装载)〔wa7〕" | String(_db.combat.settlement.core.source_label_map.get("mine_hit", "")) == "危险格" | (待填) | | | +| 1394 | 101 | "战斗日志 mine_hit 来源行含「(危险格)」(u6 消费半——_format_row 生产路径)〔wa7〕" | row_txt.contains("(危险格)") | (待填) | | | -## S188 _sweep_board_glyph(行 133–142 · 4 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) +## S188 _sweep_board_glyph(行 131–140 · 4 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1407 | 133 | "棋盘夹具含挖掘射程内未开危险格(wa8 前提——实测 %d 步)〔wa8〕" ↩ % glow_steps | mine_cell.x >= 0 | (待填) | | | -| 1408 | 139 | "已开危险格 glyph==「危」(u7 换代)〔wa8〕" | u.glyph == "危" | (待填) | | | -| 1409 | 140 | "已开危险格 plate==true(u9 牌载哨兵包含项随动)〔wa8〕" | u.plate == true | (待填) | | | -| 1410 | 141 | "已开危险格 glyph_color==RED_MINE(u7 墨色不随字形换代漂移)〔wa8〕" | u.glyph_color == UiKit.RED_MINE | (待填) | | | +| 1395 | 131 | "棋盘夹具含挖掘射程内未开危险格(wa8 前提——实测 %d 步)〔wa8〕" ↩ % glow_steps | mine_cell.x >= 0 | (待填) | | | +| 1396 | 137 | "已开危险格 glyph==「危」(u7 换代)〔wa8〕" | u.glyph == "危" | (待填) | | | +| 1397 | 138 | "已开危险格 plate==true(u9 牌载哨兵包含项随动)〔wa8〕" | u.plate == true | (待填) | | | +| 1398 | 139 | "已开危险格 glyph_color==RED_MINE(u7 墨色不随字形换代漂移)〔wa8〕" | u.glyph_color == UiKit.RED_MINE | (待填) | | | -## S189 _sweep_token_scans(行 148–158 · 2 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) +## S189 _sweep_token_scans(行 146–156 · 2 条 · 源 game/tests/unit/presentation/player_copy_sweep_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1411 | 148 | "board_view.gd 源零 \\"雷\\" 字面量(u7/u8/u9 哨兵族统一钉住)〔wa8〕" | board_src.count("\\"雷\\"") == 0 | (待填) | | | -| 1412 | 157 | "battle_orchestrator.gd 玩家面字符串字面量零「挖雷」(u13——注释保留面不入域)〔wa12〕" | orch_hits == 0 | (待填) | | | +| 1399 | 146 | "board_view.gd 源零 \\"雷\\" 字面量(u7/u8/u9 哨兵族统一钉住)〔wa8〕" | board_src.count("\\"雷\\"") == 0 | (待填) | | | +| 1400 | 155 | "battle_orchestrator.gd 玩家面字符串字面量零「挖雷」(u13——注释保留面不入域)〔wa12〕" | orch_hits == 0 | (待填) | | | ## S190 _init(行 75–132 · 18 条 · 源 game/tests/unit/presentation/standee_fx_sweep_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1413 | 75 | "站桩 enemy_id 访问器读装载值〔wd12〕" | s1.enemy_id() == "goblin_a" and s2.enemy_id() == "goblin_b" | (待填) | | | -| 1414 | 76 | "同名敌按 id 精确解析(不做名子串撞车)〔wd12〕" | stage._card_by_id("goblin_b") == s2 and stage._card_by_id("goblin_a") == s1 | (待填) | | | -| 1415 | 78 | "空 id / 未登记 id 恒不命中(名兜底归调用点)〔wd12〕" | stage._card_by_id("") == null and stage._card_by_id("no_such") == null | (待填) | | | -| 1416 | 83 | "伤害行按 target_id 命中正确站桩(闪白落点)〔wd12〕" | s2._flash_count == 1 and s1._flash_count == 0 | (待填) | | | -| 1417 | 88 | "弹道落点按 target_id 取站桩中心〔wd12〕" | c_b != null and c_b == c_b_expect | (待填) | | | -| 1418 | 91 | "无 target_id 时按名兜底同旧行为(首命中)〔wd12〕" | c_fallback != null and c_fallback == c_fallback_expect | (待填) | | | -| 1419 | 95 | "蓄力技能(heavy)置 _intent_charge=true〔wd2〕" | s1._intent_charge | (待填) | | | -| 1420 | 97 | "非蓄力攻击技(goblin_slash)零置位〔wd2〕" | not s1._intent_charge | (待填) | | | -| 1421 | 99 | "增益系零置位〔wd2〕" | not s1._intent_charge | (待填) | | | -| 1422 | 101 | "普攻零表示路径复位 _intent_charge〔wd2〕" | not s1._intent_charge | (待填) | | | -| 1423 | 108 | "首开胜利画面创建庆祝句柄〔wd4〕" | first != null and first.is_valid() | (待填) | | | -| 1424 | 111 | "重开胜利画面:旧句柄已 kill、新句柄在位(至多一个活跃)〔wd4〕" | first != null and not first.is_valid() and second != null and second.is_valid() | (待填) | | | -| 1425 | 114 | "close 必杀庆祝句柄(句柄清空)〔wd4〕" | ov.get_meta(&"celebrate_tw", null) == null | (待填) | | | -| 1426 | 119 | "模拟运行期调试入口钮同置灰〔wd5〕" | dbg._dbg_btn.disabled | (待填) | | | -| 1427 | 121 | "模拟收尾调试入口钮恢复(未锁定)〔wd5〕" | not dbg._dbg_btn.disabled | (待填) | | | -| 1428 | 130 | "控制器运行中 is_running()=true(驱动回调内读数)〔wd8〕" | drv.seen == [true, true] | (待填) | | | -| 1429 | 131 | "控制器收尾 is_running()=false〔wd8〕" | not ctrl.is_running() and not panel.running | (待填) | | | -| 1430 | 132 | "两局报告全文注入面板(run 收尾链路完整)〔wd8〕" | String(panel.report) != "" | (待填) | | | +| 1401 | 75 | "站桩 enemy_id 访问器读装载值〔wd12〕" | s1.enemy_id() == "goblin_a" and s2.enemy_id() == "goblin_b" | (待填) | | | +| 1402 | 76 | "同名敌按 id 精确解析(不做名子串撞车)〔wd12〕" | stage._card_by_id("goblin_b") == s2 and stage._card_by_id("goblin_a") == s1 | (待填) | | | +| 1403 | 78 | "空 id / 未登记 id 恒不命中(名兜底归调用点)〔wd12〕" | stage._card_by_id("") == null and stage._card_by_id("no_such") == null | (待填) | | | +| 1404 | 83 | "伤害行按 target_id 命中正确站桩(闪白落点)〔wd12〕" | s2.fx_probe().flash_count == 1 and s1.fx_probe().flash_count == 0 | (待填) | | | +| 1405 | 88 | "弹道落点按 target_id 取站桩中心〔wd12〕" | c_b != null and c_b == c_b_expect | (待填) | | | +| 1406 | 91 | "无 target_id 时按名兜底同旧行为(首命中)〔wd12〕" | c_fallback != null and c_fallback == c_fallback_expect | (待填) | | | +| 1407 | 95 | "蓄力技能(heavy)置 _intent_charge=true〔wd2〕" | s1._intent_charge | (待填) | | | +| 1408 | 97 | "非蓄力攻击技(goblin_slash)零置位〔wd2〕" | not s1._intent_charge | (待填) | | | +| 1409 | 99 | "增益系零置位〔wd2〕" | not s1._intent_charge | (待填) | | | +| 1410 | 101 | "普攻零表示路径复位 _intent_charge〔wd2〕" | not s1._intent_charge | (待填) | | | +| 1411 | 108 | "首开胜利画面创建庆祝句柄〔wd4〕" | first != null and first.is_valid() | (待填) | | | +| 1412 | 111 | "重开胜利画面:旧句柄已 kill、新句柄在位(至多一个活跃)〔wd4〕" | first != null and not first.is_valid() and second != null and second.is_valid() | (待填) | | | +| 1413 | 114 | "close 必杀庆祝句柄(句柄清空)〔wd4〕" | ov.get_meta(&"celebrate_tw", null) == null | (待填) | | | +| 1414 | 119 | "模拟运行期调试入口钮同置灰〔wd5〕" | dbg._dbg_btn.disabled | (待填) | | | +| 1415 | 121 | "模拟收尾调试入口钮恢复(未锁定)〔wd5〕" | not dbg._dbg_btn.disabled | (待填) | | | +| 1416 | 130 | "控制器运行中 is_running()=true(驱动回调内读数)〔wd8〕" | drv.seen == [true, true] | (待填) | | | +| 1417 | 131 | "控制器收尾 is_running()=false〔wd8〕" | not ctrl.is_running() and not panel.running | (待填) | | | +| 1418 | 132 | "两局报告全文注入面板(run 收尾链路完整)〔wd8〕" | String(panel.report) != "" | (待填) | | | ## S191 _init(行 24–34 · 2 条 · 源 game/tests/unit/presentation/terrain_tile_sets_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1431 | 24 | "前置:表加载零 error" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | -| 1432 | 34 | "前置:board_gen 词表非空(%d 型)" ↩ % types.size() | types.size() > 0 | (待填) | | | +| 1419 | 24 | "前置:表加载零 error" | load_ok and db.diagnostics_count("error") == 0 | (待填) | | | +| 1420 | 34 | "前置:board_gen 词表非空(%d 型)" ↩ % types.size() | types.size() > 0 | (待填) | | | ## S192 _test_set_completeness(行 56–56 · 1 条 · 源 game/tests/unit/presentation/terrain_tile_sets_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1433 | 56 | "图型 %s 瓦片套完整(16 墙 + %d 地面,缺=%s)" ↩ % [String(bt), variants, str(missing)] | missing.is_empty() | (待填) | | | +| 1421 | 56 | "图型 %s 瓦片套完整(16 墙 + %d 地面,缺=%s)" ↩ % [String(bt), variants, str(missing)] | missing.is_empty() | (待填) | | | ## S193 _test_hue_domains(行 72–82 · 4 条 · 源 game/tests/unit/presentation/terrain_tile_sets_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1434 | 72 | "图型 %s 抽检样本可读" ↩ % String(bt) | false | (待填) | | | -| 1435 | 77 | "图型 %s 墙顶均值色相在带 %s(实测 %.3f)" ↩ % [String(bt), str(band), float(wall["h"])] | float(wall["h"]) >= float(band[0]) and float(wall["h"]) <= float(band[1]) | (待填) | | | -| 1436 | 79 | "图型 %s 图底分离(墙顶明度 %.3f > 地面 %.3f)" ↩ % [String(bt), wall_v[bt], floor_v[bt]] | wall_v[bt] > floor_v[bt] | (待填) | | | -| 1437 | 82 | "deep_underdark 墙顶更暗于 underdark(更深蓝黑方向)" | float(wall_v["deep_underdark"]) < float(wall_v["underdark"]) | (待填) | | | +| 1422 | 72 | "图型 %s 抽检样本可读" ↩ % String(bt) | false | (待填) | | | +| 1423 | 77 | "图型 %s 墙顶均值色相在带 %s(实测 %.3f)" ↩ % [String(bt), str(band), float(wall["h"])] | float(wall["h"]) >= float(band[0]) and float(wall["h"]) <= float(band[1]) | (待填) | | | +| 1424 | 79 | "图型 %s 图底分离(墙顶明度 %.3f > 地面 %.3f)" ↩ % [String(bt), wall_v[bt], floor_v[bt]] | wall_v[bt] > floor_v[bt] | (待填) | | | +| 1425 | 82 | "deep_underdark 墙顶更暗于 underdark(更深蓝黑方向)" | float(wall_v["deep_underdark"]) < float(wall_v["underdark"]) | (待填) | | | ## S194 _test_digit_ink_modes(行 137–159 · 5 条 · 源 game/tests/unit/presentation/terrain_tile_sets_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1438 | 137 | "图型 %s 数字墨族断言样本可读" ↩ % String(bt) | false | (待填) | | | -| 1439 | 144 | "图型 %s 双峰纹理落牌(亮族 p90 %.2f/均值 %.2f,深族 p90 %.2f/均值 %.2f 均未过门)" ↩ % [ String(bt), float(mode["cr_p90_bright"]), float(mode["cr_mean_bright"]), float(mode["cr_p90_dark"]), float(mode["cr_mean_dark"])] | float(mode["cr_mean_dark"]) < float(TilePaletteScript.DIGIT_INK_CR_MEAN_DARK) | (待填) | | | -| 1440 | 151 | "图型 %s 所选%s 对 p90 ≥%.1f(实测 %.2f)" ↩ % [String(bt), mode_name, float(TilePaletteScript.DIGIT_INK_CR_P90), cr_p90] | cr_p90 >= float(TilePaletteScript.DIGIT_INK_CR_P90) | (待填) | | | -| 1441 | 155 | "图型 %s 所选%s 对均值 ≥%.1f(实测 %.2f)" ↩ % [String(bt), mode_name, mean_gate, cr_mean] | cr_mean >= mean_gate | (待填) | | | -| 1442 | 158 | "图型 %s 呈现模式期望=%s(实测=%s)" ↩ % [String(bt), "色牌" if bool(expect_plate[bt]) else "直画", mode_name] | plate == bool(expect_plate[bt]) | (待填) | | | +| 1426 | 137 | "图型 %s 数字墨族断言样本可读" ↩ % String(bt) | false | (待填) | | | +| 1427 | 144 | "图型 %s 双峰纹理落牌(亮族 p90 %.2f/均值 %.2f,深族 p90 %.2f/均值 %.2f 均未过门)" ↩ % [ String(bt), float(mode["cr_p90_bright"]), float(mode["cr_mean_bright"]), float(mode["cr_p90_dark"]), float(mode["cr_mean_dark"])] | float(mode["cr_mean_dark"]) < float(TilePaletteScript.DIGIT_INK_CR_MEAN_DARK) | (待填) | | | +| 1428 | 151 | "图型 %s 所选%s 对 p90 ≥%.1f(实测 %.2f)" ↩ % [String(bt), mode_name, float(TilePaletteScript.DIGIT_INK_CR_P90), cr_p90] | cr_p90 >= float(TilePaletteScript.DIGIT_INK_CR_P90) | (待填) | | | +| 1429 | 155 | "图型 %s 所选%s 对均值 ≥%.1f(实测 %.2f)" ↩ % [String(bt), mode_name, mean_gate, cr_mean] | cr_mean >= mean_gate | (待填) | | | +| 1430 | 158 | "图型 %s 呈现模式期望=%s(实测=%s)" ↩ % [String(bt), "色牌" if bool(expect_plate[bt]) else "直画", mode_name] | plate == bool(expect_plate[bt]) | (待填) | | | ## S195 _smoke_type_glow(行 215–375 · 24 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1443 | 215 | "V1 微光默认开(Q4 丙案——呈现态布尔缺省 true,生产写入面零显式开)" | bool(snap.get("enabled", false)) | (待填) | | | -| 1444 | 217 | "V1 夹具全集合法九型 ⇒ 清单外登记为空(禁静默空光通道零误报)" | (snap.get("errors", []) as Array).is_empty() | (待填) | | | -| 1445 | 220 | "V1 作用集恒等:微光格集合 == _diggable ∩ 身份可查(期望 %d,实测 %d)" ↩ % [expect.size(), (snap.get("cells", {}) as Dictionary).size()] | _glow_key_set_equal(snap.get("cells", {}), expect) | (待填) | | | -| 1446 | 222 | "V1 夹具前提:英雄未开邻格 ≥5(实测 %d——novice %d 步)" ↩ % [expect.size(), steps] | expect.size() >= 5 | (待填) | | | -| 1447 | 232 | "V1 已开格/r0 出生展开区零微光(S1)%s" ↩ % ("" if opened_leak == "" else "违例:" + opened_leak) | opened_leak == "" | (待填) | | | -| 1448 | 249 | "V2 夹具前提:未开邻格派定 ≥5 型(实测 %d)" ↩ % mapped | mapped >= 5 | (待填) | | | -| 1449 | 258 | "V2 九型→五组逐格映射(attack/defense/action/gp→增益/heal→治疗/pierce→穿刺/blast→群伤/magic→魔法)%s" ↩ % ("" if map_bad == "" else "违例:" + map_bad) | map_bad == "" | (待填) | | | -| 1450 | 272 | "V2 清单外 cell_type 显式登记(errors=%s)" ↩ % str(errs) | errs.has("keystone") | (待填) | | | -| 1451 | 273 | "V2 清单外格零微光(无静默空光——%s 不在作用集)" ↩ % str(bogus_cell) | not (snap.get("cells", {}) as Dictionary).has(bogus_cell) | (待填) | | | -| 1452 | 275 | "V2 清单外格剔除后作用集 = 派定数 −1(%d→%d)" ↩ % [mapped, (snap.get("cells", {}) as Dictionary).size()] | (snap.get("cells", {}) as Dictionary).size() == mapped - 1 | (待填) | | | -| 1453 | 288 | "V1 封锁格零微光(query null ⇒ 天然惰性——%s)" ↩ % str(blocked_cell) | not (snap.get("cells", {}) as Dictionary).has(blocked_cell) | (待填) | | | -| 1454 | 290 | "V1 封锁格零登记(errors 仍仅清单外一条——%s)" ↩ % str(snap.get("errors", [])) | (snap.get("errors", []) as Array).size() == 1 | (待填) | | | -| 1455 | 292 | "V1 封锁格剔除后作用集 = 派定数 −1(实测 %d)" ↩ % (snap.get("cells", {}) as Dictionary).size() | (snap.get("cells", {}) as Dictionary).size() == mapped - 1 | (待填) | | | -| 1456 | 298 | "V1 封锁解除后作用集复位恒等(期望 %d,实测 %d)" ↩ % [expect.size(), (snap.get("cells", {}) as Dictionary).size()] | _glow_key_set_equal(snap.get("cells", {}), expect) | (待填) | | | -| 1457 | 304 | "V7 off 态开关读数=false" | not bool(snap.get("enabled", true)) | (待填) | | | -| 1458 | 305 | "V7 off 态作用集为空(基线保护面)" | (snap.get("cells", {}) as Dictionary).is_empty() | (待填) | | | -| 1459 | 308 | "V7 on 态开关读数=true(复现)" | bool(snap_on.get("enabled", false)) | (待填) | | | -| 1460 | 309 | "V7 off→on 作用集逐位复现(关前 %d 格/开后 %d 格,组号逐位一致)" ↩ % [ (snap_before_off.get("cells", {}) as Dictionary).size(), (snap_on.get("cells", {}) as Dictionary).size()] | _glow_map_equal(snap_on.get("cells", {}), snap_before_off.get("cells", {})) | (待填) | | | -| 1461 | 312 | "V7 off→on 作用集与独立重算期望集恒等(%d 格)" ↩ % (snap_on.get("cells", {}) as Dictionary).size() | _glow_key_set_equal(snap_on.get("cells", {}), expect) | (待填) | | | -| 1462 | 316 | "V7 微光开关零玩法写入(已开集/英雄位/封印数三面对照逐位一致)" | revealed_before == revealed_after and hero_before == ms.hero_pos() and seals_before == ms.seal_count() | (待填) | | | -| 1463 | 329 | "V3 换图 rebuild 后作用集按新板重算(期望 %d,实测 %d——零旧板残留)" ↩ % [expect2.size(), (snap2.get("cells", {}) as Dictionary).size()] | _glow_key_set_equal(snap2.get("cells", {}), expect2) | (待填) | | | -| 1464 | 331 | "V3 换图清清单外登记(rebuild 清零)" | (snap2.get("errors", []) as Array).is_empty() | (待填) | | | -| 1465 | 372 | "契约 1 同板同序列 on/off 两态结算逐位一致(%d 步对照——A 开态/B 关态)%s" ↩ % [seq_steps, diverge] | diverge == "" | (待填) | | | -| 1466 | 375 | "契约 1 对照前提:同序列 ≥5 步(实测 %d)" ↩ % seq_steps | seq_steps >= 5 | (待填) | | | +| 1431 | 215 | "V1 微光默认开(Q4 丙案——呈现态布尔缺省 true,生产写入面零显式开)" | bool(snap.get("enabled", false)) | (待填) | | | +| 1432 | 217 | "V1 夹具全集合法九型 ⇒ 清单外登记为空(禁静默空光通道零误报)" | (snap.get("errors", []) as Array).is_empty() | (待填) | | | +| 1433 | 220 | "V1 作用集恒等:微光格集合 == _diggable ∩ 身份可查(期望 %d,实测 %d)" ↩ % [expect.size(), (snap.get("cells", {}) as Dictionary).size()] | _glow_key_set_equal(snap.get("cells", {}), expect) | (待填) | | | +| 1434 | 222 | "V1 夹具前提:英雄未开邻格 ≥5(实测 %d——novice %d 步)" ↩ % [expect.size(), steps] | expect.size() >= 5 | (待填) | | | +| 1435 | 232 | "V1 已开格/r0 出生展开区零微光(S1)%s" ↩ % ("" if opened_leak == "" else "违例:" + opened_leak) | opened_leak == "" | (待填) | | | +| 1436 | 249 | "V2 夹具前提:未开邻格派定 ≥5 型(实测 %d)" ↩ % mapped | mapped >= 5 | (待填) | | | +| 1437 | 258 | "V2 九型→五组逐格映射(attack/defense/action/gp→增益/heal→治疗/pierce→穿刺/blast→群伤/magic→魔法)%s" ↩ % ("" if map_bad == "" else "违例:" + map_bad) | map_bad == "" | (待填) | | | +| 1438 | 272 | "V2 清单外 cell_type 显式登记(errors=%s)" ↩ % str(errs) | errs.has("keystone") | (待填) | | | +| 1439 | 273 | "V2 清单外格零微光(无静默空光——%s 不在作用集)" ↩ % str(bogus_cell) | not (snap.get("cells", {}) as Dictionary).has(bogus_cell) | (待填) | | | +| 1440 | 275 | "V2 清单外格剔除后作用集 = 派定数 −1(%d→%d)" ↩ % [mapped, (snap.get("cells", {}) as Dictionary).size()] | (snap.get("cells", {}) as Dictionary).size() == mapped - 1 | (待填) | | | +| 1441 | 288 | "V1 封锁格零微光(query null ⇒ 天然惰性——%s)" ↩ % str(blocked_cell) | not (snap.get("cells", {}) as Dictionary).has(blocked_cell) | (待填) | | | +| 1442 | 290 | "V1 封锁格零登记(errors 仍仅清单外一条——%s)" ↩ % str(snap.get("errors", [])) | (snap.get("errors", []) as Array).size() == 1 | (待填) | | | +| 1443 | 292 | "V1 封锁格剔除后作用集 = 派定数 −1(实测 %d)" ↩ % (snap.get("cells", {}) as Dictionary).size() | (snap.get("cells", {}) as Dictionary).size() == mapped - 1 | (待填) | | | +| 1444 | 298 | "V1 封锁解除后作用集复位恒等(期望 %d,实测 %d)" ↩ % [expect.size(), (snap.get("cells", {}) as Dictionary).size()] | _glow_key_set_equal(snap.get("cells", {}), expect) | (待填) | | | +| 1445 | 304 | "V7 off 态开关读数=false" | not bool(snap.get("enabled", true)) | (待填) | | | +| 1446 | 305 | "V7 off 态作用集为空(基线保护面)" | (snap.get("cells", {}) as Dictionary).is_empty() | (待填) | | | +| 1447 | 308 | "V7 on 态开关读数=true(复现)" | bool(snap_on.get("enabled", false)) | (待填) | | | +| 1448 | 309 | "V7 off→on 作用集逐位复现(关前 %d 格/开后 %d 格,组号逐位一致)" ↩ % [ (snap_before_off.get("cells", {}) as Dictionary).size(), (snap_on.get("cells", {}) as Dictionary).size()] | _glow_map_equal(snap_on.get("cells", {}), snap_before_off.get("cells", {})) | (待填) | | | +| 1449 | 312 | "V7 off→on 作用集与独立重算期望集恒等(%d 格)" ↩ % (snap_on.get("cells", {}) as Dictionary).size() | _glow_key_set_equal(snap_on.get("cells", {}), expect) | (待填) | | | +| 1450 | 316 | "V7 微光开关零玩法写入(已开集/英雄位/封印数三面对照逐位一致)" | revealed_before == revealed_after and hero_before == ms.hero_pos() and seals_before == ms.seal_count() | (待填) | | | +| 1451 | 329 | "V3 换图 rebuild 后作用集按新板重算(期望 %d,实测 %d——零旧板残留)" ↩ % [expect2.size(), (snap2.get("cells", {}) as Dictionary).size()] | _glow_key_set_equal(snap2.get("cells", {}), expect2) | (待填) | | | +| 1452 | 331 | "V3 换图清清单外登记(rebuild 清零)" | (snap2.get("errors", []) as Array).is_empty() | (待填) | | | +| 1453 | 372 | "契约 1 同板同序列 on/off 两态结算逐位一致(%d 步对照——A 开态/B 关态)%s" ↩ % [seq_steps, diverge] | diverge == "" | (待填) | | | +| 1454 | 375 | "契约 1 对照前提:同序列 ≥5 步(实测 %d)" ↩ % seq_steps | seq_steps >= 5 | (待填) | | | ## S196 _smoke_battle_log_view(行 392–423 · 6 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1467 | 392 | "敌方伤害行格式化(baseline 行不读退役 attach)" | enemy_txt.contains("撕咬") and enemy_txt.contains("〔掷 15 vs 护甲 12") | (待填) | | | -| 1468 | 394 | "退役伏击/基线拆行文案不再出现" | not enemy_txt.contains("伏击") and not enemy_txt.contains("基线") | (待填) | | | -| 1469 | 401 | "主角穿刺型暴击行金框(单分量 payload 不触发三分量拆行)" | hero_txt.contains("暴击×") and hero_txt.contains("⚔击杀") and hero_txt.contains("■") | (待填) | | | -| 1470 | 403 | "退役三分量拆行文案不再出现" | not hero_txt.contains(" + 穿刺 ") | (待填) | | | -| 1471 | 409 | "未生效行聚合计数 ×2 呈现" | agg_txt.contains("⚠") and agg_txt.contains("×2") | (待填) | | | -| 1472 | 422 | "行型 %s 格式化落地(解析文本增长)" ↩ % String(row_type) | view.parsed_text().length() > before | (待填) | | | +| 1455 | 392 | "敌方伤害行格式化(baseline 行不读退役 attach)" | enemy_txt.contains("撕咬") and enemy_txt.contains("〔掷 15 vs 护甲 12") | (待填) | | | +| 1456 | 394 | "退役伏击/基线拆行文案不再出现" | not enemy_txt.contains("伏击") and not enemy_txt.contains("基线") | (待填) | | | +| 1457 | 401 | "主角穿刺型暴击行金框(单分量 payload 不触发三分量拆行)" | hero_txt.contains("暴击×") and hero_txt.contains("⚔击杀") and hero_txt.contains("■") | (待填) | | | +| 1458 | 403 | "退役三分量拆行文案不再出现" | not hero_txt.contains(" + 穿刺 ") | (待填) | | | +| 1459 | 409 | "未生效行聚合计数 ×2 呈现" | agg_txt.contains("⚠") and agg_txt.contains("×2") | (待填) | | | +| 1460 | 422 | "行型 %s 格式化落地(解析文本增长)" ↩ % String(row_type) | view.parsed_text().length() > before | (待填) | | | ## S197 _smoke_level_open_line(行 430–436 · 3 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1473 | 430 | "开场行 boosted=true 尾接「(含面板加成)」〔wc1〕" | String(NarratorScript.level_open_line("第一层", 42, 5, 30, true)).contains("(含面板加成)") | (待填) | | | -| 1474 | 432 | "开场行 boosted=false 零后缀(白板局不带)〔wc1〕" | not String(NarratorScript.level_open_line("第一层", 42, 5, 30, false)).contains("(含面板加成)") | (待填) | | | -| 1475 | 434 | "开场行种子/两值带入不受后缀位影响(数值带入回归)〔wc1〕" | String(NarratorScript.level_open_line("第一层", 42, 5, 30, true)).contains("种子 42") and String(NarratorScript.level_open_line("第一层", 42, 5, 30, true)).contains("攻击 5 / 生命 30") | (待填) | | | +| 1461 | 430 | "开场行 boosted=true 尾接「(含面板加成)」〔wc1〕" | String(NarratorScript.level_open_line("第一层", 42, 5, 30, true)).contains("(含面板加成)") | (待填) | | | +| 1462 | 432 | "开场行 boosted=false 零后缀(白板局不带)〔wc1〕" | not String(NarratorScript.level_open_line("第一层", 42, 5, 30, false)).contains("(含面板加成)") | (待填) | | | +| 1463 | 434 | "开场行种子/两值带入不受后缀位影响(数值带入回归)〔wc1〕" | String(NarratorScript.level_open_line("第一层", 42, 5, 30, true)).contains("种子 42") and String(NarratorScript.level_open_line("第一层", 42, 5, 30, true)).contains("攻击 5 / 生命 30") | (待填) | | | ## S198 _smoke_global_theme(行 445–540 · 22 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1476 | 445 | "V1 项目主题挂载(project.godot gui/theme/custom → main_theme.tres)" | th != null | (待填) | | | -| 1477 | 454 | "V1 主题类型覆盖 %s" ↩ % t | type_set.has(t) | (待填) | | | -| 1478 | 456 | "V1 红线 Button 类型词条零接触" | not type_set.has("Button") | (待填) | | | -| 1479 | 464 | "V1 红线 focus 族词条零定义%s" ↩ % ("" if focus_hits == "" else "(违例:" + focus_hits + ")") | focus_hits == "" | (待填) | | | -| 1480 | 468 | "V1 滚动条槽/柄三态词条在位" | vsb.has_theme_stylebox("scroll") and vsb.has_theme_stylebox("grabber") and vsb.has_theme_stylebox("grabber_highlight") and vsb.has_theme_stylebox("grabber_pressed") | (待填) | | | -| 1481 | 471 | "V1 滚动条 scroll_focus 未定义(红线)" | not th.has_stylebox("scroll_focus", "VScrollBar") and not th.has_stylebox("scroll_focus", "HScrollBar") | (待填) | | | -| 1482 | 474 | "V1 滑轨槽/填充/柄词条在位" | hsl.has_theme_stylebox("slider") and hsl.has_theme_stylebox("grabber_area") and hsl.has_theme_stylebox("grabber_area_highlight") and hsl.has_theme_icon("grabber") and hsl.has_theme_icon("grabber_highlight") and hsl.has_theme_icon("grabber_disabled") | (待填) | | | -| 1483 | 480 | "V1 LineEdit 词条在位(含占位色 font_placeholder_color)" | le.has_theme_stylebox("normal") and le.has_theme_color("font_color") and le.has_theme_color("font_placeholder_color") | (待填) | | | -| 1484 | 484 | "V1 SpinBox 箭头词条在位(updown 合并槽)" | sp.has_theme_icon("updown") | (待填) | | | -| 1485 | 487 | "V1 分隔线词条在位" | hsep.has_theme_stylebox("separator") and vsep.has_theme_stylebox("separator") | (待填) | | | -| 1486 | 491 | "V1 TooltipPanel 词条在位" | tpanel.has_theme_stylebox("panel") | (待填) | | | -| 1487 | 492 | "V1 TooltipLabel 词条在位" | tlabel.has_theme_color("font_color") and tlabel.has_theme_font_size("font_size") | (待填) | | | -| 1488 | 500 | "V2 冻结值锚定(已填充段=BG_ROOT #3A2E22,槽=暖石 #B8A582)" | fill_c.is_equal_approx(Color8(58, 46, 34)) and track_c.is_equal_approx(Color8(184, 165, 130)) | (待填) | | | -| 1489 | 503 | "V2 滑轨两段对比 %.2f:1 ≥4.5:1(拖到中点一眼可辨)" ↩ % ratio | ratio >= 4.5 | (待填) | | | -| 1490 | 505 | "V2 滑轨 stylebox 类型均为 StyleBoxFlat" | false | (待填) | | | -| 1491 | 508 | "V3 CheckBox 四态图标非空" | cbx.get_theme_icon("checked") != null and cbx.get_theme_icon("unchecked") != null and cbx.get_theme_icon("checked_disabled") != null and cbx.get_theme_icon("unchecked_disabled") != null | (待填) | | | -| 1492 | 512 | "V3 CheckButton 四态图标非空(与 CheckBox 同套 checked/unchecked 名,4.7 源码实证)" | cbt.get_theme_icon("checked") != null and cbt.get_theme_icon("unchecked") != null and cbt.get_theme_icon("checked_disabled") != null and cbt.get_theme_icon("unchecked_disabled") != null | (待填) | | | -| 1493 | 516 | "V3 PopupMenu 词条存在(弹层预盖)" | pm.has_theme_stylebox("panel") and pm.has_theme_stylebox("hover") and pm.has_theme_font_size("font_size") | (待填) | | | -| 1494 | 520 | "V3 OptionButton 词条存在(预盖)" | ob.has_theme_stylebox("normal") and ob.has_theme_stylebox("hover") and ob.has_theme_stylebox("pressed") and ob.has_theme_color("font_color") | (待填) | | | -| 1495 | 534 | "V4 退役实现零残留(%s)%s" ↩ % [path, "" if bad == "" else "违例:" + bad] | bad == "" | (待填) | | | -| 1496 | 537 | "V6 主题 default_font 非空(挂主题顶掉 custom_font 也不丢中文字形)" | th.default_font != null | (待填) | | | -| 1497 | 540 | "V6 Label 实取字体非空(中文字形防御实证)" | lbl.get_theme_font("font") != null | (待填) | | | +| 1464 | 445 | "V1 项目主题挂载(project.godot gui/theme/custom → main_theme.tres)" | th != null | (待填) | | | +| 1465 | 454 | "V1 主题类型覆盖 %s" ↩ % t | type_set.has(t) | (待填) | | | +| 1466 | 456 | "V1 红线 Button 类型词条零接触" | not type_set.has("Button") | (待填) | | | +| 1467 | 464 | "V1 红线 focus 族词条零定义%s" ↩ % ("" if focus_hits == "" else "(违例:" + focus_hits + ")") | focus_hits == "" | (待填) | | | +| 1468 | 468 | "V1 滚动条槽/柄三态词条在位" | vsb.has_theme_stylebox("scroll") and vsb.has_theme_stylebox("grabber") and vsb.has_theme_stylebox("grabber_highlight") and vsb.has_theme_stylebox("grabber_pressed") | (待填) | | | +| 1469 | 471 | "V1 滚动条 scroll_focus 未定义(红线)" | not th.has_stylebox("scroll_focus", "VScrollBar") and not th.has_stylebox("scroll_focus", "HScrollBar") | (待填) | | | +| 1470 | 474 | "V1 滑轨槽/填充/柄词条在位" | hsl.has_theme_stylebox("slider") and hsl.has_theme_stylebox("grabber_area") and hsl.has_theme_stylebox("grabber_area_highlight") and hsl.has_theme_icon("grabber") and hsl.has_theme_icon("grabber_highlight") and hsl.has_theme_icon("grabber_disabled") | (待填) | | | +| 1471 | 480 | "V1 LineEdit 词条在位(含占位色 font_placeholder_color)" | le.has_theme_stylebox("normal") and le.has_theme_color("font_color") and le.has_theme_color("font_placeholder_color") | (待填) | | | +| 1472 | 484 | "V1 SpinBox 箭头词条在位(updown 合并槽)" | sp.has_theme_icon("updown") | (待填) | | | +| 1473 | 487 | "V1 分隔线词条在位" | hsep.has_theme_stylebox("separator") and vsep.has_theme_stylebox("separator") | (待填) | | | +| 1474 | 491 | "V1 TooltipPanel 词条在位" | tpanel.has_theme_stylebox("panel") | (待填) | | | +| 1475 | 492 | "V1 TooltipLabel 词条在位" | tlabel.has_theme_color("font_color") and tlabel.has_theme_font_size("font_size") | (待填) | | | +| 1476 | 500 | "V2 冻结值锚定(已填充段=BG_ROOT #3A2E22,槽=暖石 #B8A582)" | fill_c.is_equal_approx(Color8(58, 46, 34)) and track_c.is_equal_approx(Color8(184, 165, 130)) | (待填) | | | +| 1477 | 503 | "V2 滑轨两段对比 %.2f:1 ≥4.5:1(拖到中点一眼可辨)" ↩ % ratio | ratio >= 4.5 | (待填) | | | +| 1478 | 505 | "V2 滑轨 stylebox 类型均为 StyleBoxFlat" | false | (待填) | | | +| 1479 | 508 | "V3 CheckBox 四态图标非空" | cbx.get_theme_icon("checked") != null and cbx.get_theme_icon("unchecked") != null and cbx.get_theme_icon("checked_disabled") != null and cbx.get_theme_icon("unchecked_disabled") != null | (待填) | | | +| 1480 | 512 | "V3 CheckButton 四态图标非空(与 CheckBox 同套 checked/unchecked 名,4.7 源码实证)" | cbt.get_theme_icon("checked") != null and cbt.get_theme_icon("unchecked") != null and cbt.get_theme_icon("checked_disabled") != null and cbt.get_theme_icon("unchecked_disabled") != null | (待填) | | | +| 1481 | 516 | "V3 PopupMenu 词条存在(弹层预盖)" | pm.has_theme_stylebox("panel") and pm.has_theme_stylebox("hover") and pm.has_theme_font_size("font_size") | (待填) | | | +| 1482 | 520 | "V3 OptionButton 词条存在(预盖)" | ob.has_theme_stylebox("normal") and ob.has_theme_stylebox("hover") and ob.has_theme_stylebox("pressed") and ob.has_theme_color("font_color") | (待填) | | | +| 1483 | 534 | "V4 退役实现零残留(%s)%s" ↩ % [path, "" if bad == "" else "违例:" + bad] | bad == "" | (待填) | | | +| 1484 | 537 | "V6 主题 default_font 非空(挂主题顶掉 custom_font 也不丢中文字形)" | th.default_font != null | (待填) | | | +| 1485 | 540 | "V6 Label 实取字体非空(中文字形防御实证)" | lbl.get_theme_font("font") != null | (待填) | | | -## S199 _smoke_button_five_states(行 553–625 · 23 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) +## S199 _smoke_button_five_states(行 553–612 · 21 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1498 | 553 | "V1 普通钮 %s 样式盒非空" ↩ % st | b.has_theme_stylebox_override(st) | (待填) | | | -| 1499 | 559 | "V1 普通钮 normal/hover/disabled 均为石纹九宫格贴图样式盒" | n_sb is StyleBoxTexture and h_sb is StyleBoxTexture and d_sb is StyleBoxTexture | (待填) | | | -| 1500 | 563 | "V1 普通钮 hover=modulate 提亮(乙案,异于常态 WHITE)" | h_mod != Color.WHITE and h_mod.r > 1.0 | (待填) | | | -| 1501 | 567 | "V1 普通钮 disabled=本族底 α0.45 幽灵化(甲案)" | is_equal_approx(d_mod.a, 0.45) and d_mod.r == 1.0 and d_mod.g == 1.0 | (待填) | | | -| 1502 | 570 | "V1 普通钮 disabled≠pressed(旧错配已修正)" | _sb_content_differs(d_sb, p_sb) | (待填) | | | -| 1503 | 572 | "V1 普通钮 focus 样式盒为扁平环(StyleBoxFlat)" | f_sb is StyleBoxFlat | (待填) | | | -| 1504 | 574 | "V1 焦点环色=INK(与 font_focus_color 同源)" | fsb.border_color == uikit_s.INK | (待填) | | | -| 1505 | 575 | "V1 焦点环宽=2px" | is_equal_approx(fsb.border_width_left, 2.0) and is_equal_approx(fsb.border_width_right, 2.0) and is_equal_approx(fsb.border_width_top, 2.0) and is_equal_approx(fsb.border_width_bottom, 2.0) | (待填) | | | -| 1506 | 578 | "V1 焦点环内缩=1px" | is_equal_approx(fsb.expand_margin_left, -1.0) and is_equal_approx(fsb.expand_margin_top, -1.0) and is_equal_approx(fsb.expand_margin_right, -1.0) and is_equal_approx(fsb.expand_margin_bottom, -1.0) | (待填) | | | -| 1507 | 581 | "V1 焦点环无底(叠加态不遮钮底)" | not fsb.draw_center and fsb.bg_color.a < 0.001 | (待填) | | | -| 1508 | 586 | "V1 主钮 %s 样式盒非空" ↩ % st | pb.has_theme_stylebox_override(st) | (待填) | | | -| 1509 | 590 | "V1 主钮 pressed/disabled=本族平涂(非石纹)" | ppn is StyleBoxFlat and pdd is StyleBoxFlat | (待填) | | | -| 1510 | 592 | "V1 主钮 pressed=烛光黄深档(现行值不动)" | (ppn as StyleBoxFlat).bg_color == Color8(214, 172, 88) | (待填) | | | -| 1511 | 594 | "V1 主钮 disabled=本族底 α0.45 幽灵化" | is_equal_approx((pdd as StyleBoxFlat).bg_color.a, 0.45) | (待填) | | | -| 1512 | 596 | "V1 主钮 disabled≠pressed" | _sb_content_differs(ppn, pdd) | (待填) | | | -| 1513 | 597 | "V1 主钮 hover=提亮档(承现行不动)" | (phv as StyleBoxFlat).bg_color == uikit_s.CANDLE.lightened(0.06) | (待填) | | | -| 1514 | 603 | "V1 危险钮 %s 样式盒非空" ↩ % st | db.has_theme_stylebox_override(st) | (待填) | | | -| 1515 | 606 | "V1 危险钮 pressed/disabled=本族平涂(非石纹,旧错配已修正)" | dpr is StyleBoxFlat and dds is StyleBoxFlat | (待填) | | | -| 1516 | 608 | "V1 危险钮 pressed=深红深档(暗于常态)" | (dpr as StyleBoxFlat).bg_color.r < 180.0 / 255.0 | (待填) | | | -| 1517 | 610 | "V1 危险钮 disabled=本族底 α0.45 幽灵化" | is_equal_approx((dds as StyleBoxFlat).bg_color.a, 0.45) | (待填) | | | -| 1518 | 612 | "V1 危险钮 disabled≠pressed" | _sb_content_differs(dpr, dds) | (待填) | | | -| 1519 | 617 | "V2 红线维持 Button 类型词条零接触" | not th.get_type_list().has("Button") | (待填) | | | -| 1520 | 624 | "V2 红线维持 focus 族词条零定义%s" ↩ % ("" if focus_hits == "" else "(违例:" + focus_hits + ")") | focus_hits == "" | (待填) | | | +| 1486 | 553 | "V1 普通钮 %s 样式盒非空" ↩ % st | b.has_theme_stylebox_override(st) | (待填) | | | +| 1487 | 559 | "V1 普通钮 normal/hover/disabled 均为石纹九宫格贴图样式盒" | n_sb is StyleBoxTexture and h_sb is StyleBoxTexture and d_sb is StyleBoxTexture | (待填) | | | +| 1488 | 563 | "V1 普通钮 hover=modulate 提亮(乙案,异于常态 WHITE)" | h_mod != Color.WHITE and h_mod.r > 1.0 | (待填) | | | +| 1489 | 567 | "V1 普通钮 disabled=本族底 α0.45 幽灵化(甲案)" | is_equal_approx(d_mod.a, 0.45) and d_mod.r == 1.0 and d_mod.g == 1.0 | (待填) | | | +| 1490 | 570 | "V1 普通钮 disabled≠pressed(旧错配已修正)" | _sb_content_differs(d_sb, p_sb) | (待填) | | | +| 1491 | 572 | "V1 普通钮 focus 样式盒为扁平环(StyleBoxFlat)" | f_sb is StyleBoxFlat | (待填) | | | +| 1492 | 574 | "V1 焦点环色=INK(与 font_focus_color 同源)" | fsb.border_color == uikit_s.INK | (待填) | | | +| 1493 | 575 | "V1 焦点环宽=2px" | is_equal_approx(fsb.border_width_left, 2.0) and is_equal_approx(fsb.border_width_right, 2.0) and is_equal_approx(fsb.border_width_top, 2.0) and is_equal_approx(fsb.border_width_bottom, 2.0) | (待填) | | | +| 1494 | 578 | "V1 焦点环内缩=1px" | is_equal_approx(fsb.expand_margin_left, -1.0) and is_equal_approx(fsb.expand_margin_top, -1.0) and is_equal_approx(fsb.expand_margin_right, -1.0) and is_equal_approx(fsb.expand_margin_bottom, -1.0) | (待填) | | | +| 1495 | 581 | "V1 焦点环无底(叠加态不遮钮底)" | not fsb.draw_center and fsb.bg_color.a < 0.001 | (待填) | | | +| 1496 | 586 | "V1 主钮 %s 样式盒非空" ↩ % st | pb.has_theme_stylebox_override(st) | (待填) | | | +| 1497 | 590 | "V1 主钮 pressed/disabled=本族平涂(非石纹)" | ppn is StyleBoxFlat and pdd is StyleBoxFlat | (待填) | | | +| 1498 | 592 | "V1 主钮 pressed=烛光黄深档(现行值不动)" | (ppn as StyleBoxFlat).bg_color == Color8(214, 172, 88) | (待填) | | | +| 1499 | 594 | "V1 主钮 disabled=本族底 α0.45 幽灵化" | is_equal_approx((pdd as StyleBoxFlat).bg_color.a, 0.45) | (待填) | | | +| 1500 | 596 | "V1 主钮 disabled≠pressed" | _sb_content_differs(ppn, pdd) | (待填) | | | +| 1501 | 597 | "V1 主钮 hover=提亮档(承现行不动)" | (phv as StyleBoxFlat).bg_color == uikit_s.CANDLE.lightened(0.06) | (待填) | | | +| 1502 | 603 | "V1 危险钮 %s 样式盒非空" ↩ % st | db.has_theme_stylebox_override(st) | (待填) | | | +| 1503 | 606 | "V1 危险钮 pressed/disabled=本族平涂(非石纹,旧错配已修正)" | dpr is StyleBoxFlat and dds is StyleBoxFlat | (待填) | | | +| 1504 | 608 | "V1 危险钮 pressed=深红深档(暗于常态)" | (dpr as StyleBoxFlat).bg_color.r < 180.0 / 255.0 | (待填) | | | +| 1505 | 610 | "V1 危险钮 disabled=本族底 α0.45 幽灵化" | is_equal_approx((dds as StyleBoxFlat).bg_color.a, 0.45) | (待填) | | | +| 1506 | 612 | "V1 危险钮 disabled≠pressed" | _sb_content_differs(dpr, dds) | (待填) | | | -## S200 _smoke_texture_filter(行 632–674 · 9 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) +## S200 _smoke_texture_filter(行 620–662 · 9 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1521 | 632 | "V3 工程设置 default_texture_filter=0(Nearest,翻转面=立绘/瓦片/九宫格 crisp)" | v != null and int(v) == 0 | (待填) | | | -| 1522 | 636 | "V3 白名单在位:暗角 vignette 显式 LINEAR" | vg.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR | (待填) | | | -| 1523 | 640 | "V3 白名单在位:光柱 beams 显式 LINEAR" | bm.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR | (待填) | | | -| 1524 | 658 | "T4 光柱带心峰值 α≈0.05(8bit 截断量化下限 12/255,实测 %.5f)" ↩ % max_a | max_a >= 12.0 / 255.0 and max_a <= 0.05 | (待填) | | | -| 1525 | 660 | "T4 光柱带内连续 alpha 过渡像素 %d 个(旧零过渡硬边已修)" ↩ % mid_a | mid_a > 100 | (待填) | | | -| 1526 | 661 | "T4 光柱纵向衰减生效(上半亮度 %.0f > 下半 %.0f 的 2 倍——上亮下暗)" ↩ % [top_sum, bot_sum] | top_sum > bot_sum * 2.0 | (待填) | | | -| 1527 | 665 | "V3 白名单在位:地图渐变 gradient_base 显式 LINEAR" | gb.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR | (待填) | | | -| 1528 | 670 | "V3 脚影显式 LINEAR 不丢(board_world.gd)" | bw_src.contains("shadow.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR") | (待填) | | | -| 1529 | 673 | "V3 状态图标显式 LINEAR 不丢(status_slot_row.gd)" | ss_src.contains("icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR") | (待填) | | | +| 1507 | 620 | "V3 工程设置 default_texture_filter=0(Nearest,翻转面=立绘/瓦片/九宫格 crisp)" | v != null and int(v) == 0 | (待填) | | | +| 1508 | 624 | "V3 白名单在位:暗角 vignette 显式 LINEAR" | vg.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR | (待填) | | | +| 1509 | 628 | "V3 白名单在位:光柱 beams 显式 LINEAR" | bm.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR | (待填) | | | +| 1510 | 646 | "T4 光柱带心峰值 α≈0.05(8bit 截断量化下限 12/255,实测 %.5f)" ↩ % max_a | max_a >= 12.0 / 255.0 and max_a <= 0.05 | (待填) | | | +| 1511 | 648 | "T4 光柱带内连续 alpha 过渡像素 %d 个(旧零过渡硬边已修)" ↩ % mid_a | mid_a > 100 | (待填) | | | +| 1512 | 649 | "T4 光柱纵向衰减生效(上半亮度 %.0f > 下半 %.0f 的 2 倍——上亮下暗)" ↩ % [top_sum, bot_sum] | top_sum > bot_sum * 2.0 | (待填) | | | +| 1513 | 653 | "V3 白名单在位:地图渐变 gradient_base 显式 LINEAR" | gb.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR | (待填) | | | +| 1514 | 658 | "V3 脚影显式 LINEAR 不丢(board_world.gd)" | bw_src.contains("shadow.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR") | (待填) | | | +| 1515 | 661 | "V3 状态图标显式 LINEAR 不丢(status_slot_row.gd)" | ss_src.contains("icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR") | (待填) | | | -## S201 _smoke_contrast_inks(行 683–699 · 9 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) +## S201 _smoke_contrast_inks(行 671–687 · 9 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1530 | 683 | "V4 顺位徽标/重甲徽标 GOLD_BRIGHT 对舞台底 %.2f:1 ≥4.5:1" ↩ % r_order | r_order >= 4.5 | (待填) | | | -| 1531 | 685 | "V4 魔法护甲徽标 HERO_BLUE_BRIGHT 对舞台底 %.2f:1 ≥4.5:1" ↩ % r_ward | r_ward >= 4.5 | (待填) | | | -| 1532 | 687 | "V4 RAP 反馈行 STONE_DEEP 对数据板 %.2f:1 ≥4.5:1" ↩ % r_rap | r_rap >= 4.5 | (待填) | | | -| 1533 | 690 | "V4 顺位徽标①②③ 墨=GOLD_BRIGHT(13px 行原位)" | sb_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_BRIGHT") | (待填) | | | -| 1534 | 692 | "V4 魔法护甲徽标墨=HERO_BLUE_BRIGHT" | sb_src.contains("col = UiKit.HERO_BLUE_BRIGHT") | (待填) | | | -| 1535 | 693 | "V4 重甲徽标墨=GOLD_BRIGHT" | sb_src.contains("col = UiKit.GOLD_BRIGHT") | (待填) | | | -| 1536 | 694 | "V4 意图蓄力档判据=分档布尔(standee_body:179 CHARGE_SKILL_IDS 判定;旧 GOLD_DEEP 恒不可达判据清零)〔wd2〕" | sb_src.contains("_intent_charge = pid in CHARGE_SKILL_IDS") and not sb_src.contains("if _intent_col == UiKit.GOLD_DEEP:") | (待填) | | | -| 1537 | 697 | "V4 RAP 反馈行墨=STONE_DEEP(原位行锚定)" | ap_src.contains('UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)') | (待填) | | | -| 1538 | 699 | "V4 RAP 反馈行旧墨 STONE_FAINT 清零(含注记面)" | not ap_src.contains("STONE_FAINT") | (待填) | | | +| 1516 | 671 | "V4 顺位徽标/重甲徽标 GOLD_BRIGHT 对舞台底 %.2f:1 ≥4.5:1" ↩ % r_order | r_order >= 4.5 | (待填) | | | +| 1517 | 673 | "V4 魔法护甲徽标 HERO_BLUE_BRIGHT 对舞台底 %.2f:1 ≥4.5:1" ↩ % r_ward | r_ward >= 4.5 | (待填) | | | +| 1518 | 675 | "V4 RAP 反馈行 STONE_DEEP 对数据板 %.2f:1 ≥4.5:1" ↩ % r_rap | r_rap >= 4.5 | (待填) | | | +| 1519 | 678 | "V4 顺位徽标①②③ 墨=GOLD_BRIGHT(13px 行原位)" | sb_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_BRIGHT") | (待填) | | | +| 1520 | 680 | "V4 魔法护甲徽标墨=HERO_BLUE_BRIGHT" | sb_src.contains("col = UiKit.HERO_BLUE_BRIGHT") | (待填) | | | +| 1521 | 681 | "V4 重甲徽标墨=GOLD_BRIGHT" | sb_src.contains("col = UiKit.GOLD_BRIGHT") | (待填) | | | +| 1522 | 682 | "V4 意图蓄力档判据=分档布尔(standee_body:179 CHARGE_SKILL_IDS 判定;旧 GOLD_DEEP 恒不可达判据清零)〔wd2〕" | sb_src.contains("_intent_charge = pid in CHARGE_SKILL_IDS") and not sb_src.contains("if _intent_col == UiKit.GOLD_DEEP:") | (待填) | | | +| 1523 | 685 | "V4 RAP 反馈行墨=STONE_DEEP(原位行锚定)" | ap_src.contains('UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)') | (待填) | | | +| 1524 | 687 | "V4 RAP 反馈行旧墨 STONE_FAINT 清零(含注记面)" | not ap_src.contains("STONE_FAINT") | (待填) | | | -## S202 _smoke_element_details(行 708–727 · 8 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) +## S202 _smoke_element_details(行 696–715 · 8 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1539 | 708 | "T5② BCard 图标槽白图标对比 %.2f:1 ≥3:1(硬约束)" ↩ % r_white | r_white >= 3.0 | (待填) | | | -| 1540 | 711 | "T5② 明度差降档生效(槽对卡面 %.2f:1 < 旧 %.2f:1)" ↩ % [r_new, r_old] | r_new < r_old | (待填) | | | -| 1541 | 713 | "T5② BCard 图标槽落柔和档(SLOT_BACKDROP_SOFT)" | uk_src.contains("UiKit.panel_flat(SLOT_BACKDROP_SOFT, PANEL_BORDER, 0, 1)") | (待填) | | | -| 1542 | 716 | "T5② B 区状态图标槽仍用深档(SLOT_BACKDROP,未误伤)" | ss_src.contains("UiKit.SLOT_BACKDROP") | (待填) | | | -| 1543 | 720 | "T5③ 商店卡片区垂直居中(ALIGNMENT_CENTER)" | shop_src.contains("_cards_box.alignment = BoxContainer.ALIGNMENT_CENTER") | (待填) | | | -| 1544 | 724 | "T5③ 营地三 holder 统一 min 高 336(实测 %d 处)" ↩ % camp_min | camp_min == 3 | (待填) | | | -| 1545 | 726 | "T5③ 营地四栏面板纵向 FILL 等高(实测 %d 处)" ↩ % camp_fill | camp_fill == 4 | (待填) | | | -| 1546 | 727 | "T5③ 三处锁文垂直居中在位" | camp_src.count("SIZE_SHRINK_CENTER") >= 3 | (待填) | | | +| 1525 | 696 | "T5② BCard 图标槽白图标对比 %.2f:1 ≥3:1(硬约束)" ↩ % r_white | r_white >= 3.0 | (待填) | | | +| 1526 | 699 | "T5② 明度差降档生效(槽对卡面 %.2f:1 < 旧 %.2f:1)" ↩ % [r_new, r_old] | r_new < r_old | (待填) | | | +| 1527 | 701 | "T5② BCard 图标槽落柔和档(SLOT_BACKDROP_SOFT)" | uk_src.contains("UiKit.panel_flat(SLOT_BACKDROP_SOFT, PANEL_BORDER, 0, 1)") | (待填) | | | +| 1528 | 704 | "T5② B 区状态图标槽仍用深档(SLOT_BACKDROP,未误伤)" | ss_src.contains("UiKit.SLOT_BACKDROP") | (待填) | | | +| 1529 | 708 | "T5③ 商店卡片区垂直居中(ALIGNMENT_CENTER)" | shop_src.contains("_cards_box.alignment = BoxContainer.ALIGNMENT_CENTER") | (待填) | | | +| 1530 | 712 | "T5③ 营地三 holder 统一 min 高 336(实测 %d 处)" ↩ % camp_min | camp_min == 3 | (待填) | | | +| 1531 | 714 | "T5③ 营地四栏面板纵向 FILL 等高(实测 %d 处)" ↩ % camp_fill | camp_fill == 4 | (待填) | | | +| 1532 | 715 | "T5③ 三处锁文垂直居中在位" | camp_src.count("SIZE_SHRINK_CENTER") >= 3 | (待填) | | | -## S203 _smoke_diag_button_retired(行 734–747 · 7 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) +## S203 _smoke_diag_button_retired(行 722–735 · 7 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1547 | 734 | "V6 主区无 build_diag_button 调用残留(旧实现同批清理)" | not main_src.contains("build_diag_button") | (待填) | | | -| 1548 | 736 | "V6 转储入口连接随迁(组合根接浮窗 diagnose_requested 信号)" | main_src.contains("debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)") | (待填) | | | -| 1549 | 738 | "V6 转储编排承接函数在位(dump_diagnosis 卡死取证保留)" | main_src.contains("func _on_diagnose_pressed()") | (待填) | | | -| 1550 | 741 | "V6 build_diag_button 工厂已退役(ui_assembler 零残留)" | not asm_src.contains("build_diag_button") | (待填) | | | -| 1551 | 744 | "V6 调试浮窗 diagnose_requested 信号在位" | dbg_src.contains("signal diagnose_requested") | (待填) | | | -| 1552 | 745 | "V6 浮窗转储钮外发方法在位(纯视图边界)" | dbg_src.contains("_emit_diagnose_requested") | (待填) | | | -| 1553 | 746 | "V6 浮窗诊断转储钮在位(玩家画面角落钮清零后唯一入口)" | dbg_src.contains('UiKit.button("转储")') | (待填) | | | +| 1533 | 722 | "V6 主区无 build_diag_button 调用残留(旧实现同批清理)" | not main_src.contains("build_diag_button") | (待填) | | | +| 1534 | 724 | "V6 转储入口连接随迁(组合根接浮窗 diagnose_requested 信号)" | main_src.contains("debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)") | (待填) | | | +| 1535 | 726 | "V6 转储编排承接函数在位(dump_diagnosis 卡死取证保留)" | main_src.contains("func _on_diagnose_pressed()") | (待填) | | | +| 1536 | 729 | "V6 build_diag_button 工厂已退役(ui_assembler 零残留)" | not asm_src.contains("build_diag_button") | (待填) | | | +| 1537 | 732 | "V6 调试浮窗 diagnose_requested 信号在位" | dbg_src.contains("signal diagnose_requested") | (待填) | | | +| 1538 | 733 | "V6 浮窗转储钮外发方法在位(纯视图边界)" | dbg_src.contains("_emit_diagnose_requested") | (待填) | | | +| 1539 | 734 | "V6 浮窗诊断转储钮在位(玩家画面角落钮清零后唯一入口)" | dbg_src.contains('UiKit.button("转储")') | (待填) | | | -## S204 _status_panel_projection(行 763–793 · 10 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) +## S204 _status_panel_projection(行 751–781 · 10 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1554 | 763 | "状态面板开层可见(open_status 直开——P4 甲零动效路径)" | po.visible | (待填) | | | -| 1555 | 764 | "余额徽投影 = 快照 balance 读数(sv4 回归锚)〔sv4〕" | String(po._balance_badge.value_label.text) == "钥石 77" | (待填) | | | -| 1556 | 766 | "属性行全值投影 = 契约4 钉值格式逐字(生命/上限/攻击/AC/GP 五现值)〔sv1〕" | String(po._attrs_label.text) == "生命 13/29 · 攻击 7 · 护甲 AC 11 · GP 41" | (待填) | | | -| 1557 | 768 | "秘宝行投影 = 受理序 join+已用件「(已用)」尾缀〔sv2〕" | String(po._curios_label.text) == "青苔密卷 · 锈蚀怀表(已用)" | (待填) | | | -| 1558 | 770 | "专长行投影 = 行串 join〔sv3〕" | String(po._feats_label.text) == "疼痛记忆 ×2 · 双管镐 ×1" | (待填) | | | -| 1559 | 777 | "面板子树零「道具工坊/天赋树」文案(跨关概览退役零残留)〔sv5〕" | stale == "" | (待填) | | | -| 1560 | 778 | "旧段成员退役(_workshop_holder/_tree_label 零残留——契约2 数据/成员层面清零)〔sv5〕" | po.get("_workshop_holder") == null and po.get("_tree_label") == null | (待填) | | | -| 1561 | 785 | "空秘宝态句=「未携带秘宝」〔sv2〕" | String(po2._curios_label.text) == "未携带秘宝" | (待填) | | | -| 1562 | 786 | "空专长态句=「未获专长」〔sv3〕" | String(po2._feats_label.text) == "未获专长" | (待填) | | | -| 1563 | 792 | "提示行=「查看本局冒险进度(只读)」(新定位换代)〔sv8〕" | hint_txt == "查看本局冒险进度(只读)" | (待填) | | | +| 1540 | 751 | "状态面板开层可见(open_status 直开——P4 甲零动效路径)" | po.visible | (待填) | | | +| 1541 | 752 | "余额徽投影 = 快照 balance 读数(sv4 回归锚)〔sv4〕" | String(po._balance_badge.value_label.text) == "钥石 77" | (待填) | | | +| 1542 | 754 | "属性行全值投影 = 契约4 钉值格式逐字(生命/上限/攻击/AC/GP 五现值)〔sv1〕" | String(po._attrs_label.text) == "生命 13/29 · 攻击 7 · 护甲 AC 11 · GP 41" | (待填) | | | +| 1543 | 756 | "秘宝行投影 = 受理序 join+已用件「(已用)」尾缀〔sv2〕" | String(po._curios_label.text) == "青苔密卷 · 锈蚀怀表(已用)" | (待填) | | | +| 1544 | 758 | "专长行投影 = 行串 join〔sv3〕" | String(po._feats_label.text) == "疼痛记忆 ×2 · 双管镐 ×1" | (待填) | | | +| 1545 | 765 | "面板子树零「道具工坊/天赋树」文案(跨关概览退役零残留)〔sv5〕" | stale == "" | (待填) | | | +| 1546 | 766 | "旧段成员退役(_workshop_holder/_tree_label 零残留——契约2 数据/成员层面清零)〔sv5〕" | po.get("_workshop_holder") == null and po.get("_tree_label") == null | (待填) | | | +| 1547 | 773 | "空秘宝态句=「未携带秘宝」〔sv2〕" | String(po2._curios_label.text) == "未携带秘宝" | (待填) | | | +| 1548 | 774 | "空专长态句=「未获专长」〔sv3〕" | String(po2._feats_label.text) == "未获专长" | (待填) | | | +| 1549 | 780 | "提示行=「查看本局冒险进度(只读)」(新定位换代)〔sv8〕" | hint_txt == "查看本局冒险进度(只读)" | (待填) | | | -## S205 _camp_recent_projection(行 818–819 · 1 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) +## S205 _camp_recent_projection(行 806–807 · 1 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1564 | 818 | "营地最近冒险行完成关数措辞(实际「%s」)〔口径换代 V6〕" ↩ % recent_text | recent_text.contains("完成 2 关") and not recent_text.contains("通关") | (待填) | | | +| 1550 | 806 | "营地最近冒险行完成关数措辞(实际「%s」)〔口径换代 V6〕" ↩ % recent_text | recent_text.contains("完成 2 关") and not recent_text.contains("通关") | (待填) | | | -## S206 _career_overlay_projection(行 852–897 · 11 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) +## S206 _career_overlay_projection(行 840–885 · 11 条 · 源 game/tests/unit/presentation/theme_projection_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1565 | 852 | "生涯页开层可见(open_career 直开——零动效路径)" | co.visible | (待填) | | | -| 1566 | 853 | "总局数投影 = adventure_total() 读数(%s)" ↩ % co._total_label.text | String(co._total_label.text) == "2 局" | (待填) | | | -| 1567 | 855 | "冒险成功局数投影 = adventure_cleared() 读数(%s)" ↩ % co._cleared_label.text | String(co._cleared_label.text) == "1 局(三关全通)" | (待填) | | | -| 1568 | 860 | "各关完成次数投影 = clear_count() 逐关读数(%s)" ↩ % co._clears_label.text | String(co._clears_label.text) == " · ".join(expect_clears) | (待填) | | | -| 1569 | 862 | "Boss 首杀/总杀投影 = 首杀标记计数 + boss_kill_count()(%s / %s)" ↩ % [co._boss_first_label.text, co._boss_kills_label.text] | String(co._boss_first_label.text) == "3/3 关" and String(co._boss_kills_label.text) == "4 次" | (待填) | | | -| 1570 | 864 | "钥石余额徽投影 = keystone_balance() 读数(%s)" ↩ % co._keystone_badge.value_label.text | String(co._keystone_badge.value_label.text) == "钥石 %d" % mp.keystone_balance() | (待填) | | | -| 1571 | 871 | "生涯页行标签换代(冒险成功局数/各关完成次数)〔口径换代 V4〕" | career_labels_joined.contains("冒险成功局数") and career_labels_joined.contains("各关完成次数") | (待填) | | | -| 1572 | 873 | "生涯页 label 全集零「通关」残留〔口径换代 V4〕" | not career_labels_joined.contains("通关") | (待填) | | | -| 1573 | 874 | "秘宝解锁条件标签冒险成功口径(UiKit 单点常量)〔口径换代 V5〕" | UiKit.TREASURE_REQ_LABELS["adventure_cleared"] == "冒险成功" | (待填) | | | -| 1574 | 883 | "v5 现役档升 v6 载入成功(计数域缺省补零)" | v5.load_from_disk() and v5.adventure_total() == 0 and v5.adventure_cleared() == 0 | (待填) | | | -| 1575 | 894 | "旧档载入后生涯页正常显示(v5 升 v6:总局数/冒险成功局数显示 0,既有钥石余额原样呈现)" | co_v5.visible and String(co_v5._total_label.text) == "0 局" \\ and String(co_v5._cleared_label.text) == "0 局(三关全通)" \\ and String(co_v5._keystone_badge.value_label.text) == "钥石 3" | (待填) | | | +| 1551 | 840 | "生涯页开层可见(open_career 直开——零动效路径)" | co.visible | (待填) | | | +| 1552 | 841 | "总局数投影 = adventure_total() 读数(%s)" ↩ % co._total_label.text | String(co._total_label.text) == "2 局" | (待填) | | | +| 1553 | 843 | "冒险成功局数投影 = adventure_cleared() 读数(%s)" ↩ % co._cleared_label.text | String(co._cleared_label.text) == "1 局(三关全通)" | (待填) | | | +| 1554 | 848 | "各关完成次数投影 = clear_count() 逐关读数(%s)" ↩ % co._clears_label.text | String(co._clears_label.text) == " · ".join(expect_clears) | (待填) | | | +| 1555 | 850 | "Boss 首杀/总杀投影 = 首杀标记计数 + boss_kill_count()(%s / %s)" ↩ % [co._boss_first_label.text, co._boss_kills_label.text] | String(co._boss_first_label.text) == "3/3 关" and String(co._boss_kills_label.text) == "4 次" | (待填) | | | +| 1556 | 852 | "钥石余额徽投影 = keystone_balance() 读数(%s)" ↩ % co._keystone_badge.value_label.text | String(co._keystone_badge.value_label.text) == "钥石 %d" % mp.meta_state.keystone_balance() | (待填) | | | +| 1557 | 859 | "生涯页行标签换代(冒险成功局数/各关完成次数)〔口径换代 V4〕" | career_labels_joined.contains("冒险成功局数") and career_labels_joined.contains("各关完成次数") | (待填) | | | +| 1558 | 861 | "生涯页 label 全集零「通关」残留〔口径换代 V4〕" | not career_labels_joined.contains("通关") | (待填) | | | +| 1559 | 862 | "秘宝解锁条件标签冒险成功口径(UiKit 单点常量)〔口径换代 V5〕" | UiKit.TREASURE_REQ_LABELS["adventure_cleared"] == "冒险成功" | (待填) | | | +| 1560 | 871 | "v5 现役档升 v6 载入成功(计数域缺省补零)" | v5.load_from_disk() and v5.meta_state.career_state().adventure_total == 0 and v5.meta_state.career_state().adventure_cleared == 0 | (待填) | | | +| 1561 | 882 | "旧档载入后生涯页正常显示(v5 升 v6:总局数/冒险成功局数显示 0,既有钥石余额原样呈现)" | co_v5.visible and String(co_v5._total_label.text) == "0 局" \\ and String(co_v5._cleared_label.text) == "0 局(三关全通)" \\ and String(co_v5._keystone_badge.value_label.text) == "钥石 3" | (待填) | | | -## S207 _test_grouping_closed(行 61–79 · 7 条 · 源 game/tests/unit/presentation/type_glow_test.gd · 去向:待分类回填) +## S207 _test_grouping_closed(行 61–77 · 6 条 · 源 game/tests/unit/presentation/type_glow_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1576 | 61 | "九型封闭:%s → 组%d(期望组%d)" ↩ % [t, got, int(TYPE_EXPECT[t])] | got == int(TYPE_EXPECT[t]) | (待填) | | | -| 1577 | 65 | "清单外值「%s」→ GLOW_NONE(禁静默空光)" ↩ % bad | UiKitScript.glow_group_for_cell_type(bad) == UiKitScript.GLOW_NONE | (待填) | | | -| 1578 | 70 | "两带并存锚定:卡顶带 blast→TG_MAGIC(E5 回滚既存行为未动)" | band_blast == UiKitScript.TG_MAGIC | (待填) | | | -| 1579 | 71 | "两带并存锚定:格面微光 blast→群伤组(与卡顶带刻意不同)" | UiKitScript.glow_group_for_cell_type("blast") == UiKitScript.GLOW_AOE | (待填) | | | -| 1580 | 74 | "glow_color_for_group 群伤组 = TG_GLOW_AOE" | UiKitScript.glow_color_for_group(UiKitScript.GLOW_AOE) == UiKitScript.TG_GLOW_AOE | (待填) | | | -| 1581 | 76 | "glow_color_for_group 无组(GLOW_NONE)→ 全透明" | UiKitScript.glow_color_for_group(UiKitScript.GLOW_NONE) == Color(0, 0, 0, 0) | (待填) | | | -| 1582 | 78 | "glow_color_for_group 越界组号 → 全透明" | UiKitScript.glow_color_for_group(99) == Color(0, 0, 0, 0) | (待填) | | | +| 1562 | 61 | "九型封闭:%s → 组%d(期望组%d)" ↩ % [t, got, int(TYPE_EXPECT[t])] | got == int(TYPE_EXPECT[t]) | (待填) | | | +| 1563 | 65 | "清单外值「%s」→ GLOW_NONE(禁静默空光)" ↩ % bad | UiKitScript.glow_group_for_cell_type(bad) == UiKitScript.GLOW_NONE | (待填) | | | +| 1564 | 70 | "两带并存锚定:卡顶带 blast→TG_MAGIC(E5 回滚既存行为未动)" | band_blast == UiKitScript.TG_MAGIC | (待填) | | | +| 1565 | 72 | "glow_color_for_group 群伤组 = TG_GLOW_AOE" | UiKitScript.glow_color_for_group(UiKitScript.GLOW_AOE) == UiKitScript.TG_GLOW_AOE | (待填) | | | +| 1566 | 74 | "glow_color_for_group 无组(GLOW_NONE)→ 全透明" | UiKitScript.glow_color_for_group(UiKitScript.GLOW_NONE) == Color(0, 0, 0, 0) | (待填) | | | +| 1567 | 76 | "glow_color_for_group 越界组号 → 全透明" | UiKitScript.glow_color_for_group(99) == Color(0, 0, 0, 0) | (待填) | | | -## S208 _test_frozen_hue_face(行 88–102 · 5 条 · 源 game/tests/unit/presentation/type_glow_test.gd · 去向:待分类回填) +## S208 _test_frozen_hue_face(行 86–95 · 3 条 · 源 game/tests/unit/presentation/type_glow_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1583 | 88 | "实现值锚定:%s = #%02X%02X%02X(色值单点 ui_kit,逐位钉死)" ↩ % [CONST_NAME[g], int(m.r8), int(m.g8), int(m.b8)] | impl_const == m | (待填) | | | -| 1584 | 93 | "冻结色相保持:%s ΔH=%.3f° ≤0.2° 且 ΔS=%.4f ≤0.01(只动明度档)" ↩ % [g, dh * 360.0, ds] | dh <= 0.2 / 360.0 and ds <= 0.01 | (待填) | | | -| 1585 | 98 | "群伤组专项:H 与冻结 #FF2D55 差 %.4f°(零移动)" ↩ % (absf(aoe_m.h - aoe_f.h) * 360.0) | absf(aoe_m.h - aoe_f.h) < 0.001 | (待填) | | | -| 1586 | 100 | "群伤组专项:S 与冻结差 %.4f ≤0.01(8bit 量化噪声级)" ↩ % absf(aoe_m.s - aoe_f.s) | absf(aoe_m.s - aoe_f.s) <= 0.01 | (待填) | | | -| 1587 | 102 | "群伤组专项:V=0.65 明度档( provisional 域内)" | absf(aoe_m.v - 166.0 / 255.0) < 0.002 | (待填) | | | +| 1568 | 86 | "实现值锚定:%s = #%02X%02X%02X(色值单点 ui_kit,逐位钉死)" ↩ % [CONST_NAME[g], int(m.r8), int(m.g8), int(m.b8)] | impl_const == m | (待填) | | | +| 1569 | 91 | "冻结色相保持:%s ΔH=%.3f° ≤0.2° 且 ΔS=%.4f ≤0.01(只动明度档)" ↩ % [g, dh * 360.0, ds] | dh <= 0.2 / 360.0 and ds <= 0.01 | (待填) | | | +| 1570 | 95 | "群伤组专项:V=0.65 明度档( provisional 域内)" | absf(aoe_m.v - 166.0 / 255.0) < 0.002 | (待填) | | | -## S209 _test_render_params(行 107–113 · 3 条 · 源 game/tests/unit/presentation/type_glow_test.gd · 去向:待分类回填) +## S209 _test_render_params(行 100–106 · 3 条 · 源 game/tests/unit/presentation/type_glow_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1588 | 107 | "泛光强度 α = 170/255(v2 呈现参数冻结)" | is_equal_approx(UiKitScript.TYPE_GLOW_ALPHA, 170.0 / 255.0) | (待填) | | | -| 1589 | 109 | "带宽 = cell÷8(TYPE_GLOW_BAND_FRAC=0.125)" | is_equal_approx(UiKitScript.TYPE_GLOW_BAND_FRAC, 0.125) | (待填) | | | -| 1590 | 111 | "组号常量域连续(GLOW_NONE=0…GLOW_MAGIC=5)" | UiKitScript.GLOW_NONE == 0 and UiKitScript.GLOW_BUFF == 1 and UiKitScript.GLOW_HEAL == 2 and UiKitScript.GLOW_PIERCE == 3 and UiKitScript.GLOW_AOE == 4 and UiKitScript.GLOW_MAGIC == 5 | (待填) | | | +| 1571 | 100 | "泛光强度 α = 170/255(v2 呈现参数冻结)" | is_equal_approx(UiKitScript.TYPE_GLOW_ALPHA, 170.0 / 255.0) | (待填) | | | +| 1572 | 102 | "带宽 = cell÷8(TYPE_GLOW_BAND_FRAC=0.125)" | is_equal_approx(UiKitScript.TYPE_GLOW_BAND_FRAC, 0.125) | (待填) | | | +| 1573 | 104 | "组号常量域连续(GLOW_NONE=0…GLOW_MAGIC=5)" | UiKitScript.GLOW_NONE == 0 and UiKitScript.GLOW_BUFF == 1 and UiKitScript.GLOW_HEAL == 2 and UiKitScript.GLOW_PIERCE == 3 and UiKitScript.GLOW_AOE == 4 and UiKitScript.GLOW_MAGIC == 5 | (待填) | | | -## S210 _test_gamma_ladder(行 128–139 · 3 条 · 源 game/tests/unit/presentation/type_glow_test.gd · 去向:待分类回填) +## S210 _test_gamma_ladder(行 121–132 · 3 条 · 源 game/tests/unit/presentation/type_glow_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1591 | 128 | "阶梯单调:%s 合成亮度 %.4f < 上一档 %.4f(伽马合成空间)" ↩ % [g, lum, prev_lum] | lum < prev_lum | (待填) | | | -| 1592 | 134 | "相邻组模型 CR:%s/%s = %.3f ≥1.20(色弱互辨预判)" ↩ % [order[i], order[i + 1], cr] | cr >= 1.20 | (待填) | | | -| 1593 | 138 | "群伤/魔法线性亮度序:群伤 %.4f < 魔法 %.4f(首版 CIE L* 排反事故的守门)" ↩ % [aoe_lin, magic_lin] | aoe_lin < magic_lin | (待填) | | | +| 1574 | 121 | "阶梯单调:%s 合成亮度 %.4f < 上一档 %.4f(伽马合成空间)" ↩ % [g, lum, prev_lum] | lum < prev_lum | (待填) | | | +| 1575 | 127 | "相邻组模型 CR:%s/%s = %.3f ≥1.20(色弱互辨预判)" ↩ % [order[i], order[i + 1], cr] | cr >= 1.20 | (待填) | | | +| 1576 | 131 | "群伤/魔法线性亮度序:群伤 %.4f < 魔法 %.4f(首版 CIE L* 排反事故的守门)" ↩ % [aoe_lin, magic_lin] | aoe_lin < magic_lin | (待填) | | | -## S211 _test_probe_api_shape(行 146–153 · 5 条 · 源 game/tests/unit/presentation/type_glow_test.gd · 去向:待分类回填) +## S211 _test_probe_api_shape(行 139–146 · 5 条 · 源 game/tests/unit/presentation/type_glow_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1594 | 146 | "probe 键集 = {enabled, cells, errors}" | snap.has("enabled") and snap.has("cells") and snap.has("errors") and snap.size() == 3 | (待填) | | | -| 1595 | 148 | "probe 默认态:enabled=true(Q4 丙案生产默认开)" | bool(snap.get("enabled", false)) | (待填) | | | -| 1596 | 149 | "probe 默认态:作用集空(无板未派生)" | (snap.get("cells", {}) as Dictionary).is_empty() | (待填) | | | -| 1597 | 150 | "probe 默认态:清单外登记空" | (snap.get("errors", []) as Array).is_empty() | (待填) | | | -| 1598 | 152 | "set_type_glow_enabled(false) → probe enabled=false(无板不崩)" | not bool(view.type_glow_probe().get("enabled", true)) | (待填) | | | +| 1577 | 139 | "probe 键集 = {enabled, cells, errors}" | snap.has("enabled") and snap.has("cells") and snap.has("errors") and snap.size() == 3 | (待填) | | | +| 1578 | 141 | "probe 默认态:enabled=true(Q4 丙案生产默认开)" | bool(snap.get("enabled", false)) | (待填) | | | +| 1579 | 142 | "probe 默认态:作用集空(无板未派生)" | (snap.get("cells", {}) as Dictionary).is_empty() | (待填) | | | +| 1580 | 143 | "probe 默认态:清单外登记空" | (snap.get("errors", []) as Array).is_empty() | (待填) | | | +| 1581 | 145 | "set_type_glow_enabled(false) → probe enabled=false(无板不崩)" | not bool(view.type_glow_probe().get("enabled", true)) | (待填) | | | ## S212 _init(行 16–28 · 6 条 · 源 game/tests/unit/presentation/ui_font_asset_test.gd · 去向:待分类回填) | 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet | |---|---|---|---|---|---|---| -| 1599 | 16 | "随包字体守卫装载非空(res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf)" | f != null | (待填) | | | -| 1600 | 18 | "中文字形在集(攻/雷/旗/封)" | f.has_char("攻".unicode_at(0)) and f.has_char("雷".unicode_at(0)) \\ and f.has_char("旗".unicode_at(0)) and f.has_char("封".unicode_at(0)) | (待填) | | | -| 1601 | 20 | "中文字形在集(危——已开危险格字形换代前置,迭代36 u12/wa9)" | f.has_char("危".unicode_at(0)) | (待填) | | | -| 1602 | 21 | "ASCII/数字字形在集(A/0/%)" | f.has_char("A".unicode_at(0)) and f.has_char("0".unicode_at(0)) and f.has_char("%".unicode_at(0)) | (待填) | | | -| 1603 | 25 | "中文采样文本宽度 > 0(C6 headless 判据)" | w_cn > 0.0 and w_label > 0.0 | (待填) | | | -| 1604 | 28 | "字体缺失时后续断言跳过(缺位=回归,此行仅占位计数)" | false | (待填) | | | +| 1582 | 16 | "随包字体守卫装载非空(res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf)" | f != null | (待填) | | | +| 1583 | 18 | "中文字形在集(攻/雷/旗/封)" | f.has_char("攻".unicode_at(0)) and f.has_char("雷".unicode_at(0)) \\ and f.has_char("旗".unicode_at(0)) and f.has_char("封".unicode_at(0)) | (待填) | | | +| 1584 | 20 | "中文字形在集(危——已开危险格字形换代前置,迭代36 u12/wa9)" | f.has_char("危".unicode_at(0)) | (待填) | | | +| 1585 | 21 | "ASCII/数字字形在集(A/0/%)" | f.has_char("A".unicode_at(0)) and f.has_char("0".unicode_at(0)) and f.has_char("%".unicode_at(0)) | (待填) | | | +| 1586 | 25 | "中文采样文本宽度 > 0(C6 headless 判据)" | w_cn > 0.0 and w_label > 0.0 | (待填) | | | +| 1587 | 28 | "字体缺失时后续断言跳过(缺位=回归,此行仅占位计数)" | false | (待填) | | | --- -全源共 212 章、1604 条(静态调用点口径)。 +全源共 212 章、1587 条(静态调用点口径)。 diff --git a/reports/工程改动报告-单元闸挂死守护-2026-10-28.md b/reports/工程改动报告-单元闸挂死守护-2026-10-28.md new file mode 100644 index 00000000..7109bd33 --- /dev/null +++ b/reports/工程改动报告-单元闸挂死守护-2026-10-28.md @@ -0,0 +1,32 @@ +# 工程改动报告——单元闸挂死守护(run_all.gd 每套件阻塞面超时) + +**承接**:待办清单 2026-10-25 条目「闸与探针批上下文挂死防护——probe_gate 进程树回收与 run_all 阻塞面超时」件②(件① battle_log_view 已随迭代 38 卡⑥ 出账,probe_gate.sh 整树回收件 2026-10-28 已落;本批完成后该条目整条删除,git 历史可考)。 +**车道**:工程改动直改批(改动四类,不写卡不编迭代),触及代码面走 worktree 批——分支 `runall-hangguard-20261028`,改动提交 a2710f22,并 master 后合并提交 4afe6c47。 +**实证**:迭代 40 批末阻塞面挂死实拍三例(测试 `_init` 在 `quit()` 前中断,进程挂至外层超时 124),与件②同根。 + +## 改动—验收对照表 + +| 需求点 | 改动(文件:行为) | 验收测试 | 层级 | 真实入口 | 判据绑定面 | 结果 | +|---|---|---|---|---|---|---| +| 每套件截止时间守护(默认 120s,环境变量 `RUN_ALL_SUITE_TIMEOUT_SEC` 可覆盖,非法值回落默认) | `run_all.gd` `_run_suite` 阻塞调用换 `OS.create_process` 非阻塞+200ms 轮询+截止判定;常量 `SUITE_TIMEOUT_*` 与 `_suite_timeout_sec()` | 机制验证:临时挂死夹具(`_init` 死循环不 quit)登记 manifest 后 `--only` 选跑,`RUN_ALL_SUITE_TIMEOUT_SEC=8` → 输出 `[run_all] FAIL …dev_hang_fixture… — 超时挂死(8s),进程树已整树回收(pid=…)`+`[run_all] 1 个套件失败`,进程退出码 1 | 闸基建 | `run_all.gd` `--script` 直跑(选跑态) | FAIL 行文本+批退出码 | 绿(夹具与 manifest 行验证后即删,未提交) | +| 超时整树回收零孤儿 | `taskkill /F /T /PID`(杀 cmd wrapper 连引擎孙进程)+等死确认 10s 宽限(确认不了报红留 PID)+临界窗口 `.code` 同删 | 杀树后 PowerShell 按命令行清点该 worktree 路径 Godot 进程=0 | 闸基建 | 同上+外部进程清点 | 清点输出 | 绿 | +| 退出码通道等价(文件通道值=原 cmd 回传值,判定形态不变) | 命令串追加 `& echo !ERRORLEVEL! > 文件`(`cmd /V:ON`);读毕即删+起跑前清陈值;缺文件/非数字 fail-closed 判红 | 负面套件 expect=1 照常判 PASS;全量 65 套件 `all green (65 suites)` 退出码 0 | 闸基建 | `run_all.gd` 全量直跑(与收口闸 02 同源) | PASS 行 exit 值+all green 行 | 绿 | +| README 超时记载换代 | `game/tests/README.md` 工具速查表 run_all 行补守护说明;Running Tests 段失真行「本地闸必须配外部 wall-clock 超时(run_all_gates.sh 已内置)」删除(该脚本实测无 timeout 包装),换代为内建守护说明 | 收口闸 06 gdd_lint/07 doc_link/08 doc_term/08c 流水线闸/08d 引用可核全绿 | 文档 | 闸机扫描 | 闸退出码 | 绿 | +| 非行为改动:文件头注重写 | 只描述现行行为;原头注「本地闸 02 闸包装外部超时:timeout 120s」与闸机现实不符,属失真句换代 | — | — | — | — | 非行为改动 | + +验证集清单(`game/tests/验证集清单.md`)零变化:本批不增删不改判任何套件断言,改动全部在运行器进程机制层。 + +## 机制验证的 API 实证(开工探针四件,30 秒级临时件,验证后即删) + +1. 现行 `OS.execute(…, out, true, false)` 实测同步等待(ping 约 3s 计时吻合)、返回退出码——第五参在 Godot 4.7.2 上不是阻塞开关,阻塞面挂死根因坐实; +2. `OS.create_process` 返回真实 Windows PID,`is_process_running` 存活期 true、taskkill 后 false; +3. `cmd /V:ON /C "… & echo !ERRORLEVEL! > 文件"` 退出码落盘通道成立(错误命令落 1); +4. `taskkill /F /T` 对 cmd→ping 树整树连坐闭环(存活计数 1→杀→0,cmd 自身 running=false)——镜像 probe_gate 已落件的同款机制。 + +## 闸况 + +全量域闸回执 2955c049(domain=full,dirty_game=False,总判定验收通过):影响面选闸判测量面(diff 全在 `game/tests/`)→ 00 冒烟/03/04 探针/05 渲染闸/11 回放确定性 SKIP;01 系统测试、02 单元与集成测试、06–08d 文档域闸、10 熵增闸全绿;09 静态扫描退出码 1=存量红(`level_map_screen.gd|quality/max-class-variables` 22 对基线登记 21 的恶化留账,还债归迭代 41 在案;指纹与入库基线一致,本批零新增)。合并后两件秒级事:`--editor --quit` 重刷类名缓存退出码 0+冒烟启动 17 项 0 失败三闸全过。 + +## 移交与观察 + +`game/tests/README.md`「套件 manifest 现况」行记 64 条,manifest 实数 65 条(62 单元+3 集成)——漂移先于本批存在(主区与任务分支同值),本批未动套件数,已登记待办清单新条目(tests/README manifest 计数行漂移订正)归文档批。 diff --git a/reports/工程改动报告-存量断言精简AB档-2026-10-28.md b/reports/工程改动报告-存量断言精简AB档-2026-10-28.md new file mode 100644 index 00000000..4c4472a5 --- /dev/null +++ b/reports/工程改动报告-存量断言精简AB档-2026-10-28.md @@ -0,0 +1,76 @@ +# 工程改动报告——存量断言精简(A+B 档首批删并) + +**承接**:待办清单 2026-10-28 条目「存量断言精简——验证内容不变下的断言收敛」执行形态的第一波(盘点先行→逐批裁决→分波执行→件核)。 +**车道**:工程改动触及代码面(`game/tests/*.gd`)走 worktree 批——分支 `assert-slim-20261028`,区 `../g-plan2-assert-slim-20261028`。 +**盘点方法**:机器初筛(同文件标签近似 223 对、cond 全同 42 组 109 条、跨文件同标签、护栏类 41 条)+七路只读子代理逐域语义裁决(battle_settlement/core 三路/data_config+contracts/presentation/integration+system_test)+主会话逐条抽验复核。子代理报称候选约 150 点,掺有运行时展开数与「不同查询函数间的算术蕴含」,按本批判据线核减至 19 点。 + +## 一、本批判据线(删除属显式判据改判,先立判据) + +只删满足 **同一读数面 + 同一输入 + 谓词字面等价或字面上是承接者的真子集** 的站点。两类不算冗余、本批不删: + +1. **不同查询函数之间的算术蕴含**——删了就失去对被调函数实现本身的守卫(例:`has_unrevealed()` 与 `revealed_cells().size()2 else ""` 取空串 ⟹ :76 必红 | +| 14 | #168 | roll_multiplier:131 | nat20 种子可达 | ① | #169 `:132` 行留档 roll=20+crit | `crit_seed>0` 与 `crit_rows.roll==crit_roll` 同一扫描同真同假 | +| 15 | #324 | seal_polarity:443 | 构造触达:标中标空入队 | ① | #323 `:442` 极性路由种子可达 | `_polarity_probe_accepts` 合取项(:249-252),可达即三腿同真 | +| 16 | #325 | seal_polarity:444 | 构造触达:挖危险格 ≥1 | ① | 同上 | 同上(:253) | +| 17 | #326 | seal_polarity:445 | 构造触达:挖安全格 ≥1 | ① | 同上 | 同上(:253) | +| 18 | —(口径外) | smoke_boot.gd:69 | run_scope 非空 | ① | `:70` run_scope 在树 | `!= null` ⊂ `!= null ∧ is_inside_tree()`;GDScript `and` 短路,无空引用崩溃风险 | +| 19 | —(口径外) | smoke_boot.gd:72 | _start_overlay 已装配 | ① | `:73` 启动态菜单可见 | `!= null` ⊂ `!= null ∧ visible` | + +**两条改判(标签变更,判据不变)** + +- seal_polarity:442 标签追加「缺腿:%s」,把第 15/16/17 行的三腿诊断信息折进保留条;三腿取值仍读同一 `probe`,与原三条的语义强度逐字相同。 +- roll_multiplier:132 标签追加「种子可达 seed=%d」,把第 14 行的 seed 读数折进保留条。 + +**随行清理**:theme_projection:614-625 的 `var th` 与 focus 扫描循环随其承载的两条断言一并删除(该循环只为已删断言供数据);type_glow 的 `aoe_f` 局部量随 H/S 两条删除后无消费者,一并删;两处注释改写为指认真正承载该事实的保留条。 + +## 三、件核(验证项目覆盖不变) + +| 核对项 | 结果 | +|---|---| +| 冻结清单条数 | 1604 → **1587**(−17;smoke_boot 两站点本不在清单源列,条数不受其影响) | +| `extract_checks.py --diff` | PASS——标签/cond/计数三对齐,零差异 | +| 被删 19 条标签全仓残留 grep(`tests/`+`design/`+`reports/`) | 零悬挂引用(唯一命中的「run_scope 非空」是 system_test 自己的 V2e 在册条,非本批对象) | +| 承接指认 | 15 条保留行的「处置」列逐条回填去向(旧序号+蕴含依据+批次名),新清单在册 | +| 判据等价复验 | 由代码域闸承担(见闸况)——本批不新增行为,删后被承接行为以保留断言复验 | +| 取证方式 | 未使用变异法与撤销类取证(2026-10-24/28 用户裁);删除凭据=上表逐条申报+承接者指认 | + +## 四、闸况 + +全量域闸(域=full,`dirty_game=false`,`all_green=true`,总判定「闸况与基线一致,无新增红」)回执 commit `4fbacc43`:01 系统测试、02 单元与集成测试、06/06b/07/08/08c/08d 文档域闸、10 熵增闸全绿;09 静态扫描退出码 1=存量红(`game/presentation/views/level_map_screen.gd|quality/max-class-variables` 22 对基线登记 21 的恶化留账,还债归迭代 41 在案;指纹 `8467b4588a20` 与入库基线一致,本批零新增);00 冒烟、03/04 探针、05 渲染闸、11 回放确定性由影响面选闸判 SKIP(本批 diff 全在 `game/tests/` 测量面)。 + +**本批复验缺口一条(合回后已补)**:第 18/19 两行改的是冒烟宿主自身 `smoke_boot.gd`,而 00 冒烟测试恰被影响面选闸跳过——该两站点在批末跑批中没有执行证据,只有静态蕴含论证(GDScript `and` 短路,保留条 `!= null and …` 在空值上判 false 不崩)。合回 master 后按 §3.9 code 批两件秒级事跑 `python tools/run_smoke.py`=三闸全过、`[smoke] 摘要:15 项检查,0 项失败`(原 17 项检查删两条后为 15 项计数),缺口就此闭合。第 1–17 行所在套件的复验由 01/02 全绿承担。 + +跑批踩坑一条备案:闸日志首跑写在仓库根,触 10 熵增闸「根目录文件越界」判红——我的临时件写错位置,非批次改动引发;日志挪到仓库外重跑后 10 归绿,代码面零改动,回执按同一 commit 重生成。 + +## 五、移交与观察(后续批次账) + +1. **类别② 在本仓基本不成立**:全仓钉内部路径/私有字段/中间态的断言约 52 条,被入口**严格承载 0 条**,约 51 条因覆盖入口打不到的边界/反例载荷必须保留(`fx.dispatch` 逐事件、`pending_ap_delta` 兑现时机、`_intent_charge`);且 §8.4 本身要求判据绑提交面。后续批次的可精简空间只在类别①。 +2. **前置护栏不可删**:`load_ok and diagnostics_count("error")==0` 全域 8 处属每进程 fail-fast,删致 `if load_ok:` 静默跳过→假绿,属负收益。 +3. **静默通道在冻结口径之外**:`Kit.check_quiet` 静态站点实测 **407 个**,提取器 `CALL_RE = check\(` 匹配不到该形态(test-conventions:38 已备案该工具局限)——清单条数与〔尾缀〕grep 均覆盖不到。因此「循环 quiet 与聚合条双钉」两处真冗余(node_map_growth 47/51 vs #1073/74、node_map_generator 163 vs #fv9 聚合条)删的是口径外站点,不减 1604 这个数,且聚合侧带〔fv8〕〔fv9〕验收尾缀,本批不动;若要收该账,先做提取器改版(工程批)。 +4. **恒真断言另立一账**(属判据缺陷,非重复,本批未动):feat_vouchers:51 `deck_refresh_before == 0 or true`、treasure_effects:263 `... or true`、level_shop_economy:593 与 :696 `green_min_hp > 0`(初值 99999、仅 hp>0 分支取 min,恒真);system_contracts:67 标签称「互异」而 cond 只查非空,属名实不符需改判据。建议按 §8.4 走「判据绑不上即停卡呈报」通道单独裁决。 +5. **未采的删并形态**:C 档「手写族→表驱动循环合并」(level_gen_params 逐类负向 10→1、rng_stream_boot 8 向量→1、player_copy 禁词族、standee_fx 三入参、ui_font 字形集、forge_workbench 双行)静态可再省约 25~35 点,代价=失败定位改用违例明细串(仓内已有 `forge_table_test:181-189`、`table_structure_guard:260/299` 范式);D 档「是否同一验证项目」待owner 认账项(theme:215/217 vs type_glow:148/150、level_shop_economy 现行图跨表覆盖簇 vs 〔v11〕扩面段的尾缀义务)。 +6. **example_test.gd** 两条硬编码占位断言在盘且随清单在册(旧 #S167),不进生产 manifest(run_all.gd:79-80 明示)——整件退役与否属样板件裁决,不入本账。 +7. 本批不动 `game/tests/README.md`(断言双口径行仍成立)与任何 `design/` 文件。 diff --git a/tools/pipeline_gate.py b/tools/pipeline_gate.py index fe707606..57224f7b 100644 --- a/tools/pipeline_gate.py +++ b/tools/pipeline_gate.py @@ -52,6 +52,8 @@ LEDGER_RULE_DATE = (2026, 10, 21) LEDGER_HEADING_RE = re.compile(r"^#{2,4}\s*.*(验收对照表|改动[—\-]验收)") LEDGER_REQUIRED = ("需求点", "改动", "验收测试", "真实入口") LEDGER_PROOF_MARKS = ("撤销", "证明") +BINDING_RULE_DATE = (2026, 10, 28) # req-card §8:换代为判据绑定面列,撤销/变异证明列同日禁列 +BANNED_PROOF_MARKS = ("撤销证明", "变异证明") AUDIT_MARK = "验收审计" DATE_RE = re.compile(r"\d{4}-\d{2}-\d{2}") @@ -451,7 +453,7 @@ def card_date(path): return tuple(int(x) for x in found[-1].split("-")) -def check_ledger(text, path, issues): +def check_ledger(text, path, when, issues): """C3 改动—验收对照表结构检查(生效 2026-10-21)。 只查:对照表节在不在、表头有没有五个必需列、有没有数据行、必填格空不空。 @@ -491,13 +493,29 @@ def check_ledger(text, path, issues): "缺「%s」列" % name)) return idx[name] = hit - proof_hit = next((k for k, cell in enumerate(header) - if any(mark in cell for mark in LEDGER_PROOF_MARKS)), None) - if proof_hit is None: - issues.append((str(path), header_line, "验收对照表缺列", - "缺「撤销证明」列")) - return - idx["证明"] = proof_hit + banned = next((cell for cell in header + if any(mark in cell for mark in BANNED_PROOF_MARKS)), None) + binding_hit = next((k for k, cell in enumerate(header) + if "判据绑定面" in cell), None) + if when >= BINDING_RULE_DATE: + if banned is not None: + issues.append((str(path), header_line, "验收对照表禁列", + "「%s」列已随 2026-10-24/28 两裁禁列(req-card §8.4," + "验收断言绑真实入口+判据绑定面)" % banned)) + return + if binding_hit is None: + issues.append((str(path), header_line, "验收对照表缺列", + "缺「判据绑定面」列(2026-10-28 判据口径换代,req-card §8)")) + return + idx["绑定面"] = binding_hit + else: + proof_hit = next((k for k, cell in enumerate(header) + if any(mark in cell for mark in LEDGER_PROOF_MARKS)), None) + if proof_hit is None: + issues.append((str(path), header_line, "验收对照表缺列", + "缺「撤销证明」列")) + return + idx["证明"] = proof_hit rows = 0 for i in range(header_line, len(lines)): @@ -531,7 +549,8 @@ def check_cards(root, issues): C1 判据:「关联文档同步面」表内,目标格指向 design/ 或根规则文件、而义务格仍写 「随批」者红(2026-10-17 req-card §6 改口径后,这类点只能列 reports/ 反哺点清单)。 C2 判据:全文既无「设计事实源」也无「唯一事实源」者红(两种写法都与库内实态对齐)。 - C3 判据:有「改动—验收对照表」节;表头含需求点/改动/验收测试/真实入口/证明列; + C3 判据:有「改动—验收对照表」节;表头含需求点/改动/验收测试/真实入口+证明列 + (2026-10-28 起换代为「判据绑定面」列,并禁「撤销证明/变异证明」列); 至少一行;必填格非空;全文含「验收审计」声明。语义覆盖不在此判。 """ dirn = root / CARD_DIR @@ -550,7 +569,7 @@ def check_cards(root, issues): "须含「设计事实源」或「唯一事实源」声明句(req-card §2 三选一," "GDD 节+版本号/样张+art-bible 版本/工程形态证据指认)")) if when >= LEDGER_RULE_DATE: - check_ledger(text, path, issues) + check_ledger(text, path, when, issues) if AUDIT_MARK not in text: issues.append((str(path), 1, "独立验收审计未声明", "须在卡内写明独立验收审计入口与报告文件(2026-10-21 起," diff --git a/tools/undo_proof.py b/tools/undo_proof.py deleted file mode 100644 index 071fd430..00000000 --- a/tools/undo_proof.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python3 -"""撤销证明自动化工具:一条命令完成「改前绿 → 真撤销复红 → 还原复绿」三连并出回执。 - -撤销证明的判据(test-conventions §8.4):只撤销该改动、跑能覆盖它的最窄测试、对应断言 -必须变红、还原后复绿;撤销后仍绿=断言没钉在改动上,判缺漏。本工具把这三连机械化—— -合法性属于「真撤销」(回退本批真实改动),全项目禁做变异类测试(改断言极性、置空状态、 -改名键、删调用行等人为制造红=伪造证据)。 - -用法: - python tools/undo_proof.py --cmd "<最窄测试命令>" --revert-commit <该批提交> - python tools/undo_proof.py --cmd "<最窄测试命令>" --revert-files --base - 可选 --out 落回执;--timeout <秒> 限制单次测试命令时长(缺省 3600)。 - - --revert-commit:按该笔提交触过的文件集回退到其父提交版本(改动须已提交在分支上)。 - --revert-files+--base:把指定文件回退到 (默认 HEAD)版本——适合改动尚未单独 - 成笔、混在当前尖里的场景。 - 测试命令在仓库根以 shell 执行;单套件直跑形态见 game/tests/README.md(测试工具手册)。 - -流程与退出码: - ① 改前基线:测试命令必须绿(红=改前就红,无证明意义,中止)。 - ② 真撤销:指定文件回退到基准版本 → 重跑命令,必须红(红=断言钉在该改动上)。 - ③ 还原:文件恢复到 HEAD → 重跑命令,必须绿。 - ④ 任何一步异常都先还原再退出;三步全过=退出码 0 并打印回执,否则退出码 1。 -""" - -from __future__ import annotations - -import argparse -import json -import subprocess -import sys -from pathlib import Path - -REPO_DEFAULT = Path(__file__).resolve().parent.parent -REPO = REPO_DEFAULT - - -def run(args: list[str], check: bool = True) -> str: - r = subprocess.run(["git", "-C", str(REPO)] + args, - capture_output=True, text=True, encoding="utf-8", errors="replace", - timeout=60) - if check and r.returncode != 0: - sys.stderr.write("git %s 失败:%s\n" % (" ".join(args), (r.stderr or "").strip())) - sys.exit(1) - return (r.stdout or "").strip() - - -def run_cmd(cmd: str, timeout: int) -> int: - r = subprocess.run(cmd, shell=True, cwd=str(REPO), capture_output=True, - text=True, encoding="utf-8", errors="replace", timeout=timeout) - if r.returncode != 0: - tail = "\n".join((r.stdout or "").splitlines()[-15:] or ["(无输出)"]) - sys.stderr.write(" | 测试命令失败(exit=%d),输出尾部:\n%s\n" % (r.returncode, tail)) - return r.returncode - - -def _in_rev(rev: str, f: str) -> bool: - r = subprocess.run(["git", "-C", str(REPO), "cat-file", "-e", "%s:%s" % (rev, f)], - capture_output=True, timeout=30) - return r.returncode == 0 - - -def apply_rev(rev: str, files: list[str]) -> None: - # 逐文件:基准版本存在 → 恢复到该版本;基准没有(该批新增的文件)→ 从工作区删除。 - for f in files: - if _in_rev(rev, f): - run(["checkout", rev, "--", f]) - else: - subprocess.run(["git", "-C", str(REPO), "rm", "-q", "-f", "--", f], - capture_output=True, text=True, timeout=30) - - -def main() -> None: - ap = argparse.ArgumentParser(description="自动三连出撤销证明红绿回执") - ap.add_argument("--cmd", required=True, help="最窄测试命令(shell 形,仓库根执行)") - ap.add_argument("--revert-commit", default=None, help="该批提交:按其触过的文件集回退到父版本") - ap.add_argument("--revert-files", default=None, help="逗号分隔的文件清单(与 --base 配用)") - ap.add_argument("--base", default="HEAD", help="回退基准版本(默认 HEAD)") - ap.add_argument("--timeout", type=int, default=3600, help="单次测试命令时长上限(秒)") - ap.add_argument("--repo", default=None, help="仓库/worktree 路径(缺省=本脚本所在仓库)") - ap.add_argument("--out", default=None, help="回执 JSON 落盘路径") - args = ap.parse_args() - - global REPO - REPO = Path(args.repo).resolve() if args.repo else REPO_DEFAULT - - try: - sys.stdout.reconfigure(encoding="utf-8", errors="replace") - sys.stderr.reconfigure(encoding="utf-8", errors="replace") - except Exception: - pass - - if bool(args.revert_commit) == bool(args.revert_files): - ap.error("--revert-commit 与 --revert-files 二选一") - - if args.revert_commit: - files = [x.strip() for x in run( - ["-c", "core.quotepath=false", "show", "--name-only", "--format=", - args.revert_commit]).splitlines() if x.strip()] - base = "%s^" % args.revert_commit - if not files: - sys.stderr.write("[撤销证明] 提交 %s 未触碰任何文件,无可撤销\n" % args.revert_commit) - sys.exit(2) - else: - files = [x.strip() for x in args.revert_files.split(",") if x.strip()] - base = args.base - - dirty = run(["-c", "core.quotepath=false", "status", "--porcelain", "--"] + files) - if dirty: - sys.stderr.write("[撤销证明] 指定文件带未提交改动,撤销与还原的基准不清,先提交:\n%s\n" % dirty) - sys.exit(2) - - steps: list[dict[str, object]] = [] - result = "pass" - - def step(name: str, expect: str, code: int) -> bool: - ok = (code == 0) if expect == "绿" else (code != 0) - steps.append({"step": name, "expect": expect, "exit": code, "ok": ok}) - print("[撤销证明] %s:期望%s,实测 exit=%d → %s" - % (name, expect, code, "符合" if ok else "不符合")) - return ok - - print("[撤销证明] 对象文件 %d 个;基准 %s;命令:%s" % (len(files), base, args.cmd)) - try: - if not step("① 改前基线", "绿", run_cmd(args.cmd, args.timeout)): - result = "fail(改前就红——先修绿再做撤销证明)" - else: - apply_rev(base, files) - try: - if not step("② 真撤销复红", "红", run_cmd(args.cmd, args.timeout)): - result = "fail(撤销后仍绿=断言没钉在该改动上,判缺漏)" - else: - apply_rev("HEAD", files) - if not step("③ 还原复绿", "绿", run_cmd(args.cmd, args.timeout)): - result = "fail(还原后未复绿——还原面有残留,查工作区)" - finally: - apply_rev("HEAD", files) - except subprocess.TimeoutExpired: - result = "fail(测试命令超时)" - finally: - # 兜底还原:无论走到哪一步,退出前把对象文件恢复到 HEAD 态 - apply_rev("HEAD", files) - - residue = run(["-c", "core.quotepath=false", "status", "--porcelain", "--"] + files) - if residue: - result = "fail(退出前工作区仍有对象文件残留改动,手工核对)" - - print("─" * 56) - print("[撤销证明] 结论:%s" % result) - if args.out: - with open(args.out, "w", encoding="utf-8") as f: - json.dump({"cmd": args.cmd, "base": base, "files": files, - "steps": steps, "result": result}, f, ensure_ascii=False, indent=1) - print("[撤销证明] 回执已落 %s" % args.out) - sys.exit(0 if result == "pass" else 1) - - -if __name__ == "__main__": - main() diff --git a/待办清单.md b/待办清单.md index 71d3026b..17402668 100644 --- a/待办清单.md +++ b/待办清单.md @@ -2,9 +2,9 @@ 本清单是流水线的唯一登记处:要做的事先在此登记一条,随流水线段推进改状态,做完整条删除;各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。 -#### 存量断言内部直查全量归位——判据达标专项批(2026-10-28 登记,来源=会话提及) +#### 存量断言精简——验证内容不变下的断言收敛(2026-10-28 登记,来源=会话提及) -**实况**:目标=unit/integration 存量断言级内部直查 89 处(grep 口径,2026-10-28 本会话实测)按 test-conventions §8.2 新判据(145c1865 落文)全部改钉公开可观测面。证据=§8.2 受控例外条文+本会话 grep 计数;样板=treasure_effects_test(e62e90c8 试点批,12 处改钉零产品码全绿)。执行形态=工程改动 worktree 批(纯测试维护零行为改动,不写卡不编迭代):盘点先行(逐套件定四类改法——属性直换/公开面等价式/真实路径重塑/公开面对比——并标出需产品码只读访问器的呈报项)→ 按域分波每波闸 → 收尾件核(断言级直查 grep=0)。边界:只清断言级(`Kit.check` 行内 `._` 命中),setup 直调写口构造状态不动(§5.4 惯例);断言标签逐字不动(验证集清单锚);禁放宽/删断言;需访问器项逐处呈用户裁(产品码边界);非 quiet 断言改判同步验证集清单对账。时机=排在迭代 41 之前,与波 0 gdstyle 基线刷新互不影响(tests 不在扫描面)。 +**实况**:目标=减少现有断言数量、更精简,配合 §8.2 接口测试导向(对外方法、功能行为验证),前提=验证清单的验证项目覆盖逐条不变。证据=判据达标专项批已完成(本清单已销号条目,59 改钉+35 例外——全部断言已钉公开可观测面,每条断言的验证对象已显式化,做减法有了干净基线);验证集清单 1604 静态调用点为对照基准。口径界定:验证项目≠check 调用点——验证项目(卡片验收条目/GDD 登记验证面)覆盖逐条不降,check 调用点可降;可删并的两类=①同一行为被多条断言重复钉(同族合并)②钉内部路径细节而该行为已被入口断言严格承载(逐条申报承接者)。删除属显式判据改判:逐条申报理由+承接者指认(写入验证集清单「处置/去向」列),禁批量拍删;删后被承接行为以保留断言复验(套件绿),禁变异法。执行形态=工程改动 worktree 批:盘点先行(逐套件标冗余候选两类清单)→ 用户逐批裁决删并清单 → 分波执行每波闸 → 件核(验证项目覆盖不变+清单对账 PASS+条数下降留痕)。盘点与第一波执行凭据:机器初筛(同文件标签近似 223 对/cond 全同 42 组 109 条/护栏类 41 条)+七路只读语义裁决+逐条抽验,判据线收紧为「同一读数面+同一输入+谓词字面等价或字面是承接者真子集」,候选由约 150 点核减至 19 点;第一波(A+B 档 19 站点,其中 17 在册+`smoke_boot.gd` 2 点口径外)已合回——删并申报表与承接者逐条指认 `reports/工程改动报告-存量断言精简AB档-2026-10-28.md`,清单 1604→1587 且 `--diff` 对账 PASS、15 条保留行「处置」列去向在册,全量域闸回执 commit `20c13c3a`(domain=full/all_green/09 静态扫描存量红零新增),合并提交 65054b1d。余账四条:①类别② 实测不成立(全仓内部面断言约 52 条被入口严格承载 0 条,后续波次只在类别① 内找)②C 档手写族改表驱动循环合并(静态约再省 25~35 点,代价=定位改用违例明细串)③D 档「是否同一验证项目」待 owner 认账四处(本批用户裁全保留)④恒真与名实不符断言五条属判据缺陷另账,走 §8.4 呈报通道非精简账;另 静默收集通道 `Kit.check_quiet` 静态站点 407 个在冻结口径外(test-conventions:38 已备案工具局限),要收该账先做提取器改版。 **改动面**:游戏代码 @@ -397,33 +397,23 @@ **状态**:待写卡(备选) -#### 闸与探针批上下文挂死防护——probe_gate 进程树回收与 run_all 阻塞面超时(2026-10-25 登记,来源=批次移交) +#### tests/README manifest 计数行漂移订正——「64 条」对实际 65 条(2026-10-28 登记,来源=批次移交) -**实况**:证据=迭代 36 批末闸机观测(非本批引入)。原登记两件存量噪声,件①(battle_log_view.gd:17 类型声明错致运行期 TypedArray 错误、日志失真)已随迭代 38 卡⑥ 收口出账(git 提交 ce49a8ee),本条只剩件②:`level_shop_economy_test` 在闸批量上下文存在挂死形态(直跑恒绿),`tools/probe_gate.sh` 的 per-suite timeout 只杀 wrapper 不杀引擎子进程,孤儿泄漏会殃及后续全部 Godot 启动(批末四次闸跑批中断的根源,处置=清杀泄漏进程+删重建 `game/.godot`)。挂死防护=闸壳/测试基建,按改动四类属**工程改动走直改通道**。 -- 直改落点:tools/probe_gate.sh(per-suite 进程树回收)+game/tests/run_all.gd(OS.execute 阻塞面超时守护)——测试基建直改。**2026-10-28 用户裁四批全做,probe_gate 件已落**(run_suite 整树回收:启动记 WINPID → 超时先 `taskkill /T` 杀全树再 `kill -9` 收 MSYS 侧〔顺序颠倒会让 /T 走不到已死父进程〕→ 标志文件判超时〔taskkill 强杀后 MSYS wait 可能报 0〕;默认五段实跑全绿+超时/正常双路径机制验证通过)。run_all.gd 件属代码面(game/tests),按车道铁律留待触及测试基建的 worktree 批顺带,本条目保持待执行。**2026-10-28 迭代 40 批末补实证(阻塞面挂死实拍)**:迁移类改动的回退使测试 `_init` 在 `quit()` 前中断(红行形态 `Invalid call. Nonexistent function`),进程挂至外层超时 124——卡① wr1/wr4/wr9 取证轮三例复现,与件②同根(阻塞面无超时守护),实证链在案(实现报告 §三第 1 条)。 +**实况**:证据=单元闸挂死守护批收尾实数(合并提交 4afe6c47,报告 reports/工程改动报告-单元闸挂死守护-2026-10-28.md)——`game/tests/run_all.gd` manifest 实为 65 条(62 单元+3 集成,grep '{"path"' 计数),`game/tests/README.md`「套件 manifest 现况」行仍记「64 条(61 单元+3 集成,2026-10-28 实数)」。漂移先于该批存在(主区与任务分支同值),该批未动套件数不属其 scope,登记归文档批订正。 - -**改动面**:工具代码 +**改动面**:文档 **状态**:待执行 -#### undo_proof.py 退役——撤销证明禁令生效后的死工具(2026-10-28 登记,来源=批次移交) +#### 主角棋盘小人像素管线重产——原「美术资产链路审计两项 P0」之 P0②(2026-10-09 登记,来源=审计发现) -**实况**:证据=test-conventions §8.4 现行口径(2026-10-24/2026-10-28 用户两裁:禁变异类测试+禁撤销类取证,验收唯一主判据=真实入口断言当轮执行并通过;AGENTS.md 红线「验收闭合」与 req-card SKILL 第 8 条同批换代)。`tools/undo_proof.py`(159 行,挂死防护批产物)为撤销证明的自动化载体(三连红绿回执并自还原),服务对象已被禁令整体废止——工具本体退役删除。同族残留顺带裁:`.zcode|.qoder/skills/iter/evals/files/mock-card-iter40-1.md` 表头「撤销/变异证明」字样(两份,反向残留);`tools/pipeline_gate.py` 是否加「变异测试/撤销证明」短语级扫描防复发(**短语级**——按「变异」单词扫会命中地形变异等玩法词,实测假红面大)。 +**实况**:主角棋盘小人未走像素管线(插值糊图),重产事项未在任何登记处在册;审计报告 HTML 件从未进版本库、现已删除,现状以本条正文为准。2026-10-13 用户令随「UI 美化三层诊断」登记:本条属第三层「观感提升」,Alpha 阶段需完成。原 P0①(文档面:两处引用已丢失的冻结样张路径)2026-10-28 按用户裁乙销号——art-bible §1.4 冻结记录与 art-status-checklist P0 样张行删路径、留「产物原件已丢失」,机检报告凭据(git 提交 7a847c7)保留。 -**改动面**:工具代码 +**缺口**:还差美术面的重产(按像素管线重画主角棋盘小人),归美术设计段。 -**状态**:待执行 +**改动面**:美术素材 -#### 美术资产链路审计两项 P0 — 待用户裁决(2026-10-09 登记,来源=审计发现) - -**实况**:**P0①(文档面)**:`design/art/art-bible.md` 第 68/527 行与 art-status-checklist 第 13 行至今仍引用已丢失的冻结样张 sample-batch-v3-final-2026-09-30.png(磁盘实测零命中),死指涉待修复。**P0②(美术面)**:主角棋盘小人未走像素管线(插值糊图),重产事项未在任何登记处在册。审计报告 HTML 件从未进版本库、现已删除,两项 P0 的现状以本条正文为准。裁=修复死指涉+登记重产,或判过期随旧管线退役。2026-10-13 用户令随「UI 美化三层诊断」登记:本条属第三层「观感提升」,Alpha 阶段需完成。 - -**缺口**:还差用户的路线裁决(修复死指涉+登记重产,还是判过期随旧管线退役)——裁决后文档面修复与美术重产登记各按轨推进。 - -**改动面**:文档+美术素材 - -**状态**:待文档设计+待美术设计 +**状态**:待美术设计 #### 技术债观察登记——长函数余量与 lambda 连接防回归(2026-10-15 登记,来源=批次移交)