diff --git a/game/core/config_db_cross_validator.gd b/game/core/config_db_cross_validator.gd index 7bb4b95f..4e7d8912 100644 --- a/game/core/config_db_cross_validator.gd +++ b/game/core/config_db_cross_validator.gd @@ -51,60 +51,8 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void: for s in required_sources: if not st_tab.source_label_map.has(s): push_diag.call("error", "segorder_violation", "source_label_map 缺必需键: %s" % s) - # ④ LevelTable 节点悬空引用(battle/boss 节点:board_gen 侧须有精确行;enemy 侧按 - # (关卡序号, 行类) 带寻址——落带即设计语义,节点深度高于顶带键=顶 clamp 消解属调参事故,加载期直接拦。 - # 同关同一张图多终点 Boss 节点解析带键须互异(共带=两终点命中同一 Boss 行集,「走哪条路打谁」落空)) - var lv: LevelTable = loaded.get("level") - var bg: BoardGenTable = loaded.get("board_gen") - var en: EnemyTable = loaded.get("enemy") - if lv != null and bg != null and en != null: - var bg_keys := {} - for row in bg.rows: - bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true - var en_bands := {} # "%d_%s" % [L, row_kind] → 升序带键数组 - for row in en.rows: - var ek := "%d_%s" % [int(row.level_index), String(row.get("row_kind", ""))] - if not en_bands.has(ek): - en_bands[ek] = [] - var eb := int(row.get("depth_band", -1)) - if not (en_bands[ek] as Array).has(eb): - (en_bands[ek] as Array).append(eb) - for ek in en_bands.keys(): - (en_bands[ek] as Array).sort() - for lrow in lv.rows: - var li: int = int(lrow.level_index) - # 模板池感知(关卡节点图生成 v0.4——池内全模板跨表校验,不只 row.nodes) - var node_lists: Array[Array] = [] - var tpls: Array[Variant] = lrow.get("templates", []) - if tpls.is_empty(): - node_lists.append(lrow.get("nodes", [])) - else: - for tpl in tpls: - node_lists.append(tpl.get("nodes", [])) - for tpl_i in range(node_lists.size()): - var boss_bands := {} # 本图已占 Boss 带键 - for node in node_lists[tpl_i]: - var t: String = String(node.get("type", "")) - if t != "battle" and t != "boss": - continue - var nd: int = int(node.get("node_depth", -1)) - var key := "%d_%d" % [li, nd] - if not bg_keys.has(key): - push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key]) - var nk := "%d_%s" % [li, t] - if not en_bands.has(nk): - push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%d) 在 enemy 无该行类行集" % [li, t, nd]) - continue - var hit_band: int = EnemyTable.band_of(en_bands[nk], nd) - if hit_band < 0: - push_diag.call("error", "level_node_dangling", "level %d %s 节点深度 %d 高于 (level %d, %s) 顶带键 %d——顶 clamp 消解" % [ - li, t, nd, li, t, int((en_bands[nk] as Array)[-1])]) - continue - if t == "boss": - if boss_bands.has(hit_band): - push_diag.call("error", "rowset_rule", "level %d 模板 %d 多终点 Boss 节点同带(band %d)——共带使「走哪条路打谁」落空" % [li, tpl_i, hit_band]) - boss_bands[hit_band] = true # ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验) + var en: EnemyTable = loaded.get("enemy") var skt = loaded.get("skill") if skt != null and en != null: for e in validate_skill_pool_links(skt, en, loaded.get("status_effects")): diff --git a/game/core/save_manager.gd b/game/core/save_manager.gd index 01e0dc26..ef3f9f86 100644 --- a/game/core/save_manager.gd +++ b/game/core/save_manager.gd @@ -299,9 +299,10 @@ func _default_data() -> Dictionary[String, Variant]: } -# ---- 首次冒险布尔位(关卡节点图生成 v0.4)---- -# false = 首次脚本模式(全关零抽签取池内模板 0=现行固定图);置位只在冒险收束挂点(MetaProgression.settle_adventure)。 -# 老档迁移:缺字段 = 该档已有游玩史 → true(不重放教学脚本)。 +# ---- 首次冒险布尔位(关卡节点图生成 v1.1 确定性承载)---- +# false = 本档尚无游玩史 → 首局走固定种子 42 + 参数档生成图(教学与存档复现的固定图由此承载,无兜底模板); +# 置位只在冒险收束挂点(MetaProgression.settle_adventure)。 +# 老档迁移:缺字段 = 该档已有游玩史 → true(不重放首局固定图)。 func is_first_adventure_done() -> bool: return bool(_data["meta"].get("first_adventure_done", true)) diff --git a/game/tests/unit/core/config_db_cross_validator_test.gd b/game/tests/unit/core/config_db_cross_validator_test.gd index f3fac232..e2b7731c 100644 --- a/game/tests/unit/core/config_db_cross_validator_test.gd +++ b/game/tests/unit/core/config_db_cross_validator_test.gd @@ -16,7 +16,6 @@ func _init() -> void: _test_all_links_valid() _test_dangling_skill_id() _test_cast_side_mismatch() - _test_level_node_band_rules() quit(Kit.finish("config_db_cross_validator_test")) @@ -83,61 +82,3 @@ func _test_cast_side_mismatch() -> void: var status := _status([{"effect_id": "st_hero_side", "target_side": "hero"}]) var errs: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemies, status) Kit.check("buff 挂主角侧状态被报(施放类 side 守卫)", _cats(errs).has("cast_side")) - - -# ---- ④ 节点 × 敌人表带寻址(单终点零误报/同带双 Boss 报错/顶 clamp 消解报错)---- -const LevelTableScript := preload("res://data/schemas/level_table.gd") -const BoardGenTableScript := preload("res://data/schemas/board_gen_table.gd") -const RewardTableScript := preload("res://data/schemas/reward_table.gd") -const SettlementTableScript := preload("res://data/schemas/settlement_table.gd") - - -func _loaded(level_nodes: Array, bg_depths: Array, boss_bands: Array, battle_bands: Array) -> Dictionary: - var lv := LevelTableScript.new() - lv.rows.assign([{"level_index": 1, "display_name": "夹具关", "templates": [{"nodes": level_nodes}]}]) - var bg := BoardGenTableScript.new() - var bg_rows: Array = [] - for d in bg_depths: - bg_rows.append({"level_index": 1, "node_depth": int(d), "board_width": 10}) - bg.rows.assign(bg_rows) - var en := EnemyTableScript.new() - var en_rows: Array = [] - for b in battle_bands: - en_rows.append({"level_index": 1, "row_kind": "battle", "depth_band": int(b), "enemy_id": "goblin", - "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}) - for b in boss_bands: - en_rows.append({"level_index": 1, "row_kind": "boss", "depth_band": int(b), "enemy_id": "ogre_chief", - "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}) - en.rows.assign(en_rows) - var st := SettlementTableScript.new() - st.source_label_map.assign({"safe_cell": "a", "seal_hit": "b", "mine_hit": "c", "clear_reward": "d", - "hero_attack": "e", "enemy_action": "f", "seal_miss": "g"}) - return {"status_effects": StatusTableScript.new(), "reward": RewardTableScript.new(), - "settlement": st, "level": lv, "board_gen": bg, "enemy": en} - - -func _cross_cats(loaded: Dictionary, needle: String) -> Array[String]: - var out: Array[String] = [] - ValidatorScript.new().cross_validate(loaded, - func(_sev, cat, msg) -> void: - if String(msg).contains(needle): - out.append(String(cat))) - return out - - -func _test_level_node_band_rules() -> void: - # 正向:单终点三关同构(battle 节点落 band 0、boss 节点精确落顶带)→ ④ 零误报 - var ok_nodes := [{"type": "battle", "node_depth": 0}, {"type": "boss", "node_depth": 9}] - var ok_cats := _cross_cats(_loaded(ok_nodes, [0, 9], [9], [0]), "level 1") - Kit.check("单终点图 ④ 敌侧带寻址零误报〔fv17〕", ok_cats.is_empty()) - # 负向:一路两终点落同一 Boss 带(深度 3/4 都落带 4)→ rowset_rule - var same_band_nodes := [{"type": "battle", "node_depth": 0}, - {"type": "boss", "node_depth": 3}, {"type": "boss", "node_depth": 4}] - var same_band_cats := _cross_cats(_loaded(same_band_nodes, [0, 3, 4], [4, 9], [0]), "同带") - Kit.check("同关双终点共带报 rowset_rule〔fv15〕", - same_band_cats.has("rowset_rule")) - # 负向:Boss 节点深度高于顶带键(顶 clamp 消解)→ level_node_dangling - var over_nodes := [{"type": "battle", "node_depth": 0}, {"type": "boss", "node_depth": 10}] - var over_cats := _cross_cats(_loaded(over_nodes, [0, 10], [9], [0]), "顶 clamp 消解") - Kit.check("Boss 节点越顶带报 level_node_dangling〔fv16〕", - over_cats.has("level_node_dangling"))