From cd133932f9627704f31387d7ebf3c976eab78e3b Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Tue, 3 Nov 2026 06:31:05 +0800 Subject: [PATCH] =?UTF-8?q?iter50-=E5=8D=A12=EF=BC=9A=E5=8D=A1B(=E6=96=AD?= =?UTF-8?q?=E8=A8=80=E8=A1=A5=E9=BD=90):=20=E5=9B=9B=E5=9F=9F13=E6=96=B0?= =?UTF-8?q?=E6=96=AD=E8=A8=80+2=E5=A4=84=E6=97=A2=E6=9C=89=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E2=80=94=E2=80=94=E2=91=A0=E5=A2=9E=E7=9B=8A=E7=BB=93?= =?UTF-8?q?=E7=AE=97=E9=93=BEV1-V5=20=E2=91=A1=E8=AE=BE=E7=BD=AE=E5=AD=98?= =?UTF-8?q?=E5=82=A8V6-V8=20=E2=91=A4Boss=E9=97=A8=E6=8E=A7V9/V10=20?= =?UTF-8?q?=E2=91=A5=E9=94=BB=E9=80=A0V11-V16;=20=E6=96=B0=E4=BB=B6setting?= =?UTF-8?q?s=5Fstore=5Ftest=E4=B8=8Eboss=5Fgate=5Ftest;=20manifest+2;=2073?= =?UTF-8?q?=E5=A5=97=E4=BB=B6=E5=85=A8=E7=BB=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/tests/run_all.gd | 2 + .../unit/battle_settlement/boss_gate_test.gd | 48 +++++++++++ .../battle_settlement/boss_gate_test.gd.uid | 1 + .../unit/contracts/system_contracts_test.gd | 82 +++++++++++++++++++ .../unit/core/meta_progression_forge_test.gd | 82 ++++++++++++++++++- .../unit/core/node_map_generator_test.gd | 36 ++++++++ .../unit/data_config/forge_table_test.gd | 15 ++++ .../unit/presentation/settings_store_test.gd | 63 ++++++++++++++ .../presentation/settings_store_test.gd.uid | 1 + 9 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 game/tests/unit/battle_settlement/boss_gate_test.gd create mode 100644 game/tests/unit/battle_settlement/boss_gate_test.gd.uid create mode 100644 game/tests/unit/presentation/settings_store_test.gd create mode 100644 game/tests/unit/presentation/settings_store_test.gd.uid diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index bd198255..7b0744ac 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -186,6 +186,8 @@ func _suites() -> Array: {"path": "res://tests/unit/core/curio_refresh_paid_test.gd", "expect": 0}, # 三选一刷新免费双源+GP 付费档(对齐商店 MC-1:价曲线/优先序/停点清零/钱包单口) {"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值) {"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # AC⑤/T9——排空零分叉/零丢行/延迟双态 + {"path": "res://tests/unit/presentation/settings_store_test.gd", "expect": 0}, # 减动效开关存储契约(持久化回读/缺省值/引擎动效档应用)〔断言补齐批〕 + {"path": "res://tests/unit/battle_settlement/boss_gate_test.gd", "expect": 0}, # Boss 节点开战必选 Boss 行集(行类路由+端到端)〔断言补齐批〕 {"path": "res://tests/unit/presentation/standee_fx_sweep_test.gd", "expect": 0}, # 呈现域缺陷扫掠(d2 蓄力档/d4 庆祝句柄/d5+d8 互斥/d12 站桩按 id 解析) {"path": "res://tests/unit/presentation/tune_sim_batch_parity_test.gd", "expect": 0}, # 测速循环批双实现对拍(帧索引/运行态/进度串/stats/报告五面——信号步进 vs await 基线) {"path": "res://tests/unit/presentation/standee_present_values_test.gd", "expect": 0}, # 战斗一屏样张批 V5–V12(血条危险红/名字行素底不压地台与提档 FS_HINT/徽标/护甲 PNG 绑定/素材入包/地台色合并退役) diff --git a/game/tests/unit/battle_settlement/boss_gate_test.gd b/game/tests/unit/battle_settlement/boss_gate_test.gd new file mode 100644 index 00000000..bf32d8c4 --- /dev/null +++ b/game/tests/unit/battle_settlement/boss_gate_test.gd @@ -0,0 +1,48 @@ +# tests/unit/battle_settlement/boss_gate_test.gd — Boss 节点开战必选 Boss 行集(端到端) +# 断言面 = 行类路由(resolver:boss 行集全行 boss / battle 行集零 boss 行)+端到端 +# (begin_battle(node_type="boss") → 实例化敌人全部出自 boss 行集)。固定种子经 RngService 流口。 +# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/boss_gate_test.gd +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") + + +func _init() -> void: + _test_boss_node_spawns_boss_rowset() + quit(Kit.finish("boss_gate_test")) + + +func _test_boss_node_spawns_boss_rowset() -> void: + var db = load("res://core/config_db.gd").new() + var load_ok: bool = db.load_tables("res://data/tables", true) and db.diagnostics_count("error") == 0 + Kit.check("前置:表加载零 error", load_ok) + if not load_ok: + return + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(20261103) # 固定种子经 RngService 流口(RNG 单点) + # 行类路由面:resolver 预解析(boss 行集全行 row_kind=="boss";对照 battle 行集零 boss 行) + var boss_ids := {} + var battle_ids := {} + var boss_kind_ok := true + for row in db.combat.enemy.resolve_rowset(1, 4, "boss"): + boss_ids[String(row["enemy_id"])] = true + if String(row["row_kind"]) != "boss": + boss_kind_ok = false + var battle_kind_ok := true + for row in db.combat.enemy.resolve_rowset(1, 4, "battle"): + battle_ids[String(row["enemy_id"])] = true + if String(row["row_kind"]) == "boss": + battle_kind_ok = false + Kit.check("行类路由:boss 行集非空全行 boss、battle 行集非空零 boss 行〔断言补齐批 V10〕", + boss_kind_ok and battle_kind_ok and boss_ids.size() > 0 and battle_ids.size() > 0) + # 端到端:begin_battle(node_type="boss") → 实例化敌人行键全部出自 boss 行集 + var oc = load("res://core/battle_orchestrator.gd").new(db, rng) + var ok_boss: bool = oc.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(db.combat.hero.max_hp), "node_type": "boss"}) + Kit.check("前置:boss 节点开局受理(L1 D4 Boss 行集)", ok_boss) + if not ok_boss: + return + var all_from_boss := true + for u in oc._enemy_system.all_enemies(): + if not boss_ids.has(String(u["enemy_id"])): + all_from_boss = false + Kit.check("Boss 节点开战必选 Boss 行集(实例化敌人全部出自 boss 行集——跨宿主真实入口链)〔断言补齐批 V10〕", all_from_boss) diff --git a/game/tests/unit/battle_settlement/boss_gate_test.gd.uid b/game/tests/unit/battle_settlement/boss_gate_test.gd.uid new file mode 100644 index 00000000..548c836c --- /dev/null +++ b/game/tests/unit/battle_settlement/boss_gate_test.gd.uid @@ -0,0 +1 @@ +uid://dkmph2dfqjqer diff --git a/game/tests/unit/contracts/system_contracts_test.gd b/game/tests/unit/contracts/system_contracts_test.gd index 3cc47790..4c212e80 100644 --- a/game/tests/unit/contracts/system_contracts_test.gd +++ b/game/tests/unit/contracts/system_contracts_test.gd @@ -29,6 +29,10 @@ func _init() -> void: _test_status_grant_all_per_unit(db) _test_status_death_clears(db) _test_status_merge_per_unit(db) + _test_status_decay_and_clear(db) + _test_status_stack_full_reject(db) + _test_status_independent_branch(db) + _test_status_snapshot_first_grant(db) quit(Kit.finish("system_contracts_test")) @@ -209,3 +213,81 @@ func _test_status_merge_per_unit(db) -> void: granted_n += 1 Kit.check("同体合并照旧:叠加行满层拒绝(恰授 max_stack=%d 层)〔个体化批 V4〕" % max_stack, granted_n == max_stack) Kit.check("同体合并照旧:满层时异体仍受理(拒绝只在同体内原子)〔个体化批 V4〕", effects.apply_effect("enrage", "e3", src) == true) + + +# ---- B4 结算链契约(断言补齐批——①增益状态结算链):衰减逐值/战斗结束全清/满层拒绝/独立并存/首授快照 ---- + +func _test_status_decay_and_clear(db) -> void: + var effects = StatusContainerScript.new(db) + effects.notify_battle_start() + var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} + var wrow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("weaken") + var w_dur: int = int(wrow.duration_settlements) + Kit.check("V1 前置:weaken 时长 ≥2(两拍衰减观测面)", w_dur >= 2) + Kit.check("V1 前置:e1 受理", effects.apply_effect("weaken", "e1", src)) + Kit.check("衰减逐值:首次 decay 返回移除数 0(未归零)〔断言补齐批 V1〕", effects.decay_round() == 0) + Kit.check("衰减逐值:每结算恰 −1(remaining=%d−1)〔断言补齐批 V1〕" % (w_dur - 1), + effects.query_active("e1").size() == 1 and int(effects.query_active("e1")[0].remaining_settlements) == w_dur - 1) + Kit.check("到期移除:第二次 decay 返回移除数 1〔断言补齐批 V2〕", effects.decay_round() == 1) + Kit.check("到期移除:归零实例当轮销毁(投影零行)〔断言补齐批 V2〕", effects.query_active("e1").is_empty()) + effects.apply_effect("weaken", "e2", src) + effects.apply_effect("attack_up", StatusContainerScript.OWNER_HERO, src) + Kit.check("V2 前置:两桶各持实例", effects.query_active_side("enemy").size() == 1 and effects.query_active(StatusContainerScript.OWNER_HERO).size() == 1) + Kit.check("战斗结束全清:notify_battle_end 返回 true 且全实例清空(两桶零行)〔断言补齐批 V2〕", + effects.notify_battle_end() and effects.query_active_side("enemy").is_empty() and effects.query_active(StatusContainerScript.OWNER_HERO).is_empty()) + + +func _test_status_stack_full_reject(db) -> void: + var effects = StatusContainerScript.new(db) + effects.notify_battle_start() + var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} + var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("enrage") + var max_stack: int = int(erow.max_stack) + for i in range(max_stack): + effects.apply_effect("enrage", "e1", src) + Kit.check("V3 前置:满层(恰 %d 层)" % max_stack, + effects.query_active("e1").size() == 1 and int(effects.query_active("e1")[0].layers) == max_stack) + var before_layers: int = int(effects.query_active("e1")[0].layers) + var before_rem: int = int(effects.query_active("e1")[0].remaining_settlements) + Kit.check("满层拒绝:再授返 false〔断言补齐批 V3〕", effects.apply_effect("enrage", "e1", src) == false) + Kit.check("满层拒绝:层数与剩余零写入(容器逐字不变——原子拒绝)〔断言补齐批 V3〕", + int(effects.query_active("e1")[0].layers) == before_layers and int(effects.query_active("e1")[0].remaining_settlements) == before_rem) + + +func _test_status_independent_branch(db) -> void: + var effects = StatusContainerScript.new(db) + effects.notify_battle_start() + var probe := { + "effect_id": "probe_independent_v4", + "display_name": "并存试验", + "target_side": "enemy", + "magnitude": 7, + "unit": "flat", + "duration_settlements": 2, + "stack_policy": "independent", + "max_stack": 1, + } + db.combat.status_effects.rows.append(probe) # 直改已加载表(运行时注入试验行——并存分支零内容行,夹具面) + var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0} + Kit.check("independent 并存:第一次授予受理〔断言补齐批 V4〕", effects.apply_effect("probe_independent_v4", "e1", src)) + Kit.check("independent 并存:第二次授予受理(并存——无合并无满层语义)〔断言补齐批 V4〕", effects.apply_effect("probe_independent_v4", "e1", src)) + var both: Array = effects.query_active("e1") + Kit.check("independent 并存:两实例各持时长 2 独立倒数〔断言补齐批 V4〕", + both.size() == 2 and int(both[0].remaining_settlements) == 2 and int(both[1].remaining_settlements) == 2) + db.combat.status_effects.rows.pop_back() # 测毕还原(表行零残留) + Kit.check("independent 试验行测毕还原(row_by_id 零命中)", db.combat.status_effects.row_by_id("probe_independent_v4").is_empty()) + + +func _test_status_snapshot_first_grant(db) -> void: + var effects = StatusContainerScript.new(db) + effects.notify_battle_start() + var src_first := {"source_class": "seal_hit", "origin_cell": Vector2i(3, 4), "round_index": 1} + var src_second := {"source_class": "mine_hit", "origin_cell": Vector2i(8, 9), "round_index": 5} + effects.apply_effect("weaken", "e1", src_first) + effects.apply_effect("weaken", "e1", src_second) # 累加:时长加、归因与快照沿首授 + var inst: Array = effects.query_active("e1") + Kit.check("首授快照:累加合并仍单实例〔断言补齐批 V5〕", inst.size() == 1) + Kit.check("首授快照:origin_cell/round_index/source_class 沿首授(合并实例只持首授归因)〔断言补齐批 V5〕", + String(inst[0].source_class) == "seal_hit" and inst[0].origin_cell == Vector2i(3, 4) and int(inst[0].round_index) == 1) + Kit.check("首授快照:幅度快照 = 首授时表值(抗热重载域)〔断言补齐批 V5〕", + int(inst[0].snap_magnitude) == int(db.combat.status_effects.row_by_id("weaken").magnitude)) diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index 0a3f5373..28a8b9ae 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -37,6 +37,10 @@ func _init() -> void: _smoke_forge_axes() _smoke_tree() _smoke_tree_rules() + _test_forge_buy_reject() + _test_blessing_no_forge_leak() + _test_default_axes_pre_key() + _test_armor_switch_dual_axes() quit(Kit.finish("meta_progression_forge_test")) @@ -50,7 +54,8 @@ const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry "user://smoke_meta_tree.json", "user://smoke_meta_career.json", "user://smoke_meta_career_v5.json", "user://smoke_meta_treasure_unlock.json", "user://smoke_meta_treasure_blank.json", "user://smoke_meta_forge.json", "user://smoke_meta_forge2.json", "user://smoke_meta_forge3.json", - "user://smoke_meta_forge4.json", "user://smoke_meta_forge5.json"] + "user://smoke_meta_forge4.json", "user://smoke_meta_forge5.json", + "user://smoke_meta_reject.json", "user://smoke_meta_blessing.json", "user://smoke_meta_axesw.json"] func _meta_smoke_cleanup() -> void: @@ -418,6 +423,12 @@ func _smoke_forge_axes() -> void: Kit.check("长剑二级购:扣 30、等级→2;轻甲独立零扰动(六路线各练各的)〔锻造工坊批 V2〕", mp2.forge_buy_level("sword") and sv2.meta_state.forge_state().levels.get("sword", 0) == 2 and sv2.meta_state.keystone_balance() == 50 and sv2.meta_state.forge_state().levels.get("light", 0) == 0) + # V12:长剑购买链走至 3 级(45 档扣款——余额 50−45=5,几何价第三档);链后续夹具段余额基准 + # 由 add_keystone 补回(注入口径——直写存档域构造状态,下游 V3 段 50−20 语义不变) + Kit.check("长剑三级购:扣 45、等级→3(余额 50−45=5)〔断言补齐批 V12〕", + mp2.forge_buy_level("sword") and sv2.meta_state.forge_state().levels.get("sword", 0) == 3 + and sv2.meta_state.keystone_balance() == 5) + sv2.meta_state._add_keystone(45) # V2③:余额不足拒且零副作用(等级/选中/解锁三面零变化) var sv2d = _meta_new_save(_META_SMOKE_PATHS[14]) var mp2d = load("res://core/meta_progression.gd").new(_db, sv2d) @@ -760,3 +771,72 @@ func _smoke_tree_rules() -> void: Kit.check("聚:战斗掉落乘算 ×1.5(provisional——同种子配对 %d→%d,入账前=结算框显示实得)", gp_hoard == int(round(float(gp_base) * 1.5)) and gp_hoard >= gp_base) _meta_smoke_cleanup() + + +# ---- 锻造契约补齐(断言补齐批——⑥锻造工坊):拒购零副作用/祝福件零泄漏/默认值解耦/甲型双轴 ---- + +func _test_forge_buy_reject() -> void: + var sv = _meta_new_save("user://smoke_meta_reject.json") + var mp = load("res://core/meta_progression.gd").new(_db, sv) + sv.meta_state._add_keystone(10) # 低于首购价 20——余额不足面 + var fs0: Dictionary = sv.meta_state.forge_state().duplicate(true) + var bal0: int = sv.meta_state.keystone_balance() + Kit.check("余额不足拒购:sword false、等级与余额零变化〔断言补齐批 V13〕", + not mp.forge_buy_level("sword") and sv.meta_state.forge_state() == fs0 and sv.meta_state.keystone_balance() == bal0) + sv.meta_state._add_keystone(1000) # 充值后未解锁面不变(spear 直购未走/练满未达) + Kit.check("未解锁拒购:spear false、等级与余额零变化〔断言补齐批 V13〕", + not mp.forge_buy_level("spear") and sv.meta_state.forge_state() == fs0 + and sv.meta_state.keystone_balance() == bal0 + 1000) + + +func _test_blessing_no_forge_leak() -> void: + var sv = _meta_new_save("user://smoke_meta_blessing.json") + var mp = load("res://core/meta_progression.gd").new(_db, sv) + var fs0: Dictionary = sv.meta_state.forge_state().duplicate(true) + # 真实入口=商店购买口(LevelRunner 装配的 shop_session.buy_product——祝福件走 GP 钱包+局内加算区) + var runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(80)) + var lvl_ok: bool = runner.start_level(1, 80) + Kit.check("V14 前置:关卡启动(L1 行集)", lvl_ok) + if not lvl_ok: + return + runner.gp = 100 + Kit.check("V14 前置:攻击祝福受理", runner.shop.buy_product("atk_up")) + Kit.check("V14 前置:护甲祝福受理", runner.shop.buy_product("armor_up")) + Kit.check("祝福件零泄漏:购买后 forge_state 投影逐位不变(levels/选中/解锁三面)〔断言补齐批 V14〕", + sv.meta_state.forge_state() == fs0) + Kit.check("祝福件住局内加算区(atk +1 在加算日记——通道分离实证)", runner.journal().bonus_total("atk") == 1) + + +func _test_default_axes_pre_key() -> void: + var sv = _meta_new_save("user://smoke_meta_axesw.json") + var mp = load("res://core/meta_progression.gd").new(_db, sv) + var ax: Dictionary = mp.hero_type_axes() + Kit.check("树钥匙未点亮:hero_type_axes 返回锻造配置默认 normal/light(默认值常驻解耦)〔断言补齐批 V15〕", + String(ax["attack"]) == "normal" and String(ax["armor"]) == "light") + + +func _test_armor_switch_dual_axes() -> void: + var sv = _meta_new_save("user://smoke_meta_axesw.json") + sv.meta_state._add_keystone(1000) + var mp = load("res://core/meta_progression.gd").new(_db, sv) + Kit.check("V16 前置:重甲直购解锁+首购+选中(200+20 钥石)", + mp.forge_buy_unlock("heavy") and mp.forge_buy_level("heavy") and mp.forge_select("armor", "heavy")) + var adr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(81)) + var p: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv, mp) + var adv_ok: bool = adr.start_adventure(81, p) + Kit.check("V16 前置:参数面冒险启动", adv_ok) + if not adv_ok: + return + Kit.check("甲型双轴:heavy 选中 → AC 轴 12 基础+路线 Lv1=13(light 同级 11 的 +2 档差)〔断言补齐批 V16〕", + int(adr.runner().effective_ac()) == 13 and String(mp.hero_type_axes()["armor"]) == "heavy") + Kit.check("甲型双轴:挨普通攻击修正 −1(克制矩阵 normal→heavy)〔断言补齐批 V16〕", + int(_db.combat.settlement.damage_roll.armor_effectiveness["normal"]["heavy"]) == -1) + Kit.check("V16 前置:切回 light(初始持有已解锁可切)", mp.forge_select("armor", "light")) + var adr2 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(82)) + var p2b: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv, mp) + var adv2_ok: bool = adr2.start_adventure(82, p2b) + Kit.check("V16 前置:切回后新参数面冒险启动(快照冻结契约——新 runner 读新选中)", adv2_ok) + if not adv2_ok: + return + Kit.check("甲型双轴:切回 light 复原(AC 10+0)且 heavy 等级保留(Lv1 不丢)〔断言补齐批 V16〕", + int(adr2.runner().effective_ac()) == 10 and sv.meta_state.forge_state().levels.get("heavy", 0) == 1) diff --git a/game/tests/unit/core/node_map_generator_test.gd b/game/tests/unit/core/node_map_generator_test.gd index 57054f1f..c2701bb1 100644 --- a/game/tests/unit/core/node_map_generator_test.gd +++ b/game/tests/unit/core/node_map_generator_test.gd @@ -29,6 +29,7 @@ func _init() -> void: _test_shop_and_attribution() _test_battle_floor() _test_exit_structure() + _test_boss_exit_per_region() _test_rng_single_point() _test_negative_fixtures() quit(Kit.finish("node_map_generator_test")) @@ -303,6 +304,41 @@ func _terminals_reachable(g: Dictionary, id: int) -> int: return c +# ---- V9 Boss 出场门控(难度曲线与Boss——每区域恰一 Boss 终点)---- + +func _test_boss_exit_per_region() -> void: + # 真实生成多区域图:全部终点 type=="boss"、boss 终点数==exit_count、深度=L−r(r∈[0,exit_count) + # ——每区域恰一 boss、深度互异即分属不同区域) + # 节点 depth 字段=入口起最长路径深(生成器校验轴);区域序号 r 的可观测形态=最深 boss(r=0) + # 与该 boss 的深度差(同层 boss 深度 = d0 − r——区域段 L−r 的 1 步差) + var all_ok := true + for L in [1, 2, 3]: + for s in [42, 43, 44]: + var g := _graph(L, s) + var by_id := {} + for n in g["nodes"]: + by_id[int(n["id"])] = n + var exits := int(_params(L)["exit_count"]) + var d0 := 0 + for tid in g["terminals"]: + d0 = maxi(d0, int(by_id[int(tid)]["depth"])) + var per_region := {} + for tid in g["terminals"]: + var tn: Dictionary = by_id[int(tid)] + if String(tn["type"]) != "boss": + all_ok = false + var r := d0 - int(tn["depth"]) + if r < 0 or r >= exits: + all_ok = false + per_region[r] = int(per_region.get(r, 0)) + 1 + if per_region.size() != exits: + all_ok = false + for r in per_region: + if int(per_region[r]) != 1: + all_ok = false + Kit.check("每区域恰一 Boss 终点(type=boss × exit_count 个、深度=d0−r 互异分域——区域段 L−r 的可观测形态)〔断言补齐批 V9〕", all_ok) + + # ---- V10 RNG 单点(纯流面 counter 不变)---- func _test_rng_single_point() -> void: diff --git a/game/tests/unit/data_config/forge_table_test.gd b/game/tests/unit/data_config/forge_table_test.gd index 370bc4b5..5f8e94e0 100644 --- a/game/tests/unit/data_config/forge_table_test.gd +++ b/game/tests/unit/data_config/forge_table_test.gd @@ -97,6 +97,21 @@ func _test_price_ladder(db) -> void: prices == [20, 30, 45, 67, 101]) Kit.check("已满级(owned=5)再购价照算 151(cap 闸归写口,价格函数纯几何)〔锻造工坊批 V2〕", ForgeTableScript.level_price(row, 5) == 151) + # 六路线全量梯(断言补齐批 V11——floor 公式按表值推算不写死表值、逐级严格递增、满级档价=上限档推算值) + var all_ladder_ok := true + for frow in db.crossrun.forge.routes: + var base: int = int(frow["base_price"]) + var ratio: int = int(frow["price_ratio_pct"]) + var cap: int = int(frow["cap"]) + var prev := 0 + for owned in range(cap + 1): + var expect: int = int(floor(float(base) * pow(float(ratio) / 100.0, float(owned)))) + if ForgeTableScript.level_price(frow, owned) != expect: + all_ladder_ok = false + if owned > 0 and expect <= prev: + all_ladder_ok = false # 逐级严格递增(ratio>100 几何保证) + prev = expect + Kit.check("六路线全量梯:floor 公式表值推算逐级严格递增、满级档价=上限档推算值〔断言补齐批 V11〕", all_ladder_ok) # ---- 负向:出域行逐类构造各一行全部被拒(V1)---- diff --git a/game/tests/unit/presentation/settings_store_test.gd b/game/tests/unit/presentation/settings_store_test.gd new file mode 100644 index 00000000..91a7e4a0 --- /dev/null +++ b/game/tests/unit/presentation/settings_store_test.gd @@ -0,0 +1,63 @@ +# tests/unit/presentation/settings_store_test.gd — 设置存储单元契约(回读/缺省/应用链) +# 断言面 = SettingsStore 公开方法:save_settings/load_settings 持久化回读、缺文件出厂默认、 +# apply() 引擎档应用(UiKit.motion_scale 双态)。隔离卫生:一律经 _init path 注入指向临时 cfg, +# 禁碰真实 user://settings.cfg(ui_click_probe 设置面段同款纪律)。 +# 运行:godot --headless --path game --script res://tests/unit/presentation/settings_store_test.gd +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const StoreScript := preload("res://presentation/settings_store.gd") + +const ISO_PATH := "user://settings_store_unit_test.cfg" # 隔离注路径(临时件——测毕清理) + + +func _init() -> void: + _test_settings_roundtrip() + _test_settings_default() + _test_settings_apply_chain() + _cleanup() + quit(Kit.finish("settings_store_test")) + + +func _cleanup() -> void: + DirAccess.remove_absolute(ProjectSettings.globalize_path(ISO_PATH)) + + +# ---- 持久化回读(写 true→存→新实例回读 true;节/键 motion/reduced)---- + +func _test_settings_roundtrip() -> void: + var w = StoreScript.new(ISO_PATH) + w.reduced_motion = true + Kit.check("前置:save_settings 落盘成功", w.save_settings()) + var r = StoreScript.new(ISO_PATH) + r.load_settings() + Kit.check("持久化回读:新实例回读 reduced_motion=true(motion/reduced 节键)〔断言补齐批 V6〕", r.reduced_motion == true) + w.reduced_motion = false + Kit.check("回写往返:改 false 再存再读得 false〔断言补齐批 V6〕", w.save_settings() and (func(): + var r2 = StoreScript.new(ISO_PATH) + r2.load_settings() + return r2.reduced_motion == false).call()) + + +# ---- 缺省值(无文件缺省 false)---- + +func _test_settings_default() -> void: + var absent_path := "user://settings_store_unit_absent_%d.cfg" % (Time.get_ticks_msec()) + DirAccess.remove_absolute(ProjectSettings.globalize_path(absent_path)) # 确保无文件 + var d = StoreScript.new(absent_path) + d.load_settings() + Kit.check("无文件缺省:reduced_motion=false(缺文件=出厂默认合法态)〔断言补齐批 V7〕", d.reduced_motion == false) + + +# ---- 引擎档应用链(reduced→apply()→UiKit.motion_scale==0.5,false→1.0;测毕还原静态初值)---- + +func _test_settings_apply_chain() -> void: + var saved_scale: float = UiKit.motion_scale # 静态初值留底——测毕还原 + var a = StoreScript.new(ISO_PATH) + a.reduced_motion = true + a.apply() + Kit.check("应用链:reduced=true → UiKit.motion_scale==0.5〔断言补齐批 V8〕", UiKit.motion_scale == 0.5) + a.reduced_motion = false + a.apply() + Kit.check("应用链:reduced=false → UiKit.motion_scale==1.0〔断言补齐批 V8〕", UiKit.motion_scale == 1.0) + UiKit.motion_scale = saved_scale # 还原(静态共享面——不渗入其他套件) diff --git a/game/tests/unit/presentation/settings_store_test.gd.uid b/game/tests/unit/presentation/settings_store_test.gd.uid new file mode 100644 index 00000000..32168909 --- /dev/null +++ b/game/tests/unit/presentation/settings_store_test.gd.uid @@ -0,0 +1 @@ +uid://bqscskbjj026u