- level_table.gd: info_visible_layers 字段(int≥1)+validate 越域 domain 错;level.tres 挂 2 - level_runner.gd: type_visible_ids() 纯函数(入口 BFS 层差 ≤N ∪ Boss ∪ 候选,锚=当前节点层/入口停点 0;visited 不豁免)+_bfs_layers() - level_map_screen.gd: node_display() 静态决策面+display_probe()+_draw_graph 图标/文字/Boss 描边三处改读决策面(结构通道零改动) - 新单测 level_map_type_vis_test(26 断言)/level_map_info_fog_test(19 断言);run_all 注册两行 - smoke_all ⑬ 真图 L2 入口停点可见性组(+5 断言);ui_input_probe ⑬ 常数锚同源化+⑱ 雾不扩可点域 轻闸读数:冒烟 1211 项 0 失败(主区基线 1206,+5=新增组)/probe_gate --click 全绿
197 lines
9.1 KiB
GDScript
197 lines
9.1 KiB
GDScript
# 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()
|