# tests/unit/presentation/level_map_info_fog_test.gd — 信息迷雾呈现显隐直接单测 # 断言面 = LevelMapScreen.node_display 决策面(显隐/Boss 豁免/详情零显示/label 三态)+ # display_probe 真机绘制面读数+大图(11 层×6 槽含双 Boss)画布拟合与布局包络。 # 节点图夹具白盒注入 LevelRunner(level_row+停点态),屏体真机实例化走 open_choices 真实入口。 # 运行:godot --headless --path game --script res://tests/unit/presentation/level_map_info_fog_test.gd(expect 0) extends SceneTree const Kit := preload("res://tests/unit/test_kit.gd") const LevelTableScript := preload("res://data/schemas/level_table.gd") const ScreenScript := preload("res://presentation/views/level_map_screen.gd") func _initialize() -> void: call_deferred("_run") func _run() -> void: var db = load("res://core/config_db.gd").new() var loaded: bool = db.load_tables("res://data/tables", false) Kit.check_quiet(loaded, "迷雾呈现用例:数值表加载成功") if not loaded: quit(Kit.finish("level_map_info_fog_test")) return _test_fog_hides_type() _test_boss_exempt_and_multi_boss() _test_details_never_shown() _test_label_three_states() await _test_big_graph_fit(db) quit(Kit.finish("level_map_info_fog_test")) # 白盒 runner:夹具行集注入 db.level + level_row,入口停点态(node_index=-1,候选=入边首节点) func _runner(db, nodes: Array, candidates: Array, n: int = 2): var t := LevelTableScript.new() t.info_visible_layers = n t.rows = [{"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}] db.level = t var r = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new()) r.level_row.assign({"level_index": 1, "display_name": "夹具关", "rest_heal_pct": 30, "nodes": nodes}) r._graph_mode = true r.node_index = -1 r._candidates.assign(candidates) return r # ---- ① 范围外只见结构:雾下节点类型图标与文字均不绘制〔fv2〕---- func _test_fog_hides_type() -> void: var node := {"node_depth": 3, "type": "battle"} var shown: Dictionary = ScreenScript.node_display(node, 2, [2], false, false) Kit.check("可见集内节点:图标/类型文字均绘制〔fv2〕", bool(shown["draw_icon"]) and bool(shown["draw_label"])) var fogged: Dictionary = ScreenScript.node_display(node, 3, [2], false, false) Kit.check("雾下节点:图标与类型文字均不绘制(显隐语义,非灰化)〔fv2〕", not bool(fogged["draw_icon"]) and not bool(fogged["draw_label"])) Kit.check("雾下非 Boss 节点无 Boss 描边〔fv2〕", not bool(fogged["boss_border"])) # ---- ② Boss/终点不入雾+多终点标记〔fv3〕---- func _test_boss_exempt_and_multi_boss() -> void: var type_vis: Array = [1, 7, 9] # 双 Boss 图:7/9 均在可见集 var boss_a: Dictionary = ScreenScript.node_display({"node_depth": 7, "type": "boss"}, 7, type_vis, false, false) var boss_b: Dictionary = ScreenScript.node_display({"node_depth": 9, "type": "boss"}, 9, type_vis, false, false) Kit.check("多终点:两个 Boss 节点均带 Boss 描边〔fv3〕", bool(boss_a["boss_border"]) and bool(boss_b["boss_border"])) Kit.check("Boss 不入雾:可见集与图标/文字三通道均在〔fv3〕", bool(boss_a["draw_icon"]) and bool(boss_a["draw_label"]) and bool(boss_b["draw_icon"])) var far_boss: Dictionary = ScreenScript.node_display({"node_depth": 9, "type": "boss"}, 9, [1], false, false) Kit.check("Boss 豁免独立于可见集:不在 type_vis 的 Boss 仍出示类型〔fv3〕", bool(far_boss["draw_icon"]) and bool(far_boss["draw_label"])) var fogged_battle: Dictionary = ScreenScript.node_display({"node_depth": 5, "type": "battle"}, 5, type_vis, false, false) Kit.check("Boss 豁免不扩散:雾下非 Boss 仍隐类型〔fv3〕", not bool(fogged_battle["draw_icon"])) # ---- ③ 节点内详情图上从不显示〔fv5〕---- func _test_details_never_shown() -> void: var plain := {"node_depth": 3, "type": "battle"} var loaded := {"node_depth": 3, "type": "battle", "enemy_id": "goblin_secret", "reward_id": "reward_secret"} var a: Dictionary = ScreenScript.node_display(plain, 3, [3], false, true) var b: Dictionary = ScreenScript.node_display(loaded, 3, [3], false, true) var same := true for k in a.keys(): if a[k] != b[k]: same = false Kit.check("决策面不消费内容键:携带 enemy_id/reward_id 与不带者输出逐键相等〔fv5〕", same) Kit.check("label_text 不含内容值(实际「%s」)〔fv5〕" % String(b["label_text"]), not String(b["label_text"]).contains("goblin_secret") and not String(b["label_text"]).contains("reward_secret")) # ---- ④ label_text 三态文案〔fv2〕---- func _test_label_three_states() -> void: var node := {"node_depth": 3, "type": "battle"} Kit.check("当前位文案=「当前位置」〔fv2〕", String(ScreenScript.node_display(node, 3, [3], true, false)["label_text"]) == "当前位置") Kit.check("候选文案=「战斗 · 可选」〔fv2〕", String(ScreenScript.node_display(node, 3, [3], false, true)["label_text"]) == "战斗 · 可选") Kit.check("其余文案=类型名「战斗」〔fv2〕", String(ScreenScript.node_display(node, 3, [3], false, false)["label_text"]) == "战斗") # ---- 大图(11 层×6 槽含双 Boss)画布拟合与布局包络〔fv8〕---- # 大图夹具:编号按 12 层×6 槽构造,层 0 入口、末层双 Boss;含层 1 首节点→第 3 层节点的跨层跳边。 # 跳边目标按 BFS 最短路径提前一层,其后继链随之提前——布局落 11 层(最大槽 7: # 跳边目标并入第 2 层槽组)。断言按布局实测取最大层/最大槽,画布公式与之同源。 func _big_graph_nodes() -> Array: var layers: Array = [] var total := 0 var tiers := 12 for l in tiers: var width: int = 1 if l == 0 else (2 if l == tiers - 1 else 6) var ids: Array = [] for i in width: ids.append(total) total += 1 layers.append(ids) var nodes: Array = [] var jump_from := -1 var jump_to := -1 for l in tiers: for i in layers[l].size(): var nid: int = layers[l][i] var n := {"node_depth": nid} if l == 0: n["type"] = "battle" n["next"] = layers[1].duplicate() # 入口扇出到全层(层宽 6) elif l == tiers - 1: n["type"] = "boss" n["next"] = [] else: if i % 5 == 0: n["type"] = "reward" if l % 2 == 0 else "rest" else: n["type"] = "battle" var w2: int = layers[l + 1].size() n["next"] = [layers[l + 1][i % w2], layers[l + 1][(i + 1) % w2]] nodes.append(n) jump_from = layers[1][0] jump_to = layers[3][2] for n in nodes: if int(n["node_depth"]) == jump_from: (n["next"] as Array).append(jump_to) break return nodes func _test_big_graph_fit(db) -> void: var nodes := _big_graph_nodes() var entry: Dictionary = nodes[0] var runner = _runner(db, nodes, [nodes[1]]) var screen = ScreenScript.new() root.add_child(screen) screen.open_choices([nodes[1]], runner) await process_frame await process_frame var min_size: Vector2 = screen.canvas_probe()["min_size"] var pos: Dictionary = screen.layout_probe(nodes) var origin: Vector2 = (pos[entry] as Vector2) - Vector2(ScreenScript.LAYER_BASE_X, ScreenScript.SLOT_BASE_Y) # 最大层 / 最大槽按布局实测反解(公式与 _fit_canvas 同源) var maxl := 0 var maxslot := 0 for node in nodes: var p: Vector2 = pos[node] maxl = maxi(maxl, int(round((p.x - origin.x - ScreenScript.LAYER_BASE_X) / ScreenScript.LAYER_STEP_X))) maxslot = maxi(maxslot, int(round((p.y - origin.y - ScreenScript.SLOT_BASE_Y) / ScreenScript.SLOT_STEP_Y))) Kit.check("大图夹具规模:≥11 层 × ≥6 槽(实测最大层 %d/最大槽 %d)〔fv8〕" % [maxl, maxslot], maxl >= 10 and maxslot >= 5) var need_w: float = ScreenScript.LAYER_BASE_X + maxl * ScreenScript.LAYER_STEP_X \ + ScreenScript.NODE_HALF.x + 8.0 var need_h: float = ScreenScript.SLOT_BASE_Y + maxslot * ScreenScript.SLOT_STEP_Y \ + ScreenScript.NODE_HALF.y + 24.0 Kit.check("大图拟合:画布 min.x 覆盖最深节点(%.1f ≥ %.1f)〔fv8〕" % [min_size.x, need_w], min_size.x >= need_w - 0.01) Kit.check("大图拟合:画布 min.y 覆盖最宽层底节点(%.1f ≥ %.1f)〔fv8〕" % [min_size.y, need_h], min_size.y >= need_h - 0.01) var fit_rect := Rect2(origin, min_size) var all_in := true for node in nodes: var p: Vector2 = pos[node] if not fit_rect.encloses(Rect2(p - ScreenScript.NODE_HALF, ScreenScript.NODE_HALF * 2.0)): all_in = false Kit.check("大图布局:全节点方块落在拟合画布内(origin=(%.1f,%.1f) size=(%.1f,%.1f))〔fv8〕" % [origin.x, origin.y, min_size.x, min_size.y], all_in) var disp: Dictionary = screen.display_probe() var boss_ids: Array = [] for node in nodes: if String(node["type"]) == "boss": boss_ids.append(int(node["node_depth"])) Kit.check("大图呈现:两个终点 Boss 均有描边标记〔fv8〕", disp.has(boss_ids[0]) and disp.has(boss_ids[1]) and bool(disp[boss_ids[0]]["boss_border"]) and bool(disp[boss_ids[1]]["boss_border"])) Kit.check("大图呈现:入口停点远处战斗节点(编号 32,层 6)类型入雾〔fv8〕", not bool(disp[32]["draw_icon"])) screen.queue_free()