iter50-卡2:卡B(断言补齐): 四域13新断言+2处既有扩展——①增益结算链V1-V5 ②设置存储V6-V8 ⑤Boss门控V9/V10 ⑥锻造V11-V16; 新件settings_store_test与boss_gate_test; manifest+2; 73套件全绿
This commit is contained in:
1 parent
e658492703
commit
cd133932f9
9 files changed
+329
-1
No files matched your search
@@ -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 绑定/素材入包/地台色合并退役)
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dkmph2dfqjqer
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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)----
|
||||
|
||||
@@ -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 # 还原(静态共享面——不渗入其他套件)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bqscskbjj026u
|
||||
Reference in new issue
Block a user