iter47-卡2:卡④(迭代47·营地场景图新建批): 十层素材落地+CampSceneView场景图+layout表fail-fast+四柜台拆解五子界面+大门篝火动线+断言迁移批+审计补差——CampOverlay退役全仓零残留

This commit is contained in:
DKChan committed 2026-11-01 23:44:29 +08:00
1 parent 0761071b37
commit bbeeea77e0
45 files changed
+1614 -822

No files matched your search

@@ -0,0 +1,89 @@
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:
call_deferred("_test_layout_hotspot_geometry")
func _test_layout_hotspot_geometry() -> void:
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:
quit(Kit.finish("camp_scene_layout_test"))
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)
quit(Kit.finish("camp_scene_layout_test"))
@@ -0,0 +1 @@
uid://ckyd57l11408
@@ -15,7 +15,7 @@ func _initialize() -> void:
func _row_by_id(ov) -> Dictionary:
var out := {}
for r in ov._forge._forge_rows:
for r in ov._forge_rows:
out[String(r.route_id)] = r
return out
@@ -30,7 +30,7 @@ func _run() -> void:
var sv = load("res://core/save_manager.gd").new(_TEST_SAVE)
sv.load_from_disk()
var mp = load("res://core/meta_progression.gd").new(db, sv)
var ov = load("res://presentation/views/camp_overlay_view.gd").new()
var ov = load("res://presentation/views/forge_view.gd").new()
root.add_child(ov)
var buys := []
var unlocks := []
@@ -42,16 +42,16 @@ func _run() -> void:
# ① 锁定态:树钥匙未点亮 → 工作间不可见、两区零行板(无任何可交互购买路径)
ov.open_camp({"balance": 999, "forge": mp.forge_shop_status(), "tree": []})
Kit.check("锁定态:工作间不可见(forge_workshop 钥匙未点亮)〔锻造工坊批 V13〕",
not ov._forge._forge_workbench.visible and ov._forge._forge_lock_l.visible)
not ov._forge_workbench.visible and ov._forge_lock_l.visible)
Kit.check("锁定态:两区零行板(占位纪律——零购买路径)〔锻造工坊批 V13〕",
ov._forge._forge_zone_weapon.get_child_count() == 0 and ov._forge._forge_zone_armor.get_child_count() == 0)
ov._forge_zone_weapon.get_child_count() == 0 and ov._forge_zone_armor.get_child_count() == 0)
# ② 解锁态:钥匙点亮 → 两区各三路线
ov.open_camp({"balance": 500, "forge": mp.forge_shop_status(),
"tree": [{"node_id": "forge_workshop", "level": 1}]})
Kit.check("解锁态:工作间可见(钥匙点亮展开)〔锻造工坊批 V13〕", ov._forge._forge_workbench.visible)
Kit.check("解锁态:工作间可见(钥匙点亮展开)〔锻造工坊批 V13〕", ov._forge_workbench.visible)
Kit.check("两区各三路线(武器锻造区 3 + 护甲锻造区 3)〔锻造工坊批 V13〕",
ov._forge._forge_zone_weapon.get_child_count() == 3 and ov._forge._forge_zone_armor.get_child_count() == 3)
ov._forge_zone_weapon.get_child_count() == 3 and ov._forge_zone_armor.get_child_count() == 3)
var rows: Dictionary = _row_by_id(ov)
Kit.check("六路线行册恰全(sword/spear/staff/light/heavy/ward)〔锻造工坊批 V13〕",
rows.has("sword") and rows.has("spear") and rows.has("staff")
@@ -52,7 +52,6 @@ func _init() -> void:
_smoke_diag_button_retired()
_status_panel_projection()
_career_overlay_projection()
_camp_recent_projection()
quit(Kit.finish("theme_projection_test"))
@@ -708,12 +707,6 @@ func _smoke_element_details() -> void:
var shop_src: String = FileAccess.get_file_as_string("res://presentation/views/shop_overlay_view.gd")
Kit.check("T5③ 商店卡片区垂直居中(ALIGNMENT_CENTER)",
shop_src.contains("_cards_box.alignment = BoxContainer.ALIGNMENT_CENTER"))
var camp_src: String = FileAccess.get_file_as_string("res://presentation/views/camp_overlay_view.gd")
var camp_min := camp_src.count("custom_minimum_size = Vector2(0, 336)")
Kit.check("T5③ 营地三 holder 统一 min 高 336(实测 %d 处)" % camp_min, camp_min == 3)
var camp_fill := camp_src.count("size_flags_vertical = Control.SIZE_FILL")
Kit.check("T5③ 营地四栏面板纵向 FILL 等高(实测 %d 处)" % camp_fill, camp_fill == 4)
Kit.check("T5③ 三处锁文垂直居中在位", camp_src.count("SIZE_SHRINK_CENTER") >= 3)
# ---- 诊断转储钮冒烟(角落钮不在玩家画面+转储入口在调试浮窗;V6)----
@@ -796,20 +789,6 @@ func _all_labels(root: Node) -> Array:
return out
# ---- 营地最近冒险行投影(口径换代 V6:完成关数措辞钉子——open_camp 全量重建路径直调;
# 最小档 data=recent 单条 completed 局,其余键缺省走 .get 兜底——与组合根 _camp_data 投影同键面)----
func _camp_recent_projection() -> void:
print("--- 营地·最近冒险行投影(open_camp 最小档)---")
var camp = load("res://presentation/views/camp_overlay_view.gd").new()
camp.open_camp({"balance": 0, "recent": [
{"result": "completed", "levels_cleared": 2, "keystone_total": 5, "master_seed": 42}]})
var recent_text: String = String(camp._side._recent_list.text)
Kit.check("营地最近冒险行完成关数措辞(实际「%s」)〔口径换代 V6〕" % recent_text,
recent_text.contains("完成 2 关") and not recent_text.contains("通关"))
camp.visible = false
camp.free()
# ---- 生涯页只读投影一致性(汇总段六类九项逐行 == SaveManager 计数访问器/
# MetaProgression 访问器读数;投影拼装与 HudPresenter.career_data 同构(同访问器族同序)。
# 只读性连接巡检与零动效断言归 ui_struct_probe 呈现侧,本段只管数据面+旧档载入显示