From 5277848151141f0f693b69cf7604a15c1aa438cc Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 28 Oct 2026 06:24:31 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E5=BD=92=E4=BD=8D=E6=B3=A21):=20adventure=5Ffeats=EF=BC=8Bfeat?= =?UTF-8?q?=5Fvouchers=EF=BC=8Bcurio=5Frefresh=5Fpaid=20=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E7=BA=A7=E5=86=85=E9=83=A8=E7=9B=B4=E6=9F=A5=E6=94=B9=E9=92=89?= =?UTF-8?q?=E5=85=AC=E5=BC=80=E5=8F=AF=E8=A7=82=E6=B5=8B=E9=9D=A2=E2=80=94?= =?UTF-8?q?=E2=80=94adventure=5Ffeats=2025=20=E8=A1=8C=E6=94=B9=E9=92=89(4?= =?UTF-8?q?4=20=E5=91=BD=E4=B8=AD=E7=89=87=E6=AE=B5:29=20=E6=96=AD?= =?UTF-8?q?=E8=A8=80=20=5Fhits=5Fof=E2=86=92=E6=B5=8B=E8=AF=95=E4=BE=A7?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E7=AD=89=E4=BB=B7=E8=BF=87=E6=BB=A4=E9=92=89?= =?UTF-8?q?=E5=85=AC=E5=BC=80=20fx.dispatch=20=E8=BF=94=E5=9B=9E=E5=80=BC?= =?UTF-8?q?=EF=BC=8B5=20=E7=8A=B6=E6=80=81=E9=92=89=E9=9D=A2:=E4=BF=A1?= =?UTF-8?q?=E5=8F=B7=E8=BD=BD=E8=8D=B7/=E5=85=AC=E5=BC=80=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=8F=A3/=E4=BB=B6=E5=87=BA=E5=8F=A3)+2=20=E4=BE=8B?= =?UTF-8?q?=E5=A4=96(hero=5Finfo=5Fpanel.=5Ffeat=5Fsection=20=E6=97=A0?= =?UTF-8?q?=E5=85=AC=E5=BC=80=E8=AF=BB=E6=95=B0=E5=8F=A3);feat=5Fvouchers?= =?UTF-8?q?=2010=20=E6=94=B9=E9=92=89(pick=5Fcurio/consume=5Fpool=5Fpurcha?= =?UTF-8?q?se=20=E7=9C=9F=E5=AE=9E=E8=B7=AF=E5=BE=84=C3=974+step=5Frow=20i?= =?UTF-8?q?nsurance=5Fused=20=E8=A1=8C=E8=BD=BD=E8=8D=B7=C3=973+=E7=AD=89?= =?UTF-8?q?=E4=BB=B7=E5=BC=8F=C3=973)+4=20=E4=BE=8B=E5=A4=96(=5Fpending=5F?= =?UTF-8?q?rap/=5Ftime=5Fsand=5Fqueue=20=E6=97=A0=E5=85=AC=E5=BC=80?= =?UTF-8?q?=E8=AF=BB=E5=8F=A3,=5Fperfect=5F*=20=E5=88=A4=E9=A2=98=E5=89=8D?= =?UTF-8?q?=E9=9B=B6=E6=80=81=E6=97=A0=E5=85=AC=E5=BC=80=E9=9D=A2);curio?= =?UTF-8?q?=5Frefresh=5Fpaid=207/7=20=E5=85=A8=E6=94=B9=E9=92=89(refresh?= =?UTF-8?q?=5Fy=20=E7=9B=B4=E6=8D=A2=C3=974+gp=5Fcost=20=E6=8A=A5=E4=BB=B7?= =?UTF-8?q?=E5=85=AC=E5=BC=8F=E7=AD=89=E4=BB=B7=E5=BC=8F=C3=972+free=5Flef?= =?UTF-8?q?t=20=E6=81=92=E7=AD=89=E5=BC=8F=C3=971);=E4=B8=89=E5=A5=97?= =?UTF-8?q?=E4=BB=B6=E5=8D=95=E8=B7=91=E5=85=A8=E7=BB=BF(138/49/21=20check?= =?UTF-8?q?s),check=20=E8=B0=83=E7=94=A8=E6=95=B0=E6=94=B9=E5=89=8D?= =?UTF-8?q?=E5=90=8E=E4=B8=80=E8=87=B4=E9=9B=B6=E5=88=A0=E6=96=AD=E8=A8=80?= =?UTF-8?q?,=E6=A0=87=E7=AD=BE=E9=80=90=E5=AD=97=E6=9C=AA=E5=8A=A8,?= =?UTF-8?q?=E4=BA=A7=E5=93=81=E7=A0=81=E9=9B=B6=E6=94=B9=E5=8A=A8;?= =?UTF-8?q?=E4=BE=8B=E5=A4=96=206=20=E5=A4=84=E6=8C=89=20=C2=A78.2=20?= =?UTF-8?q?=E5=8F=97=E6=8E=A7=E4=BE=8B=E5=A4=96=E5=8F=A3=E5=BE=84=E7=99=BB?= =?UTF-8?q?=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/tests/unit/core/adventure_feats_test.gd | 97 +++++++++++-------- .../unit/core/curio_refresh_paid_test.gd | 15 +-- game/tests/unit/core/feat_vouchers_test.gd | 48 ++++++--- 3 files changed, 99 insertions(+), 61 deletions(-) diff --git a/game/tests/unit/core/adventure_feats_test.gd b/game/tests/unit/core/adventure_feats_test.gd index e14ea2d7..c9929b8b 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.adventure.curio_value("gp", "common")), a13.stop().apply_node_curio("gp", "common") and a13.wallet().gp == gp13 + int(_db.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))) @@ -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.feats.row_by_id("arcane_ember").is_empty() and _db.feats.row_by_id("magma_body").is_empty()) Kit.check("迁出收尾: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()) @@ -513,7 +524,7 @@ func _smoke_feats() -> void: 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.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.adventure.feat_ticket_limit) a_t.stop()._deck.ticket_pity = int(a_t.db.adventure.feat_ticket_pity_step) Kit.check("A6 限购满不再触发(保底冻结)", not a_t.deck().feat_ticket_consider(rg_t)) @@ -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/curio_refresh_paid_test.gd b/game/tests/unit/core/curio_refresh_paid_test.gd index e317fc72..527ad078 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 8c691ae7..0a78989e 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.adventure.CURIO_KINDS.has("attune_battery") and not db.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〕") From 89a368227bd091854b8a0dc0989efe6d9b3f83ff Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 28 Oct 2026 06:47:31 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E5=BD=92=E4=BD=8D=E6=B3=A22+3):=20=E5=85=B6=E4=BD=99=2041=20?= =?UTF-8?q?=E5=A4=84=E6=96=AD=E8=A8=80=E7=BA=A7=E5=86=85=E9=83=A8=E7=9B=B4?= =?UTF-8?q?=E6=9F=A5=E5=A4=84=E7=BD=AE=E5=AE=8C=E6=AF=95=E2=80=94=E2=80=94?= =?UTF-8?q?=E6=94=B9=E9=92=89=2014=20=E5=A4=84:=20settle=5Fsegments=20geol?= =?UTF-8?q?ogist=20=E8=AE=A2=E9=98=85=E8=B5=B0=20settle=5Fbatch=20?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E8=B7=AF=E5=BE=84(=E5=85=AC=E5=85=B1?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E5=85=A5=E5=8F=A3+=E5=8F=97=E7=90=86?= =?UTF-8?q?=E8=BD=BD=E8=8D=B7+EffectEngine=20=E5=85=AC=E5=BC=80=E8=AE=A1?= =?UTF-8?q?=E6=95=B0);core=20=E9=9B=B6=E6=95=A3=E7=BB=84=2012=20=E5=A4=84?= =?UTF-8?q?=E5=85=A8=E6=94=B9=E9=92=89(breakpoint=5Ffragment=20=E5=85=AC?= =?UTF-8?q?=E5=BC=80=E5=BF=AB=E7=85=A7=E5=8F=A3/deck()=20=E4=BB=B6?= =?UTF-8?q?=E5=87=BA=E5=8F=A3/current=5Fprice=20=E5=85=AC=E5=BC=80?= =?UTF-8?q?=E7=AD=89=E4=BB=B7=E5=BC=8F/current=5Fbattle().encounter=20?= =?UTF-8?q?=E5=85=AC=E5=BC=80=E5=AD=97=E6=AE=B5/has=5Fwrite=5Ftarget=20?= =?UTF-8?q?=E4=B8=A5=E6=A0=BC=E7=AD=89=E4=BB=B7);presentation=20=E8=A1=A5?= =?UTF-8?q?=E9=92=89=202=20=E7=BB=84(standee=20flash=5Fcount=20=E8=B5=B0?= =?UTF-8?q?=20fx=5Fprobe=20=E5=85=AC=E5=BC=80=E5=8F=A3;meta=20=E5=88=B7?= =?UTF-8?q?=E6=96=B0=20X/Y=20=E8=B5=B0=20refresh=5Fy/free=5Fleft=20?= =?UTF-8?q?=E7=AD=89=E4=BB=B7=E5=BC=8F=E2=80=94=E2=80=94=E4=B8=A4=E5=A4=84?= =?UTF-8?q?=E5=9D=87=E4=B8=BA=E5=A4=9A=E8=A1=8C=E5=BB=B6=E7=BB=AD=E8=A1=8C?= =?UTF-8?q?=E6=BC=8F=E7=BD=91,=E6=89=A7=E8=A1=8C=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E4=B8=8E=E6=B3=A2=E4=B8=BB=E8=A1=A5=E8=8E=B7);=E4=BE=8B?= =?UTF-8?q?=E5=A4=96=2035=20=E5=A4=84=E6=8C=89=20=C2=A78.2=20=E5=8F=97?= =?UTF-8?q?=E6=8E=A7=E4=BE=8B=E5=A4=96=E7=99=BB=E8=AE=B0(=E5=91=88?= =?UTF-8?q?=E7=8E=B0=E5=9F=9F=2017:=E9=9D=A2=E6=9D=BF/=E7=94=9F=E6=B6=AF?= =?UTF-8?q?=20Label=C2=B7=E8=93=84=E5=8A=9B=E6=A1=A3=C2=B7=E7=BD=AE?= =?UTF-8?q?=E7=81=B0=E6=80=81=C2=B7=E6=8C=82=E8=B5=B7=E6=A7=BD=C2=B7forge?= =?UTF-8?q?=20=E5=8F=AF=E8=A7=81=E6=80=A7=E5=9D=87=E6=97=A0=E5=85=AC?= =?UTF-8?q?=E5=BC=80=E8=AF=BB=E6=95=B0=E5=8F=A3;=E6=88=98=E6=96=97?= =?UTF-8?q?=E5=9F=9F=2012:=E4=BA=94=E6=AE=B5=E6=8E=A5=E7=BA=BF=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=C2=B7=E5=A5=96=E5=8A=B1=E6=9D=83=E9=87=8D=E6=8A=95?= =?UTF-8?q?=E5=BD=B1=C2=B7=E8=B8=A9=E9=9B=B7=E8=B4=A6=E6=9C=AC=E6=97=A0?= =?UTF-8?q?=E5=85=AC=E5=BC=80=E6=8A=95=E5=BD=B1;core=206:=E5=BE=85?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=E8=AF=BB=E5=8F=A3=C2=B7=E5=88=A4=E9=A2=98?= =?UTF-8?q?=E5=89=8D=E9=9B=B6=E6=80=81);=E4=BB=B6=E6=A0=B8=3D=E4=BD=99=203?= =?UTF-8?q?6=20=E5=91=BD=E4=B8=AD=E4=B8=AD=2035=20=E5=AF=B9=E5=BA=94?= =?UTF-8?q?=E4=BE=8B=E5=A4=96=E5=8F=B0=E8=B4=A6+1=20=E4=B8=BA=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E6=96=87=E6=9C=AC=E6=8F=90=E5=8F=8A(=5Ftree=5Frules?= =?UTF-8?q?=20=E6=9D=A1=E4=BB=B6=E5=B7=B2=E6=94=B9)=E9=9D=9E=E7=9B=B4?= =?UTF-8?q?=E6=9F=A5;13=20=E5=A5=97=E4=BB=B6=E5=8D=95=E8=B7=91=20exit=200?= =?UTF-8?q?=20=E5=85=A8=E7=BB=BF,=E6=A0=87=E7=AD=BE=E9=80=90=E5=AD=97?= =?UTF-8?q?=E6=9C=AA=E5=8A=A8,=E4=BA=A7=E5=93=81=E7=A0=81=E9=9B=B6?= =?UTF-8?q?=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/unit/core/breakpoint_resume_test.gd | 6 ++-- game/tests/unit/core/hero_ledger_test.gd | 6 ++-- .../unit/core/level_shop_economy_test.gd | 2 +- .../unit/core/meta_progression_forge_test.gd | 15 ++++---- game/tests/unit/core/pool_case_d_test.gd | 6 ++-- .../unit/core/save_boot_no_activate_test.gd | 2 +- game/tests/unit/core/settle_segments_test.gd | 35 +++++++++++++++++-- .../presentation/standee_fx_sweep_test.gd | 2 +- 8 files changed, 55 insertions(+), 19 deletions(-) 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/hero_ledger_test.gd b/game/tests/unit/core/hero_ledger_test.gd index de96b9d1..a32a8021 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_shop_economy_test.gd b/game/tests/unit/core/level_shop_economy_test.gd index 704bd083..f75a944d 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.adventure.curio_value(kind, tier))], a8.journal().bonus_total(String(val_map[kind])) == val0 + int(_db.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 4e8683b7..d1dd3429 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() @@ -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.shop_price_level_bonus(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.shop_price_level_bonus(0))) * float(_db.shop.spend_discount)))) + and int(lr.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.shop_price_level_bonus(0))) * float(_db.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/pool_case_d_test.gd b/game/tests/unit/core/pool_case_d_test.gd index c7768486..3319f765 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 7f6a60e1..2c0725f7 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.active_slot() == 0 and not sm.has_write_target(), + Kit.check_quiet(sm.active_slot() == 0 and not sm.has_write_target(), "sv2 无参实例无激活态(路径空/激活号 0/写盘目标空)〔卡① sv2〕") Kit.check_quiet(sm.keystone_balance() == 0 and not sm.has_breakpoint() and sm.adventure_total() == 0 and sm.adventure_cleared() == 0, diff --git a/game/tests/unit/core/settle_segments_test.gd b/game/tests/unit/core/settle_segments_test.gd index 7e15cf4c..dd469fc8 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.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/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) From fbeab3e3fd3d814e68ae2555c6308558505302aa Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 28 Oct 2026 06:47:52 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E5=BD=92=E4=BD=8D=E6=89=B9=C2=B7=E6=B8=85=E5=8D=95=E5=90=8C?= =?UTF-8?q?=E6=AD=A5):=20=E9=AA=8C=E8=AF=81=E9=9B=86=E6=B8=85=E5=8D=95?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E9=87=8D=E7=94=9F=E6=88=90=E2=80=94=E2=80=94?= =?UTF-8?q?level=5Fshop=5Feconomy=20:1325=20=E9=9D=9E=20quiet=20=E6=96=AD?= =?UTF-8?q?=E8=A8=80=E6=94=B9=E9=92=89(deck().taken=20=E5=85=AC=E5=BC=80?= =?UTF-8?q?=E9=9D=A2)=E8=87=B4=20cond=20=E6=91=98=E8=A6=81=E6=BC=82?= =?UTF-8?q?=E7=A7=BB=E4=B8=80=E6=9D=A1,--carry-from=20=E6=89=BF=E6=8E=A5?= =?UTF-8?q?=E5=9B=9E=E5=A1=AB=E5=88=97,=E5=AF=B9=E8=B4=A6=20PASS=20?= =?UTF-8?q?=E9=9B=B6=E5=B7=AE=E5=BC=82;=E5=85=B6=E4=BD=99=E6=94=B9?= =?UTF-8?q?=E9=92=89=E5=85=A8=E8=90=BD=20check=5Fquiet=20=E9=80=9A?= =?UTF-8?q?=E9=81=93(=E6=8F=90=E5=8F=96=E6=89=AB=E6=8F=8F=E9=9D=A2?= =?UTF-8?q?=E5=A4=96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/tests/验证集清单.md | 480 +++++++++++++++++++-------------------- 1 file changed, 240 insertions(+), 240 deletions(-) diff --git a/game/tests/验证集清单.md b/game/tests/验证集清单.md index 0dbc51ed..a4cbca70 100644 --- a/game/tests/验证集清单.md +++ b/game/tests/验证集清单.md @@ -1,6 +1,6 @@ # 验证集清单(断言快照·冻结) -> 生成日期: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:`89a368227bd091854b8a0dc0989efe6d9b3f83ff` · 源: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 个静态调用点;行级 > `grep -c "check("` 是另一口径(含 `func check(` 定义行与注释行),两口径各自浮动, @@ -817,156 +817,156 @@ | 452 | 115 | "开域后受理已知 id(布尔受理口径 true 通道)" | effects.apply_effect(probe_id, src) == true | (待填) | | | | 453 | 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() | (待填) | | | +| 454 | 70 | "冒险启动受理(主种子注入恰一次——state=running;种子 42=首局固定种子生成图——精确路径断言需要确定图(卡② 代差))" | adr.start_adventure(42) | (待填) | | | +| 455 | 71 | "进行中重复 start_adventure 拒绝(重开须待结算完毕)" | not adr.start_adventure(43) | (待填) | | | +| 456 | 72 | "停点前移次序契约:level_started 先于入口候选呈现(反序选路口会被呈现层 runner 空防御丢弃)" | sig_seq.size() == 2 and String(sig_seq[0]) == "level_started" and String(sig_seq[1]) == "choices_presented" | (待填) | | | +| 457 | 73 | "入口停点:选路态 + 入口单候选 + 首战未开(呈现零 RNG 消耗——pick 前零战斗域)" | adr.runner().awaiting_choice() and (obs["entry_cands"] as Array).size() == 1 \\ and adr.runner().current_battle() == null | (待填) | | | +| 458 | 76 | "pick 入口候选受理(同一 _enter_node——与旧自动进入逐位等价)" | adr.runner().pick_next_node(int(adr.runner().next_candidate_ids()[0])) | (待填) | | | +| 459 | 77 | "pick 后 node_entered 到达 + 首战在飞(接口①开始关卡——停点闭合直达战斗节点)" | sig_seq.size() == 3 and String(sig_seq[2]) == "node_entered" and adr.runner().current_battle() != null | (待填) | | | +| 460 | 78 | "战斗中 abandon 拒绝(战斗域单事务在飞)" | not adr.abandon() | (待填) | | | +| 461 | 87 | "首战胜后选路态(非战斗期——current_battle 置空)" | adr.runner().current_battle() == null and adr.runner().awaiting_choice() | (待填) | | | +| 462 | 88 | "非战斗期 abandon 受理(选路态——同失败型结算)" | adr.abandon() | (待填) | | | +| 463 | 90 | "放弃结算:result=abandoned · cleared=false(已获面不追回——钥石累计 %d)" ↩ % int(sum.get("keystone_total", -1)) | String(sum.get("result", "")) == "abandoned" and not bool(sum.get("cleared", true)) | (待填) | | | +| 464 | 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)) | (待填) | | | +| 465 | 127 | "序列关数 = 行集关数(三关正式序列生效)" | int(_db.level.rows.size()) == 3 | (待填) | | | +| 466 | 144 | "生成图每个战斗/Boss 节点经解析链命中敌行集与棋盘行(扫描 %d 节点,缺失 %d)〔fv24〕" ↩ % [cov_scan, cov_miss] | cov_scan > 0 and cov_miss == 0 | (待填) | | | +| 467 | 149 | "满配冒险成功(42 / atk+900 / hp+400——cleared=true;v2 怪阶梯后旧 200/100 档不再满配)" | String(st["result"]) == "completed" and bool(sum.get("cleared", false)) | (待填) | | | +| 468 | 150 | "完成几关 = 3(levels_cleared)" | int(sum.get("levels_cleared", -1)) == 3 | (待填) | | | +| 469 | 154 | "钥石合计 = ΣBoss 首杀行值(%d——逐杀掉落新口径;断点未注入恒首杀)" ↩ % int(sum.get("keystone_total", -1)) | int(sum.get("keystone_total", -1)) == ks_exp | (待填) | | | +| 470 | 156 | "逐杀掉落明细恰 3 笔(L1/L2/L3——additive 摘要键 keystone_drops)" | drops.size() == 3 | (待填) | | | +| 471 | 157 | "摘要携带主种子(42)" | int(sum.get("master_seed", -1)) == 42 | (待填) | | | +| 472 | 168 | "关卡完成三选一恰呈现 2 次、恰兑付 2 次(L1/L2 后——末关不发;resolves 含节点回执另计)" | picks.size() == 2 and resolves.size() == 2 + node_picks | (待填) | | | +| 473 | 179 | "三关生成图各含 ≥1 奖励节点(节点三选一候选位在图)〔v4〕" | reward_levels == 3 | (待填) | | | +| 474 | 183 | "奖励分支点选→三选一候选可取且停点在途(候选 %s 件)" ↩ % str((rw.get("cands", []) as Array).size()) | bool(rw.get("pending", false)) and (rw.get("cands", []) as Array).size() >= 1 | (待填) | | | +| 475 | 196 | "奖励三选一点选兑付受理(件=%s)" ↩ % String(rw_pc.get("kind", "")) | rw_pick_ok | (待填) | | | +| 476 | 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)) | (待填) | | | +| 477 | 211 | "奖励节点兑付回执与经停逐笔相等(context=reward_node——同账计数 %d/%d)" ↩ % [node_resolves, node_picks] | node_resolves == node_picks | (待填) | | | +| 478 | 258 | "三选一候选 2–3 枚且零重复(令牌洗牌不放回——kind+tier 复合身份)" | cand_ok | (待填) | | | +| 479 | 259 | "关卡完成部分回血封顶(floor(观测上限×比例) 封顶——购血观测值精确断言,核代码增补)" | heal_ok | (待填) | | | +| 480 | 260 | "钥石逐杀入账(首杀档位 + 累计逐位)" | ks_gain_ok | (待填) | | | +| 481 | 261 | "点选恰兑付选中一件(值=令牌档位表值;未选项零生效)" | resolve_ok | (待填) | | | +| 482 | 272 | "各战上限入参 ∈ [基础, 基础+3×件值+购血余量](覆盖值通道——双渠道共享上限 3;模拟档贪心购血合法叠加〔数值修复批 v3.4〕;首战=基础 %d)" ↩ % exp_base | seq_ok | (待填) | | | +| 483 | 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"] | (待填) | | | +| 484 | 285 | "零配 L1 败北种子可达(seed=%s)" ↩ % str(lose.get("s", -1)) | not lose.is_empty() | (待填) | | | +| 485 | 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 | (待填) | | | +| 486 | 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"] | (待填) | | | +| 487 | 301 | "半配(atk+50)L2 败北种子可达(seed=%s)" ↩ % str(half.get("s", -1)) | not half.is_empty() | (待填) | | | +| 488 | 304 | "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 | 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"] | (待填) | | | +| 490 | 315 | "重复拒绝场景:冒险启动(seed 7——生成图确定同上)" | adr6.start_adventure(7) | (待填) | | | +| 491 | 323 | "重复拒绝场景就位:选路态放弃 → 已结算" | adr6.abandon() | (待填) | | | +| 492 | 326 | "重复关卡结束宣布拒绝(账本与摘要零变化——契约 7)" | adr6.summary == sum6_before | (待填) | | | +| 493 | 336 | "每战完成入账恰一次(gain %d = battles %d——GP 袋件入账单列)" ↩ % [gains, int(st["battles"])] | gains == int(st["battles"]) | (待填) | | | +| 494 | 337 | "钱包 clear 恰一次且为末事件(关卡收束不清——GP 冒险收束清零归冒险结构)" | clears == 1 and wallet.size() > 0 and String(wallet[wallet.size() - 1]["event"]) == "clear" | (待填) | | | +| 495 | 339 | "三关商店房各到达一次(每关入口→终点路径恰穿一店的驱动投影)且首达余额 > 0(%d——GP 跨关延续)" ↩ % (shop_seq[0] if shop_seq.size() > 0 else -1) | shop_seq.size() == 3 and int(shop_seq[0]) > 0 | (待填) | | | +| 496 | 350 | "同账场景种子可达(seed=%s——L2 商店房就位)" ↩ % str(same.get("s", -1)) | not same.is_empty() | (待填) | | | +| 497 | 363 | "进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" ↩ % [hp_price + l2_price_bonus, hp_price, l2_price_bonus] | gp0 >= hp_price + l2_price_bonus | (待填) | | | +| 498 | 364 | "购买 hp_up 受理(出账 + 加算区)" | r8.buy_product("hp_up") | (待填) | | | +| 499 | 366 | "统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" ↩ % acc_shop | acc_shop >= 3 | (待填) | | | +| 500 | 371 | "L2 关卡完成三选一停点在案(直调兑付前——零自动推进)〔拆件批 V2〕" | not (l2_cap["payload"] as Dictionary).is_empty() | (待填) | | | +| 501 | 372 | "未兑付推进拒绝(选择停点零自动推进契约)" | not a8.continue_to_next_level() | (待填) | | | +| 502 | 375 | "三选一点选兑付受理(候选取首件)" | a8.stop().pick_curio(String(l2_pick.get("kind", "")), String(l2_pick.get("tier", "")), String(l2_pick.get("feat_id", ""))) | (待填) | | | +| 503 | 378 | "三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" ↩ % [a8.journal().bonus_total("max_hp"), acc_shop, picked2] | a8.journal().bonus_total("max_hp") == expect_acc | (待填) | | | +| 504 | 380 | "关卡完成部分回血封顶(hp %d ≤ 生效上限 %d——血回满语义退役)" ↩ % [a8.stats().hero_hp, max_eff8] | a8.stats().hero_hp <= max_eff8 | (待填) | | | +| 505 | 381 | "兑付后推进受理(下一关开始)" | a8.continue_to_next_level() | (待填) | | | +| 506 | 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 | (待填) | | | +| 507 | 388 | "同一公式两个落点逐位一致(LevelRunner.rest_heal_amount 内置默认)" | int(lr9.rest_heal_amount(30, 70, 100)) == 30 and int(lr9.rest_heal_amount(30, 95, 100)) == 5 | (待填) | | | +| 508 | 392 | "放弃场景:冒险启动(seed 15——生成图确定同上)" | a10.start_adventure(15) | (待填) | | | +| 509 | 393 | "放弃场景:入口停点 pick 进首战(停点前移批 2026-09-23 驱动口径)" | a10.runner().pick_next_node(int(a10.runner().next_candidate_ids()[0])) | (待填) | | | +| 510 | 394 | "战斗中 abandon 拒绝(战斗域单事务在飞)" | not a10.abandon() | (待填) | | | +| 511 | 401 | "非战斗期(选路态)abandon 受理" | a10.runner().current_battle() == null and a10.abandon() | (待填) | | | +| 512 | 405 | "剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 wide_scout_band)" | a11.start_adventure(11, {"treasure_unlocked": {"wide_scout_band": true}}) | (待填) | | | +| 513 | 414 | "池耗即枯竭(attack/ap 令牌耗尽不出候选——%s)" ↩ % str(draw) | not draw_kinds.has("attack") and not draw_kinds.has("ap") | (待填) | | | +| 514 | 415 | "池耗后候选恒 ≥2(稀有档余量——防「三选项全不可选」死角)" | (draw as Array).size() >= 2 | (待填) | | | +| 515 | 424 | "池收缩——40 次重抽零 aux 候选(停点渠道 aux 卡退役,迭代 11 第①批 T7)" | not treasure_seen | (待填) | | | +| 516 | 433 | "满血关卡完成回血 0 合法(封顶口径——heal_gained=0 非负溢出)" | int(cap["payload"].get("heal_gained", -1)) == 0 | (待填) | | | +| 517 | 436 | "节点共享池场景:冒险启动(seed 13)" | a13.start_adventure(13, {"treasure_unlocked": {"wide_scout_band": true}}) | (待填) | | | +| 518 | 447 | "奖励节点共享池:令牌耗尽同样不出候选(同库同池——%s)" ↩ % str(ncands13) | not has_attack13 | (待填) | | | +| 519 | 451 | "奖励节点 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 | 452 | "节点停点态兑付后清零(恰一次语义同构)" | (a13.stop().node_curio_pending_state().get("candidates", []) as Array).is_empty() | (待填) | | | +| 521 | 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 层") | (待填) | | | +| 522 | 465 | "feat_table 已加载(30 件=基础 27+迁入 2+卡② 完美保险 1——迭代 31 卡② 换代)" | _db.feats != null and (_db.feats.rows as Array).size() == 30 | (待填) | | | +| 523 | 466 | "feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)" | _db.feats.validate().is_empty() | (待填) | | | +| 524 | 468 | "专长带场景:冒险启动(seed 71)" | a_r0.start_adventure(71) | (待填) | | | +| 525 | 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 | (待填) | | | +| 526 | 478 | "旧 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 | 479 | "迁出收尾: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 | 486 | "层级纪律:牌库 kind 全集=CURIO_KINDS+relic(秘宝 item_id 零入池)〔专长批 V18〕" | kinds_ok | (待填) | | | +| 529 | 492 | "棋盘事实不可变:新 30 行零棋盘改写类订阅(rewrite_board 已除名消费面)〔专长批 V19〕" | rewrite_rows == 0 | (待填) | | | +| 530 | 499 | "对外命名纪律:30 行 display_name/desc 零「雷/旗」字面(封印/陷阱风味系)〔专长批 V21 初筛〕" | bad_words == 0 | (待填) | | | +| 531 | 500 | "数值纪律:陷阱爆发倍率>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 | 518 | "新 schema 构造表 validate 零违例(四级 rarity/cap 字段/词表全过——流派标签已退役零 archetype)" | rt.validate().is_empty() | (待填) | | | +| 533 | 522 | "专长带场景:冒险启动(seed 72——注入 5 行临时表)" | a_r.start_adventure(72) | (待填) | | | +| 534 | 527 | "专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 102+5=107" | deck_relic == 5 and a_r.deck().deck.size() == 107 | (待填) | | | +| 535 | 538 | "关卡完成渠道只出专长(60 次重抽 ≥2 种不同 relic——身份键 feat_id 同档可多件)" | feat_seen.size() >= 2 | (待填) | | | +| 536 | 539 | "节点渠道零专长(60 次重抽零 relic——专长在非 Boss 渠道只以专长凭证形态出现)" | not node_relic_seen | (待填) | | | +| 537 | 547 | "A5 专长凭证兑付受理(节点渠道——拿到即开)" | a_t.stop().apply_node_curio("feat_ticket", "excellent") | (待填) | | | +| 538 | 553 | "A5 拿到即开——停点候选已替换为专长三选一(全 relic)" | tkt_all_relic | (待填) | | | +| 539 | 554 | "A5 零摘牌——专长凭证兑付不动令牌库(不进持有集)" | a_t.deck().pool_stock("attack") == int(stock_before["attack"]) and a_t.deck().pool_stock("gp") == int(stock_before["gp"]) | (待填) | | | +| 540 | 555 | "A5 无专长凭证残留持有集" | not (a_t.feats_held() as Array).has("feat_ticket") | (待填) | | | +| 541 | 558 | "A5 专长面兑付受理(专长凭证开出候选可正常入持有集)" | (a_t.feats_held() as Array).size() == feats_n + 1 | (待填) | | | +| 542 | 563 | "A6 保底触发(pity=5 → 第 6 次展示必出——概率无关)" | a_t.deck().feat_ticket_consider(rg_t) | (待填) | | | +| 543 | 564 | "A6 触发后清零" | a_t.deck().ticket_pity == 0 | (待填) | | | +| 544 | 567 | "A6 限购满不再触发(保底冻结)" | not a_t.deck().feat_ticket_consider(rg_t) | (待填) | | | +| 545 | 575 | "A6 概率面存在(chance=20% 200 展示内必出现——零触发=概率面失守)" | offered | (待填) | | | +| 546 | 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 | (待填) | | | +| 547 | 601 | "双管镐:第 %d 次付费挖格返 1 AP(每第 2 次——mod_eq 预评估口径)" ↩ % (i + 1) | refund | (待填) | | | +| 548 | 603 | "双管镐反例:第 %d 次不返(未满 2 次)" ↩ % (i + 1) | not refund | (待填) | | | +| 549 | 607 | "地质学家:首开新类型集 1 点〔专长批 V3〕" | _hits_kind(geo_hits, "counter_add").size() == 1 | (待填) | | | +| 550 | 611 | "地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕" | _hits_kind(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.fx_host().counters_ctx, "geo_types") == 0 | (待填) | | | +| 551 | 613 | "地质学家反例:同类型重复首开不叠点〔专长批 V3〕" | _hits_kind(geo_hits3, "counter_add").is_empty() | (待填) | | | +| 552 | 617 | "安全帽:开安全格攒层 ×2〔专长批 V3〕" | EffectEngine.counter(orch.fx_host().counters_ctx, "helm_layers") == 2 | (待填) | | | +| 553 | 623 | "安全帽:踩雷耗层减伤=层值 2(一次清零随施加面 set 行)〔专长批 V3〕" | hh_reduce == 2 | (待填) | | | +| 554 | 627 | "跳格采掘:空白 0 格攒 1 点〔专长批 V3〕" | _hits_kind(sk_zero, "counter_add").size() == 1 | (待填) | | | +| 555 | 631 | "跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕" | _hits_kind(sk_full, "reveal_tiles").size() == 1 | (待填) | | | +| 556 | 632 | "跳格采掘反例:普通开格不计点〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty() | (待填) | | | +| 557 | 639 | "爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1 | (待填) | | | +| 558 | 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() | (待填) | | | +| 559 | 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" | (待填) | | | +| 560 | 658 | "封印匠:标中每第 3 次→中立移除恰一次(频率闸死)〔专长批 V3〕" | sm_remove == 1 | (待填) | | | +| 561 | 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" | (待填) | | | +| 562 | 664 | "示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "preview_area").size() == 1 | (待填) | | | +| 563 | 673 | "疼痛记忆:攻击附加层值伤害=层值 2·用后清零(set 行随施加面)〔专长批 V3〕" | pm_bonus == 2 | (待填) | | | +| 564 | 680 | "保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty() | (待填) | | | +| 565 | 684 | "保险丝:首点不出盾(2 点才出——预评估口径)〔专长批 V3〕" | fw_shield == 0 | (待填) | | | +| 566 | 686 | "见血开窍:踩雷揭示 1 格〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1 | (待填) | | | +| 567 | 693 | "陷阱本能:踩雷本轮攻击 +2(attack_bonus 通道——面板加算)〔专长批 V3〕" | ti_val == 2 | (待填) | | | +| 568 | 698 | "连环陷阱:同轮第 2 颗起每颗额外 +1 层(疼痛记忆同账)〔专长批 V3〕" | EffectEngine.counter(orch.fx_host().counters_ctx, "pain_layers") == ct_before + 2 | (待填) | | | +| 569 | 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 | (待填) | | | +| 570 | 705 | "血蜜:踩雷回复 N 生命〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "heal").size() == 1 | (待填) | | | +| 571 | 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 | (待填) | | | +| 572 | 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() | (待填) | | | +| 573 | 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() | (待填) | | | +| 574 | 725 | "弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕" | _hits_kind(me_hits, "counter_add").size() == 1 | (待填) | | | +| 575 | 729 | "弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕" | me_atk == 4 | (待填) | | | +| 576 | 730 | "弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty() | (待填) | | | +| 577 | 733 | "一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕" | _hits_kind(ld_hits, "grant_ap").size() == 1 | (待填) | | | +| 578 | 734 | "一滴不剩反例:上轮有余不发〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty() | (待填) | | | +| 579 | 740 | "收盘:本轮开格≥8→+1 GP〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1 | (待填) | | | +| 580 | 743 | "斩首学者:击杀揭示 1 格〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1 | (待填) | | | +| 581 | 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() | (待填) | | | +| 582 | 750 | "战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1 | (待填) | | | +| 583 | 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" | (待填) | | | +| 584 | 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() | (待填) | | | +| 585 | 762 | "封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕" | _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "open_ticket").size() == 1 | (待填) | | | +| 586 | 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() | (待填) | | | +| 587 | 774 | "特性段空态隐藏(视图空态纪律同特性段)" | not panel._feat_section.visible | (待填) | | | +| 588 | 776 | "特性段持有呈现(逐件行常驻可读——「局内奖励选了什么看不到」缺口承载)" | panel._feat_section.visible and panel._feat_list.get_child_count() == 2 | (待填) | | | +| 589 | 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 · 去向:待分类回填) @@ -1423,7 +1423,7 @@ | 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 | (待填) | | | +| 861 | 1325 | "共享池账本 -1(跨渠道单点——taken %d → %d)" ↩ % [taken0, int(a8.deck().taken.get(kind, 0))] | int(a8.deck().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 | (待填) | | | @@ -1479,7 +1479,7 @@ | 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") | (待填) | | | -## 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 | |---|---|---|---|---|---|---| @@ -1514,9 +1514,9 @@ | 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 | (待填) | | | +| 927 | 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() | (待填) | | | +| 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().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() | (待填) | | | @@ -1524,116 +1524,116 @@ | 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 | (待填) | | | +| 937 | 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) | (待填) | | | +| 938 | 354 | "生命淬炼 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 | 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 | (待填) | | | +| 940 | 358 | "命中线在账未注入(契约 6——override 无 hit 字段〔天赋树批 V13〕)" | not mp.hero_override().has("hit") | (待填) | | | +| 941 | 361 | "注入参数冒险启动(start_adventure params 通道)" | adr.start_adventure(920251, {"hero_override": mp.hero_override()}) | (待填) | | | +| 942 | 365 | "首战 hero_atk = 表值 + 永久强化 flat(begin_battle 同表达式——下一局能用到强化)" | int(r.effective_atk()) == int(_db.hero.atk) + int(ov.get("atk", 0)) | (待填) | | | +| 943 | 367 | "首战 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 | 372 | "白板冒险启动(无 params 实参——B1 空参数路径)" | adr_w.start_adventure(920251) | (待填) | | | +| 945 | 375 | "零强化参数冒险启动(新档 hero_override 全零)" | adr_z.start_adventure(920251, {"hero_override": load("res://core/meta_progression.gd").new(_db, sv_white).hero_override()}) | (待填) | | | +| 946 | 379 | "白板成立:空参数 = 零强化参数 = 纯表值(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 | 383 | "ac 轴:白板恒表值、未换强化时注入零位移" | int(rw.effective_ac()) == int(_db.hero.armor_class) and int(r.effective_ac()) == int(_db.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 | (待填) | | | +| 948 | 393 | "hero.tres 无 attack_type/armor_type 字段(契约 2——静态表两轴删除)〔锻造工坊批 V5〕" | not ("attack_type" in _db.hero) and not ("armor_type" in _db.hero) | (待填) | | | +| 949 | 396 | "锻造配置默认值 attack_type=normal / armor_type=light(常驻持有)〔锻造工坊批 V5〕" | _db.forge_default_attack_type() == "normal" and _db.forge_default_armor_type() == "light" | (待填) | | | +| 950 | 400 | "spawn 受理(三参形态——两轴缺省回退面)" | uc.spawn(1, 0, int(_db.hero.max_hp)) | (待填) | | | +| 951 | 401 | "主角单位快照两轴 = 锻造配置默认值 normal/light(零注入回退——行为连续)〔锻造工坊批 V5〕" | String(uc.hero_unit().get("attack_type", "")) == "normal" and String(uc.hero_unit().get("armor_type", "")) == "light" | (待填) | | | +| 952 | 411 | "长剑锻造首购:扣 20 钥石、等级 0→1(Q1——第一级=获得装备=首个 +1)〔锻造工坊批 V2〕" | mp2.forge_buy_level("sword") and sv2.forge_route_level("sword") == 1 and sv2.keystone_balance() == 80 | (待填) | | | +| 953 | 415 | "购买持久(reload 后等级与余额不变——即时写盘)〔锻造工坊批 V2〕" | sv2r.forge_route_level("sword") == 1 and sv2r.keystone_balance() == 80 | (待填) | | | +| 954 | 418 | "长剑二级购:扣 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 | 425 | "余额不足购买拒绝且零副作用(0 余额——三域零变化)〔锻造工坊批 V2〕" | not mp2d.forge_buy_level("sword") and sv2d.keystone_balance() == 0 and sv2d.forge_routes_snapshot() == fr_dry | (待填) | | | +| 956 | 430 | "已满级拒购/未解锁拒购(cap 闸+解锁闸——零副作用)〔锻造工坊批 V2〕" | not mp2.forge_buy_level("sword") and not mp2d.forge_buy_level("spear") | (待填) | | | +| 957 | 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") | (待填) | | | +| 958 | 436 | "派生解锁后长枪可购(首购 20——余额 50−20)〔锻造工坊批 V3〕" | mp2.forge_buy_level("spear") and sv2.forge_route_level("spear") == 1 and sv2.keystone_balance() == 30 | (待填) | | | +| 959 | 443 | "通道二:魔法护甲一次性解锁价 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 | 448 | "直购后练满不重复收(已解锁=拒绝零收费——两通道互不影响)〔锻造工坊批 V3〕" | mp3.forge_route_unlocked("ward") and not mp3.forge_buy_unlock("ward") and sv3.keystone_balance() == 300 | (待填) | | | +| 961 | 453 | "不设串联门槛(法杖直购开——长枪零状态不阻)〔锻造工坊批 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 | 458 | "两币分工:购买只扣钥石(余额差=当级价 20——GP 零触碰、无材料计数)〔锻造工坊批 V4〕" | mp3.forge_buy_level("ward") and sv3.keystone_balance() == ks4 - 20 and sv3.forge_route_level("ward") == 1 | (待填) | | | +| 963 | 463 | "选中切换免费(余额零动——写存档域即盘)〔锻造工坊批 V6〕" | mp3.forge_select_armor("ward") and sv3.forge_armor() == "ward" and sv3.keystone_balance() == ks6 | (待填) | | | +| 964 | 466 | "切换后旧路线练度保留(轻甲 5 级不丢——回家待命只不生效)〔锻造工坊批 V6〕" | sv3.forge_route_level("light") == 5 | (待填) | | | +| 965 | 469 | "未解锁路线拒选零副作用〔锻造工坊批 V6〕" | not mp2d.forge_select_weapon("spear") and sv2d.forge_weapon() == "sword" | (待填) | | | +| 966 | 472 | "类型轴当前生效值随选中走(ward;未解锁档回退默认 normal/light)〔锻造工坊批 V5/V6〕" | mp3.hero_armor_type() == "ward" and mp2d.hero_attack_type() == "normal" and mp2d.hero_armor_type() == "light" | (待填) | | | +| 967 | 481 | "build_adventure_params 注入默认值 normal/light(树钥匙未点亮——新档初始持有态)〔锻造工坊批 V5〕" | String(bp_params.get("attack_type", "")) == "normal" and String(bp_params.get("armor_type", "")) == "light" | (待填) | | | +| 968 | 485 | "长枪直购解锁+首购+选中(注入面夹具——200+20 钥石)" | mp4.forge_buy_unlock("spear") and mp4.forge_buy_level("spear") and mp4.forge_select_weapon("spear") | (待填) | | | +| 969 | 488 | "build_adventure_params 读存档选中覆盖(选中长枪→pierce)〔锻造工坊批 V5〕" | String(bp_sel.get("attack_type", "")) == "pierce" and String(bp_sel.get("armor_type", "")) == "light" | (待填) | | | +| 970 | 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") | (待填) | | | +| 971 | 498 | "换当前武器→攻击加值随切换(长剑 3 级待命不生效——只认选中)〔锻造工坊批 V7〕" | int(mp4.hero_override().get("atk", -1)) == 1 | (待填) | | | +| 972 | 500 | "切回长剑→加值=3 级量(旧路线练度保留且生效)〔锻造工坊批 V7〕" | mp4.forge_select_weapon("sword") and int(mp4.hero_override().get("atk", -1)) == 3 | (待填) | | | +| 973 | 506 | "重甲直购解锁+首购+选中(三层 AC 夹具)" | mp5.forge_buy_unlock("heavy") and mp5.forge_buy_level("heavy") and mp5.forge_select_armor("heavy") | (待填) | | | +| 974 | 510 | "注入参数冒险启动(两轴+override 同面)" | adr5.start_adventure(77, p5) | (待填) | | | +| 975 | 512 | "入战 AC 三层:甲型基础 12(重甲)+路线加值 1+局内 0〔锻造工坊批 V7〕" | int(r5.effective_ac()) == 12 + 1 | (待填) | | | +| 976 | 514 | "参数面快照入 runner(attack_type=normal/armor_type=heavy)〔锻造工坊批 V6〕" | adr5.stats().attack_type == "normal" and adr5.stats().armor_type == "heavy" | (待填) | | | +| 977 | 517 | "冒险开始后锁死:改存档选中不影响参数面快照与生效值(契约 8)〔锻造工坊批 V6〕" | mp5.forge_select_armor("light") and adr5.stats().armor_type == "heavy" and int(r5.effective_ac()) == 12 + 1 | (待填) | | | +| 978 | 526 | "enc 两轴直达 spawn 英雄快照(hero_attack 行携带 pierce 键——注入链末端)〔锻造工坊批 V5〕" | pierce_rows >= 1 | (待填) | | | +| 979 | 530 | "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 | 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 | (待填) | | | +| 981 | 549 | "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 | 551 | "MetaTreeTable.validate 零违例(脉/类型/币词表出域即拒/Fork 三组每组恰一且持 a/b 双效果键/彩蛋 ≤3/effects 键封闭/零 archetype)" | _db.meta_tree.validate().is_empty() | (待填) | | | +| 983 | 559 | "easter_ley effects 键集 ⊆ {max_hp, hit}(攻/护分量已摘除——契约 9)〔天赋树批 V8 落点〕" | ley_ok | (待填) | | | +| 984 | 567 | "schema 反例:vein 出域被拒" | not t2.validate().is_empty() | (待填) | | | +| 985 | 572 | "schema 反例:fork_group 出域(fg_mastery 已退役组)被拒" | not t2.validate().is_empty() | (待填) | | | +| 986 | 577 | "schema 反例:effects.archetype 加载致命(🔴 层级纪律护栏面)〔天赋树批 V4〕" | not t2.validate().is_empty() | (待填) | | | +| 987 | 582 | "schema 反例:requires 悬空被拒" | not t2.validate().is_empty() | (待填) | | | +| 988 | 587 | "树购买受理(脊柱生命 L1——树全钥石单币)" | mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 1 | (待填) | | | +| 989 | 589 | "行门槛:第 2 级不可购(总等级 1 < 4——一线一节点主语,买第 N 级需全树总投入 ≥4×(N−1))〔天赋树批 V2〕" | not mp.tree_can_buy("sp_hp") | (待填) | | | +| 990 | 591 | "命中 L1/锻造钥匙(free 无前置=初始零购零币可点亮)/同调位 1(根节点无前置)受理(总等级 1→4)" | mp.tree_buy("sp_hit") and mp.tree_buy("forge_workshop") and mp.tree_buy("att_socket1") | (待填) | | | +| 991 | 593 | "锻造工坊钥匙零币(free)" | int(_db.tree_node("forge_workshop").get("price", -1)) == 0 | (待填) | | | +| 992 | 594 | "行门槛恰达开放(总等级 4 ≥ 4×1)" | mp.tree_can_buy("sp_hp") | (待填) | | | +| 993 | 595 | "第 2 级购买受理(门槛恰达当刻开放)" | mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 2 | (待填) | | | +| 994 | 599 | "Fork 购买=解锁+默认 A 态(一节点双态——单行承载)" | mp.tree_buy_fork("fork_posture") and sv.tree_level("fork_posture") == 1 and sv.fork_side("fg_posture") == "a" | (待填) | | | +| 995 | 601 | "已解锁 Fork 重复购买拒绝(cap 1)" | not mp.tree_can_buy("fork_posture") | (待填) | | | +| 996 | 602 | "同态重复切换零操作(false)" | not mp.tree_toggle_fork("fg_posture", "a") | (待填) | | | +| 997 | 603 | "未解锁 Fork 切换拒绝(解锁唯一入口=购买面)" | not mp.tree_toggle_fork("fg_sense", "b") | (待填) | | | +| 998 | 605 | "零币切态 b(购买解锁后 A/B 可切)" | mp.tree_toggle_fork("fg_posture", "b") and sv.fork_side("fg_posture") == "b" | (待填) | | | +| 999 | 607 | "切态零消费(钥石余额差分不动)" | int(sv.keystone_balance()) == bal_pre_toggle | (待填) | | | +| 1000 | 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") | (待填) | | | +| 1001 | 610 | "切回 A 态键集随换(六键词表在册——V11 校验面归单元段)" | mp.tree_toggle_fork("fg_posture", "a") and (mp.tree_rule_keys() as Array).has("posture_first_strike") | (待填) | | | +| 1002 | 613 | "彩蛋不经购买(手动购亮=红——觉醒唯一入口=特化判定)" | not mp.tree_can_buy("easter_ley") | (待填) | | | +| 1003 | 614 | "觉醒前置未满足不亮(两线未拉满)" | sv.tree_level("easter_ley") == 0 | (待填) | | | +| 1004 | 619 | "两线拉满+同调位全解锁 → 地脉共鸣自动点亮(零币特化判定)" | sv.tree_level("easter_ley") == 1 | (待填) | | | +| 1005 | 622 | "觉醒零消费(钥石余额不动)" | int(sv.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 | (待填) | | | +| 1006 | 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 | (待填) | | | +| 1007 | 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() | (待填) | | | +| 1008 | 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() | (待填) | | | +| 1009 | 655 | "锋:踩雷减半命中+限次记账(counter 0<2 闸开——damage_mult+counter_add 双行同预评估态)" | got_mult and got_add | (待填) | | | +| 1010 | 658 | "锋:限次耗尽全额(counter 2 不<2 闸关——每关限次 provisional 2)〔无状态化 V5〕" | (fz.dispatch(ctx_fz, "mine_hit", {}, Callable()) as Array).is_empty() | (待填) | | | +| 1011 | 664 | "锋:限次跨战持久(level 桶共享引用——替代旧副本搬运链)〔无状态化 V4〕" | (fz2.dispatch(ctx_fz2, "mine_hit", {}, Callable()) as Array).is_empty() | (待填) | | | +| 1012 | 686 | "锋:惊巢反制 mine_hit 减半分支受理(wa12 前提——halve_pct=50 效果行落 step_row)〔wa12〕" | nc_hit != "" | (待填) | | | +| 1013 | 688 | "惊巢反制效果行==「惊巢反制——危险格挖掘惩罚减半(6→3)」零「挖雷」(u13 换代)〔wa12〕" | nc_hit == "惊巢反制——危险格挖掘惩罚减半(6→3)" and not nc_hit.contains("挖雷") | (待填) | | | +| 1014 | 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"}) | (待填) | | | +| 1015 | 704 | "守侧 rig:安全格定位(非危险格=标错负样本)" | safe.x >= 0 | (待填) | | | +| 1016 | 705 | "守侧 rig:首轮后 queue 干净(开局 posture queue 已兑现)" | int(orch.action_economy.pending_ap_delta()) == 0 | (待填) | | | +| 1017 | 709 | "守①:当回合标错且封印在场 → 下轮 AP+1(queue 排队——本轮 AP 逐位零变化)" | int(orch.action_economy.pending_ap_delta()) == 1 | (待填) | | | +| 1018 | 714 | "守③④:旧封印重判零触发+同格最多一次(_seal_miss_seen 去重闸=「当回合标错动作」判别器)" | int(orch.action_economy.pending_ap_delta()) == 1 | (待填) | | | +| 1019 | 718 | "守②负样本:标中/被挖封印零触发(标错事件只在 miss 分支发射——发射面过滤承载)" | int(orch.action_economy.pending_ap_delta()) == 1 | (待填) | | | +| 1020 | 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"}) | (待填) | | | +| 1021 | 728 | "守⑤:末轮判出不返(queue 只在 notify_round_start 兑现、战亡即弃——无跨战泄漏)" | int(orch2.action_economy.pending_ap_delta()) == 0 | (待填) | | | +| 1022 | 734 | "散未激活:商品标价原价(无 economy_spend 键)" | int(lr.current_price({"base_price": 100, "price_increment": 0})) == 100 + int(_db.shop_price_level_bonus(0)) | (待填) | | | +| 1023 | 738 | "散激活:商品购买价 ×80%(provisional 折率——floor 偏向玩家、下限 1;刷新价不折=另一计价点零触碰)" | int(lr.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.shop_price_level_bonus(0))) * float(_db.shop.spend_discount)))) and int(lr.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.shop_price_level_bonus(0))) * float(_db.shop.spend_discount)))) | (待填) | | | +| 1024 | 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"}) | (待填) | | | +| 1025 | 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"}) | (待填) | | | +| 1026 | 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 · 去向:待分类回填) @@ -2378,7 +2378,7 @@ | 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 | (待填) | | | +| 1416 | 83 | "伤害行按 target_id 命中正确站桩(闪白落点)〔wd12〕" | s2.fx_probe().flash_count == 1 and s1.fx_probe().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 | (待填) | | |