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] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(=E6=96=AD=E8=A8=80=E5=BD=92?= =?UTF-8?q?=E4=BD=8D=E6=B3=A21):=20adventure=5Ffeats=EF=BC=8Bfeat=5Fvouche?= =?UTF-8?q?rs=EF=BC=8Bcurio=5Frefresh=5Fpaid=20=E6=96=AD=E8=A8=80=E7=BA=A7?= =?UTF-8?q?=E5=86=85=E9=83=A8=E7=9B=B4=E6=9F=A5=E6=94=B9=E9=92=89=E5=85=AC?= =?UTF-8?q?=E5=BC=80=E5=8F=AF=E8=A7=82=E6=B5=8B=E9=9D=A2=E2=80=94=E2=80=94?= =?UTF-8?q?adventure=5Ffeats=2025=20=E8=A1=8C=E6=94=B9=E9=92=89(44=20?= =?UTF-8?q?=E5=91=BD=E4=B8=AD=E7=89=87=E6=AE=B5:29=20=E6=96=AD=E8=A8=80=20?= =?UTF-8?q?=5Fhits=5Fof=E2=86=92=E6=B5=8B=E8=AF=95=E4=BE=A7=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E7=AD=89=E4=BB=B7=E8=BF=87=E6=BB=A4=E9=92=89=E5=85=AC?= =?UTF-8?q?=E5=BC=80=20fx.dispatch=20=E8=BF=94=E5=9B=9E=E5=80=BC=EF=BC=8B5?= =?UTF-8?q?=20=E7=8A=B6=E6=80=81=E9=92=89=E9=9D=A2:=E4=BF=A1=E5=8F=B7?= =?UTF-8?q?=E8=BD=BD=E8=8D=B7/=E5=85=AC=E5=BC=80=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E5=8F=A3/=E4=BB=B6=E5=87=BA=E5=8F=A3)+2=20=E4=BE=8B=E5=A4=96(h?= =?UTF-8?q?ero=5Finfo=5Fpanel.=5Ffeat=5Fsection=20=E6=97=A0=E5=85=AC?= =?UTF-8?q?=E5=BC=80=E8=AF=BB=E6=95=B0=E5=8F=A3);feat=5Fvouchers=2010=20?= =?UTF-8?q?=E6=94=B9=E9=92=89(pick=5Fcurio/consume=5Fpool=5Fpurchase=20?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E8=B7=AF=E5=BE=84=C3=974+step=5Frow=20insura?= =?UTF-8?q?nce=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〕")