extends SceneTree # 营地场景布局数据件校验套件(V14——camp_scene_layout.tres 加载期 fail-fast 判据+七命中区几何红线机检) # 覆盖:校验函数通过(键唯一/贴图在画布/命中成对/围墙带包络)· 七可点件键集 · 两两不相交 · # 不越画面 · 不侵顶栏禁区 · 篝火命中只取下半(关系谓词)· 门扇对齐门洞缺口 · 布告板挂围墙带。 # 断言全部为表内关系谓词——禁坐标字面量(数值单一事实源 = 数据行本表)。 # 运行:godot --headless --path game --script res://tests/unit/presentation/camp_scene_layout_test.gd(expect 0) const Kit := preload("res://tests/unit/test_kit.gd") const _EXPECTED_KEYS: Array[StringName] = [&"gate", &"forge", &"dummy", &"chest", &"notebook", &"board", &"campfire"] func _initialize() -> void: # oracle: design/ux/camp.md:29 (营地场景布局数据件——纵深与地平线组) call_deferred("_run_all") func _run_all() -> void: # oracle: design/ux/camp.md:29 (套件统一收尾——双测试函数单进程单 quit) await _test_layout_hotspot_geometry() await _test_hud_band_contrast_over_bg() quit(Kit.finish("camp_scene_layout_test", "PASS — 布局几何+顶栏栏体对比 全过")) func _test_layout_hotspot_geometry() -> void: # oracle: design/ux/camp.md:16 (七物件命中区几何红线组) var table: CampSceneLayoutTable = load("res://data/tables/camp_scene_layout.tres") Kit.check("V14 布局表加载且 schema 类在位〔营地图批 V14〕", table is CampSceneLayoutTable and table != null) if table == null: return var problems: Array[String] = table.validate() Kit.check("V14 校验函数通过(问题清单空,实得 %s)〔营地图批 V14〕" % [problems], problems.is_empty()) var clickables: Array[CampSceneLayerSpec] = [] for spec in table.layers: if spec.is_clickable(): clickables.append(spec) Kit.check("V14 可点件数=7(实得 %d)〔营地图批 V14〕" % clickables.size(), clickables.size() == 7) var got_keys: Array[StringName] = [] for spec in clickables: got_keys.append(spec.hotspot_key) got_keys.sort() var want_keys: Array[StringName] = [] for k in _EXPECTED_KEYS: want_keys.append(k) want_keys.sort() Kit.check("V14 命中键集=七物件绑定表(实得 %s)〔营地图批 V14〕" % [got_keys], got_keys == want_keys) # 两两不相交(Rect2.intersects——贴边相触不算相交) var disjoint := true var pair := "" for i in clickables.size(): for j in range(i + 1, clickables.size()): if clickables[i].hotspot.intersects(clickables[j].hotspot): disjoint = false pair = "%s×%s" % [clickables[i].key, clickables[j].key] Kit.check("V14 七命中区两两不相交(首违例 %s)〔营地图批 V14〕" % pair, disjoint) # 不越画面+不侵顶栏禁区 var in_bounds := true var offender := "" for spec in clickables: if spec.hotspot.position.x < 0.0 or spec.hotspot.position.y < table.hud_band_h \ or spec.hotspot.end.x > table.canvas.x or spec.hotspot.end.y > table.canvas.y: in_bounds = false offender = String(spec.key) Kit.check("V14 命中区不越画面且不侵顶栏禁区(y≥%d,违例 %s)〔营地图批 V14〕" % [table.hud_band_h, offender], in_bounds) # 篝火命中只取下半:命中区贴贴图底、严格小于贴图、起点在贴图内(火焰上段不可点) var fire: CampSceneLayerSpec = null var gate: CampSceneLayerSpec = null var board: CampSceneLayerSpec = null var wall: CampSceneLayerSpec = null for spec in table.layers: match String(spec.key): "campfire": fire = spec "gate": gate = spec "board": board = spec "wall": wall = spec var fire_ok := fire != null and fire.hotspot.end.y == fire.texture_rect.end.y \ and fire.hotspot.size.y < fire.texture_rect.size.y \ and fire.hotspot.position.y > fire.texture_rect.position.y Kit.check("V14 篝火命中只取下半(火堆贴底、火焰不点)〔营地图批 V14〕", fire_ok) # 门扇命中对齐门洞缺口(表内 gate_gap 字段关系谓词) var gate_ok := gate != null and gate.hotspot.position.x == float(table.gate_gap.x) \ and gate.hotspot.end.x == float(table.gate_gap.y) Kit.check("V14 门扇命中对齐门洞缺口 %s〔营地图批 V14〕" % [table.gate_gap], gate_ok) # 布告板挂围墙带:命中顶落在围墙贴图带内 var board_ok := board != null and wall != null \ and board.hotspot.position.y >= wall.texture_rect.position.y \ and board.hotspot.position.y <= wall.texture_rect.end.y Kit.check("V14 布告板命中挂围墙带〔营地图批 V14〕", board_ok) # 层序结构:10 层且 bg/wall 压前两位(z0/z1——子节点序=表序先验) var order_ok := table.layers.size() == 10 \ and String(table.layers[0].key) == "bg" and String(table.layers[1].key) == "wall" Kit.check("V14 层数=10 且 bg/wall 占 z0/z1〔营地图批 V14〕", order_ok) # ConfigDB 装载链:表经单点注册进表现域容器且装载期校验过(fail-fast 钩子在位) var db = load("res://core/config_db.gd").new() var load_ok: bool = db.load_tables("res://data/tables", true) Kit.check("V14 ConfigDB 装载含营地布局表且校验过(is_ready=%s)〔营地图批 V14〕" % db.is_ready, load_ok and db.is_ready and db.presentation.camp_scene_layout != null) # 顶栏栏体规格侧对比核验(R6——与 R5 分工:R5 核实装等值,本行核规格值本身达标): # 按规格句在测试内写死 #6E4526 与 α0.80,与背景贴图顶带实测均色做 α 合成后算 WCAG 对比 ≥3.0。 # 禁把视图件的 color 直接当预期值喂进算式(oracle 镜像——test-conventions §8.7 判红)。 func _test_hud_band_contrast_over_bg() -> void: # oracle: design/ux/camp.md:41 (顶栏栏体定值——对落位带底色对比 ≥3:1 组) var table: CampSceneLayoutTable = load("res://data/tables/camp_scene_layout.tres") var band_h := 48 if table != null: band_h = int(table.hud_band_h) # 带高取数据出口(禁字面量) var tex = load("res://assets/art/scene/scene_camp_bg.png") var img: Image = null if tex != null and tex.get_image() != null: img = tex.get_image() var scan_n := 0 var r_sum := 0.0 var g_sum := 0.0 var b_sum := 0.0 if img != null: for y in range(0, mini(band_h, img.get_height())): for x in range(img.get_width()): # 双层全像素扫描(非步长抽样——terrain_tile_sets_test 同款) var c := img.get_pixel(x, y) r_sum += c.r g_sum += c.g b_sum += c.b scan_n += 1 Kit.check("R6 扫描面非空前置(顶带像素 %d > 0——贴图装载且带高在界)〔营地交互批 V5〕" % scan_n, scan_n > 0) if scan_n == 0: return var bg := Color(r_sum / float(scan_n), g_sum / float(scan_n), b_sum / float(scan_n)) # 规格侧算式(camp.md:41 写死——#6E4526 × α0.80 对落位带背景合成) var fg := Color(0x6e / 255.0, 0x45 / 255.0, 0x26 / 255.0, 0.80) var mixed := Color( fg.r * fg.a + bg.r * (1.0 - fg.a), fg.g * fg.a + bg.g * (1.0 - fg.a), fg.b * fg.a + bg.b * (1.0 - fg.a)) var lum_fg: float = _wcag_lum(mixed) var lum_bg: float = _wcag_lum(bg) var ratio: float = (maxf(lum_fg, lum_bg) + 0.05) / (minf(lum_fg, lum_bg) + 0.05) print("[camp_scene_layout_test] 顶带均色=%s 合成后=%s WCAG 对比=%.2f:1(规格 ≥3:1)" % [bg, mixed, ratio]) Kit.check("R6 栏体合成色对落位带底色对比 ≥3.0(实测 %.2f)〔营地交互批 V5〕" % ratio, ratio >= 3.0) ## WCAG 相对亮度(sRGB 线性化——命名函数形态,避多行 lambda 嵌套三元解析坑) func _wcag_lum(c: Color) -> float: return 0.2126 * _srgb_lin(c.r) + 0.7152 * _srgb_lin(c.g) + 0.0722 * _srgb_lin(c.b) func _srgb_lin(v: float) -> float: return v / 12.92 if v <= 0.03928 else pow((v + 0.055) / 1.055, 2.4)