extends Node
## GATE — THE ASSET BROWSER (#120 L4 step 5, docs/node-tree-and-stacks.md §15.5,
## asset_browser.gd). "Kit" saved to both libraries, "Solo" to yours only.
## W1 the map registers an "Assets" strip in the BuildDock
## W2 drop_frame: floor stands (up +y), wall sticks out (up = the wall's axis, x level),
## ceiling hangs (up -y), a slope takes its nearest axis; always right-handed
## W3 tiles: one per asset; Room shows only Kit, Mine both, a search narrows
## W4 a tile click makes it the CURRENT asset
## W5 Shift+A → Asset (place_current_asset): at the 3D cursor, standing, selected
## W6 a drop on a wall (place_asset_hit): the placement's up is the wall's axis
## W7 the "Save to" switches set what Save as asset writes
## W8 a tile with a placement on an older version offers "Update 1"; pressing it updates
## it and the button goes
## W9 arm / disarm: the 3D view takes drops only while a tile's drag is in flight
## W10 the Add menus list "Asset: place the current one…" (menu-only)
##
## RUN AS A SCENE: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . res://scenes/test/gate_asset_browser.tscn
const AssetLibrary := preload("res://scenes/maps/asset_library.gd")
var fails := 0
var checks := 0
var map: Node3D
var persist: Node
var lib: Node
var ab: Node
func _ok(what: String, cond: bool, note: Variant = "") -> void:
checks += 1
if not cond:
fails += 1
print(" %-64s %s %s" % [what, "OK" if cond else "**FAIL**", str(note)])
func _ready() -> void:
var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
gs.route("world:level:*", 1, "peer")
var keet: Node = get_tree().root.get_node_or_null("/root/KeetService")
if keet != null:
keet.current_member_id = "probe"
get_tree().root.get_node("/root/GameMode").set_mode("build")
var pd := AssetLibrary.personal_dir()
if pd.ends_with("_ci"):
for f: String in DirAccess.get_files_at(pd):
DirAccess.remove_absolute(ProjectSettings.globalize_path(pd.path_join(f)))
map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
add_child(map)
for i in 6:
await get_tree().process_frame
persist = map.get("_persist")
lib = map.asset_library()
ab = map.get("_asset_browser")
_run()
if checks < 20:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE asset_browser: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _box(at: Vector3, size: Vector3) -> String:
return persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, at)), "size": [size.x, size.y, size.z],
"material_id": 2, "level_id": 0})
func _frame_ok(f: Transform3D, up: Vector3) -> bool:
var b := f.basis
return b.y.is_equal_approx(up) and is_equal_approx(b.determinant(), 1.0) \
and is_zero_approx(b.x.dot(b.y)) and is_zero_approx(b.y.dot(b.z)) and is_zero_approx(b.x.dot(b.z))
func _names() -> Array:
return (ab.shown_rows() as Array).map(func(r: Dictionary) -> String: return String(r["name"]))
func _run() -> void:
var dock: Node = get_tree().root.get_node("BuildDock")
_ok("W1 the map registers an Assets strip", ab != null and dock.has_panel("assets"))
if ab == null:
return
# ---- W2 ----
var MD: Script = map.get_script()
_ok("W2 floor: stands (up +y)", _frame_ok(MD.drop_frame(Vector3.ZERO, Vector3.UP), Vector3.UP))
var wall: Transform3D = MD.drop_frame(Vector3.ZERO, Vector3(-1, 0, 0))
_ok("W2 wall: up = the wall's axis, x level", _frame_ok(wall, Vector3(-1, 0, 0)) and is_zero_approx(wall.basis.x.y), wall.basis)
_ok("W2 ceiling: hangs (up -y)", _frame_ok(MD.drop_frame(Vector3.ZERO, Vector3.DOWN), Vector3.DOWN))
_ok("W2 a slope takes its nearest axis", _frame_ok(MD.drop_frame(Vector3.ZERO, Vector3(0.3, 0.9, 0).normalized()), Vector3.UP)
and _frame_ok(MD.drop_frame(Vector3.ZERO, Vector3(-0.2, 0.1, -0.95).normalized()), Vector3(0, 0, -1)))
_ok("W2 the frame stands ON the hit", MD.drop_frame(Vector3(3, 4, 5), Vector3.UP).origin == Vector3(3, 4, 5))
var O0 := Vector3(900.137, 0.31, 899.873)
persist.begin_gesture("room")
var a := _box(O0, Vector3(2, 2, 2))
var kit: String = persist.record("col.create", {"name": "Kit", "parent": ""})
persist.record("col.link", {"target": kit, "members": ["g:" + a]})
var s := _box(O0 + Vector3(0, 0, -20), Vector3(1, 1, 1))
var solo: String = persist.record("col.create", {"name": "Solo", "parent": ""})
persist.record("col.link", {"target": solo, "members": ["g:" + s]})
persist.end_gesture()
persist.refold()
lib.save_collection(kit, "Kit")
lib.save_collection(solo, "Solo", false, true)
# ---- W3 ----
ab.refresh()
_ok("W3 a tile per asset", ab.tile("Kit") != null and ab.tile("Solo") != null, _names())
ab.set_filter("", "room")
_ok("W3 Room: only Kit", _names() == ["Kit"], _names())
ab.set_filter("", "personal")
_ok("W3 Mine: both", _names() == ["Kit", "Solo"], _names())
ab.set_filter("so", "both")
_ok("W3 a search narrows", _names() == ["Solo"], _names())
ab.set_filter("", "both")
# ---- W4 ----
ab.tile("Kit").pressed.emit()
_ok("W4 a tile click makes it current", String(map.get("current_asset")) == "Kit" and ab.tile("Kit").button_pressed)
# ---- W5 ----
var cur: Node3D = map.get("_cursor3d")
if cur != null:
cur.global_position = O0 + Vector3(10, 0, 10)
var p5: String = map.place_current_asset()
var f5: Transform3D = (persist.placements() as Dictionary).get(p5, {}).get("frame", Transform3D())
_ok("W5 Shift+A → Asset: at the 3D cursor, standing, selected", p5 != "" and cur != null
and f5.origin.is_equal_approx(cur.global_position) and _frame_ok(f5, Vector3.UP) and map.selected_asset() == p5, [p5, f5.origin])
# ---- W6 ----
var p6: String = map.place_asset_hit("Solo", {"position": O0 + Vector3(0, 2, 30), "normal": Vector3(0, 0, 1)})
var f6: Transform3D = (persist.placements() as Dictionary).get(p6, {}).get("frame", Transform3D())
_ok("W6 a drop on a wall: up is the wall's axis", p6 != "" and _frame_ok(f6, Vector3(0, 0, 1)), f6.basis)
_ok("W6 ... and a personal-only asset was published to the room first",
String(get_tree().root.get_node("AssetSync").resolve("asset", "Solo")) != "")
# ---- W7 ----
var rb: CheckBox = ab.get("_room_box")
var pb: CheckBox = ab.get("_personal_box")
rb.button_pressed = false
pb.button_pressed = false
_ok("W7 the Save to switches set what Save as asset writes", not bool(map.get("asset_save_room"))
and not bool(map.get("asset_save_personal")))
rb.button_pressed = true
pb.button_pressed = true
# ---- W8 ----
var gs: Node = get_tree().root.get_node("GameState")
var oseq := int(p5.get_slice(":", 1)) + 9
var row_kit: Dictionary = {}
for r: Dictionary in (lib.rows() as Array):
if String(r["name"]) == "Kit":
row_kit = r
gs._on_durable_updated("", "map:%s:world:level:zz9other:op:%d" % [String(gs.current_map_id), oseq], {"v": LevelPersistence.RECORD_V,
"kind": "asset.place", "undone": false, "g": "zz9other:%d" % oseq, "src": String(persist.find_source().get("hash", "")),
"asset": row_kit["oid"], "name": "Kit", "frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, O0 + Vector3(0, 0, 50)))})
persist.refold()
persist.begin_gesture("edit")
persist.record("shape.size", {"target": a, "size": [3, 2, 2]})
persist.end_gesture()
persist.refold()
lib.save_collection(kit, "Kit")
ab.refresh()
var upd: Button = null
for c: Node in (ab.tile("Kit").get_parent().get_children()):
if c is Button and String((c as Button).text).begins_with("Update"):
upd = c
_ok("W8 a placement on an older version: the tile offers 'Update 1'", upd != null and upd.text == "Update 1",
upd.text if upd != null else null)
if upd != null:
upd.pressed.emit()
ab.refresh()
var still := false
for c2: Node in (ab.tile("Kit").get_parent().get_children()):
if c2 is Button and String((c2 as Button).text).begins_with("Update"):
still = true
_ok("W8 ... pressing it updates them and the button goes", not still)
# ---- W9 ----
var v: Control = dock.view3d()
_ok("W9 the 3D view ignores the mouse when no tile is dragged", v.mouse_filter == Control.MOUSE_FILTER_IGNORE)
ab.arm(true)
_ok("W9 armed while a tile's drag is in flight", v.mouse_filter == Control.MOUSE_FILTER_PASS and ab.armed())
# == false, not `not …`: a crashing call returns null, and `not null` passed.
var took: Variant = ab.call("_view_can_drop", Vector2.ZERO, {"something": 1})
_ok("W9 ... it takes only an asset tile's data", took is bool and took == false, took)
ab.arm(false)
_ok("W9 disarmed after", v.mouse_filter == Control.MOUSE_FILTER_IGNORE and not ab.armed())
# ---- W10 ----
var add_rows: Array = []
for sec: Array in (map.add_sections() as Array):
for r2: Array in (sec[1] as Array):
add_rows.append(String(r2[0]))
var bar_add: Array = []
for m: Array in load("res://scenes/game/editor_top_bar.gd").MENUS:
if String(m[0]) == "Add":
bar_add = m[1]
_ok("W10 Add menus list Asset: place the current one (menu-only)", add_rows.has("builder.place_asset")
and bar_add.has("builder.place_asset") and not Keymap.binding_of("builder.place_asset").has("key"))