diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 64d3fece..9173fba1 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -230,6 +230,34 @@ func _smoke_adventure_table() -> void: check(_db.attunement_growth != null and int(_db.attunement_slot_cap()) == 3 and Attunement.slots(_db, null) == 0, "同调位读数源=attunement_growth 谓词表(slot_cap 3;未注入档求值 0——transitional_sync_slots 过渡键已退役,迭代 11 第①批 T7)") + # 迭代 19 卡④——提灯载体行(fog_lantern):新行加载/schema/同调目录投影+装配→战斗贯通(核心层半边; + # 呈现半边归 ui_input_probe case⑮ 载体级段——测试三形态契约 9) + var lantern_row: Dictionary = _db.treasure_items.row_by_id("fog_lantern") + check(not lantern_row.is_empty() and String(lantern_row.get("display_name", "")) == "魔法提灯" + and String(lantern_row.get("desc", "")) == "灯光区半径+2" + and String(lantern_row.get("form", "")) == "passive" and int(lantern_row.get("slot_cost", -1)) == 1 + and (lantern_row.get("dimension_tags", []) as Array) == ["info"] + and (lantern_row.get("effect_kinds", []) as Array).is_empty() + and lantern_row.has("triggers") and (lantern_row.get("triggers", []) as Array).is_empty() + and String(lantern_row.get("target", "")) == "none" and int(lantern_row.get("magnitude", -1)) == 0 + and String(lantern_row.get("use_phase", "")) == "active_dig" + and not lantern_row.has("unlock_requires") + and not bool(lantern_row.get("retired", false)) and not bool(lantern_row.get("deferred", false)), + "提灯载体行 fog_lantern:行字段逐键=裁值(魔法提灯/灯光区半径+2/passive/slot_cost 1/[info]/空效果/显式 triggers 空数组/none/0/active_dig/unlock 键省略=已解锁/活行非封存)") + var att_lantern := {} + for r in _db.treasure_attunement_rows(): + if String(r.get("item_id", "")) == "fog_lantern": + att_lantern = r + check(not att_lantern.is_empty() and String(att_lantern.get("display_name", "")) == "魔法提灯", + "同调目录投影:fog_lantern 在装配词表且 display_name=魔法提灯(营地工坊目录九件→十件)") + var orch_l = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(191)) + check(orch_l.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp), "attunement": ["fog_lantern"]}) + and orch_l.attune_has("fog_lantern"), + "装配→战斗贯通:enc.attunement=[fog_lantern] 起战 attune_has=true(注入面+只读出口)") + var orch_e = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(192)) + check(orch_e.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}) + and not orch_e.attune_has("fog_lantern"), + "空装配基线:不带 attunement 起战 attune_has=false(off 态零扰动)") # ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态) diff --git a/game/smoke/ui_input_probe.gd b/game/smoke/ui_input_probe.gd index a3291bd5..14caae64 100644 --- a/game/smoke/ui_input_probe.gd +++ b/game/smoke/ui_input_probe.gd @@ -416,6 +416,27 @@ func _ready() -> void: _problems.append("case⑮ 提灯关闭后雾参数未复原(lantern 应=0)") if int(bv.cells_probe()[ring_cell].fog_state) == 0: _problems.append("case⑮ 提灯关闭后 dist==R+1 格未复原遮蔽") + # 载体级断言(迭代 19 卡④——fog_lantern 装配链贯通:①装配→开局参数面〔build_adventure_params + # 统一构造=卡⓪①缺陷修复面回归〕②装配集→呈现桥〔_sync_fog_lantern 消费 attune_has 只读出口 + # →set_fog_lantern〕→视野 +2 生效与卸装复原。settlement.attunement 手动注入=模拟 begin_battle + # enc 注入后的运行态〔注入面本体归冒烟+单测覆盖〕;不重启冒险——case⑯ 起既有板面继续用) + main.save_manager.set_attunement_equipped(["fog_lantern"]) + var p_lantern: Dictionary = main.build_adventure_params(main.save_manager, main.meta) + if not (p_lantern.get("attunement", []) as Array).has("fog_lantern"): + _problems.append("case⑮ 载体装配未进开局参数面(attunement=%s 应含 fog_lantern)" % str(p_lantern.get("attunement", []))) + main.settlement.attunement = ["fog_lantern"] + main._sync_fog_lantern() + if int(bv.fog_params_probe()["lantern"]) != 2: + _problems.append("case⑮ 载体装配后视野未扩(lantern 应=2——呈现桥未生效)") + if int(bv.cells_probe()[ring_cell].fog_state) != 0: + _problems.append("case⑮ 载体装配后 dist==R+1 格仍被遮(应转可见)") + main.settlement.attunement = [] + main._sync_fog_lantern() + if int(bv.fog_params_probe()["lantern"]) != 0: + _problems.append("case⑮ 载体卸装后视野未复原(lantern 应=0——契约 7 显式重算含否)") + if int(bv.cells_probe()[ring_cell].fog_state) == 0: + _problems.append("case⑮ 载体卸装后 dist==R+1 格未复原遮蔽") + main.save_manager.set_attunement_equipped([]) # ---- case⑯ 暂停层输入锁语义(迭代 17 卡③ R3——开层锁棋盘、关层恢复无残留;真实 push_input # 路径:入口角标本体 press+release 真点击 → 暂停层开;锁期挖掘点击零穿透;「继续」关层后 # 同格挖掘重新到达——锁语义与既有模态同款,无状态残留。置于全 case 之尾:取格若需前沿移位 diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index d3d9f022..3056ff35 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -43,6 +43,7 @@ func _suites() -> Array: {"path": "res://tests/unit/contracts/system_contracts_test.gd", "expect": 0}, {"path": "res://tests/unit/presentation/ui_font_asset_test.gd", "expect": 0}, {"path": "res://tests/unit/presentation/terrain_tile_sets_test.gd", "expect": 0}, + {"path": "res://tests/unit/core/fog_lantern_wiring_test.gd", "expect": 0}, # 迭代 19 卡④——提灯载体装配链(行加载/schema/只读出口双态/enc 注入面) ] diff --git a/game/tests/unit/core/breakpoint_resume_test.gd b/game/tests/unit/core/breakpoint_resume_test.gd index c0d351a5..fba5cce5 100644 --- a/game/tests/unit/core/breakpoint_resume_test.gd +++ b/game/tests/unit/core/breakpoint_resume_test.gd @@ -128,8 +128,27 @@ func _init() -> void: Kit.check_quiet(String(got_result["r"].get("picked", "")) == picked_e, "P5 已兑付不重付(resolved 重发同件)") Kit.check_quiet(adr_g.continue_to_next_level(), "P5 已兑付续玩后推进受理(下一关开始)") + # ---- Part 6:装配存档回归(迭代 19 卡④——装配→停点→回局载体态一致:params.attunement 随续玩 + # 受理面重建再注入战斗〔begin_battle enc 携带〕;呈现态每战重算归呈现桥(main._sync_fog_lantern), + # 本断言钉核心层装载往返——回局后 attune_has 只读出口读数一致)---- + var rng_h = load("res://core/rng_service.gd").new() + var sm_h = SaveManagerScript.new(_bp_path) + var adr_h = AdventureRunnerScript.new(db, rng_h) + adr_h.set_save_manager(sm_h) + var params_h := {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["fog_lantern"]} + Kit.check_quiet(adr_h.start_adventure(5678, params_h), "P6 装配冒险启动(seed 5678——params 带 fog_lantern)") + Kit.check_quiet(adr_h.runner().pick_next_node(int(adr_h.runner().next_candidate_ids()[0])), "P6 选路进首战") + Kit.check_quiet(adr_h.runner().current_battle().attune_has("fog_lantern"), "P6 首战装配集含 fog_lantern(attune_has 只读出口)") + var sm_i = SaveManagerScript.new(_bp_path) + Kit.check_quiet(sm_i.load_from_disk() and sm_i.has_breakpoint(), "P6 断点在档(停点可中断)") + var adr_i = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new()) + adr_i.set_save_manager(sm_i) + Kit.check_quiet(adr_i.resume_adventure(sm_i.breakpoint_snapshot(), params_h), "P6 续玩受理(装配集随 params 再注入)") + Kit.check_quiet(adr_i.runner().pick_next_node(int(adr_i.runner().next_candidate_ids()[0])), "P6 续玩选路进首战") + Kit.check_quiet(adr_i.runner().current_battle().attune_has("fog_lantern"), "P6 回局首战装配集仍含 fog_lantern(停点往返载体态一致)") + _clear_disk() - quit(Kit.finish("breakpoint_resume_test", "PASS — 断点往返/续玩确定性/幂等/退出清断点 全绿")) + quit(Kit.finish("breakpoint_resume_test", "PASS — 断点往返/续玩确定性/幂等/退出清断点/装配存档回归 全绿")) func _board_fingerprint(b) -> String: diff --git a/game/tests/unit/core/fog_lantern_wiring_test.gd b/game/tests/unit/core/fog_lantern_wiring_test.gd new file mode 100644 index 00000000..55bcb19f --- /dev/null +++ b/game/tests/unit/core/fog_lantern_wiring_test.gd @@ -0,0 +1,60 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 提灯载体 fog_lantern 装配链单测(迭代 19 卡④ T3——测试三形态之「呈现桥判定=注入装配集 stub 的 +# 单测 + 行加载单测」(任务书契约 9);probe=case⑮ 载体级段 / smoke=现有套件加断言,另两形态不在本件)。 +# 覆盖:真实表行加载+schema 校验零违例 / 行字段裁值逐键 / 同调目录词表收录 / attune_has 只读出口 +# 双态(组合根 _sync_fog_lantern 消费面)/ begin_battle enc 注入面。 +# 运行:godot --headless --path game --script res://tests/unit/core/fog_lantern_wiring_test.gd(expect 0) +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") # check/汇总退出样板单点(静默收集通道) + + +func _init() -> void: + # ① 行加载+schema 校验(真实 treasure_items.tres——活行空 effect_kinds 豁免以 row.has("triggers") + # 为条件(treasure_item_table.gd:111),triggers 键漏写=加载红——本断言钉住该易错点) + var tbl = load("res://data/tables/treasure_items.tres") + Kit.check_quiet(tbl != null and tbl.validate().is_empty(), + "fog_lantern 入表后 TreasureItemTable.validate 零违例(KEY_WHITELIST/活行校验全过)") + var row: Dictionary = tbl.row_by_id("fog_lantern") if tbl != null else {} + Kit.check_quiet(not row.is_empty() and String(row.get("display_name", "")) == "魔法提灯" + and String(row.get("desc", "")) == "灯光区半径+2" + and String(row.get("form", "")) == "passive" and int(row.get("slot_cost", -1)) == 1 + and row.has("triggers") and (row.get("triggers", []) as Array).is_empty() + and (row.get("effect_kinds", []) as Array).is_empty() + and String(row.get("target", "")) == "none" and int(row.get("magnitude", -1)) == 0 + and String(row.get("use_phase", "")) == "active_dig" + and (row.get("dimension_tags", []) as Array) == ["info"] + and not row.has("unlock_requires") + and not bool(row.get("retired", false)) and not bool(row.get("deferred", false)), + "行字段逐键=用户「按推荐」裁值(魔法提灯/灯光区半径+2/passive/slot_cost 1/显式 triggers 空/none/0/active_dig/[info]/unlock 键省略=已解锁/活行)") + var in_att := false + for r in (tbl.attunement_rows() as Array): + if String(r.get("item_id", "")) == "fog_lantern": + in_att = true + Kit.check_quiet(in_att, "同调目录词表收录 fog_lantern(attunement_rows=form 非空活行口径)") + + # ② 数据表宿主(②③需 db) + var db = load("res://core/config_db.gd").new() + if not db.load_tables("res://data/tables", false): + print("[fog_lantern_wiring_test] FAIL — 数值表加载失败") + quit(1) + return + + # ③ 呈现桥判定 stub:装配集直注→只读出口双态(组合根 _sync_fog_lantern 消费面——不经完整战斗) + var rng_a = load("res://core/rng_service.gd").new() + rng_a.set_master_seed(191) # 播种(EC7 未播种取数致命——begin_battle 板生成取号前提) + var orch = load("res://core/battle_orchestrator.gd").new(db, rng_a) + orch.attunement = ["fog_lantern"] + Kit.check_quiet(orch.attune_has("fog_lantern"), "attune_has(fog_lantern)=true(装配集在册)") + Kit.check_quiet(not orch.attune_has("luck_coin"), "attune_has(luck_coin)=false(他件不误判)") + orch.attunement = [] + Kit.check_quiet(not orch.attune_has("fog_lantern"), "attune_has(fog_lantern)=false(空集=复原语义读数源)") + + # ④ begin_battle enc 注入面(runner→enc.attunement→orchestrator 运行态) + var rng_b = load("res://core/rng_service.gd").new() + rng_b.set_master_seed(192) + var orch2 = load("res://core/battle_orchestrator.gd").new(db, rng_b) + Kit.check_quiet(orch2.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["fog_lantern"]}) + and orch2.attune_has("fog_lantern"), "begin_battle enc.attunement 注入→attune_has=true(每战装配链装载)") + + quit(Kit.finish("fog_lantern_wiring_test", "PASS — 行加载/schema 校验/同调词表收录/只读出口双态/enc 注入面 全绿")) diff --git a/game/tests/unit/core/fog_lantern_wiring_test.gd.uid b/game/tests/unit/core/fog_lantern_wiring_test.gd.uid new file mode 100644 index 00000000..77ccd6c8 --- /dev/null +++ b/game/tests/unit/core/fog_lantern_wiring_test.gd.uid @@ -0,0 +1 @@ +uid://jlkj801c81qd