extends SceneTree
## GATE — the 3D cursor core (hyper-games #109; docs/editor-3d-cursor.md).
##
## C1 Shift+RMB places it on the surface under the pointer, oriented to the view
## C2 Shift+RMB does NOT start the look; plain RMB still does
## C3 a Shift+RMB drag slides it and saves once, on release (not per motion)
## C4 to selected = the MEDIAN of the selected brushes' centres
## C5 to grid lands on the lattice; to origin; to active
## C6 a menu-only action (no key) has no InputMap event, and Keymap.run still runs it
## C7 Shift+S with no other movement key opens the menu with the 3D cursor section;
## with move_forward held it does not
## C8 a fresh cursor on the same map loads the saved transform
## C9 hidden in play mode
##
## RUN: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$GODOT" --headless --path . --script res://scenes/test/gate_editor_cursor.gd
var _frame := 0
var _map: Node = null
var _cur: Node3D = null
var _cam: Camera3D = null
var fails := 0
var checks := 0
var _a := -1
var _b := -1
var _saves := 0
func _p(c: bool, m: String, got: Variant = "") -> void:
checks += 1
if not c:
fails += 1
print(" %-66s %s %s" % [m, "OK" if c else "**FAIL**", str(got)])
func _box(lo: Vector3, hi: Vector3) -> Array:
return [1, 0, 0, hi.x, -1, 0, 0, -lo.x, 0, 1, 0, hi.y, 0, -1, 0, -lo.y, 0, 0, 1, hi.z, 0, 0, -1, -lo.z]
func _rmb(pressed: bool, shift: bool, at: Vector2) -> void:
var e := InputEventMouseButton.new()
e.button_index = MOUSE_BUTTON_RIGHT
e.pressed = pressed
e.shift_pressed = shift
e.position = at
e.global_position = at
root.push_input(e)
func _motion(at: Vector2) -> void:
var m := InputEventMouseMotion.new()
m.position = at
m.global_position = at
root.push_input(m)
func _key(code: int, shift := false) -> void:
for p: bool in [true, false]:
var e := InputEventKey.new()
e.keycode = code
e.physical_keycode = code
e.shift_pressed = shift
e.pressed = p
root.push_input(e)
var _step := 0
func _process(_d: float) -> bool:
_frame += 1
if _frame == 1:
root.size = Vector2i(1920, 1080)
root.get_node("/root/GameMode").set_mode("build")
var keet: Node = root.get_node_or_null("/root/KeetService")
if keet != null:
keet.current_member_id = "gate-author"
var gs: Node = root.get_node_or_null("/root/GameState")
if gs != null:
gs.route("world:level:*", 1, "peer")
_map = (load("res://scenes/maps/map_flat.tscn") as PackedScene).instantiate()
root.add_child(_map)
return false
if _frame < 30 or _frame % 3 != 0:
return false
var gm: Node = root.get_node("/root/GameMode")
var centre := Vector2(960, 540)
match _step:
0: _setup()
1: _rmb(true, true, centre) # Shift+RMB press
2:
_p(_cur.global_position.distance_to(Vector3(0, 301, 0)) < 0.05,
"C1 Shift+RMB places it on the surface under the pointer", _cur.global_position)
_p(_cur.global_transform.basis.is_equal_approx(_cam.global_transform.basis.orthonormalized()),
"C1 oriented to the view")
_p(not bool(gm.get("_temp_look")), "C2 Shift+RMB did not start the look")
var dock: Node = root.get_node("/root/BuildDock")
_saves = 1 if dock.pref(String(_cur.call("_pref_key")), null) != null else 0
dock.set("_prefs", {}) # forget it: the drag must not write
_motion(centre + Vector2(150, 0))
3:
var dock2: Node = root.get_node("/root/BuildDock")
_p(_cur.global_position.x > 0.5 and dock2.pref(String(_cur.call("_pref_key")), null) == null,
"C3 the drag slides it and writes nothing per motion", _cur.global_position)
_rmb(false, true, centre + Vector2(150, 0))
4:
var dock3: Node = root.get_node("/root/BuildDock")
_p(dock3.pref(String(_cur.call("_pref_key")), null) != null, "C3 the release saves it")
_rmb(true, false, centre) # plain RMB
5:
_p(bool(gm.get("_temp_look")), "C2 plain RMB still starts the look")
_rmb(false, false, centre)
6:
# That RMB press + release was a TAP — it opened the context menu, whose `menu`
# context holds every builder key. Close it, or C6 / C7 test nothing.
var tap_menu: Node = _map.get("_context_menu")
if tap_menu != null and tap_menu.is_open():
(tap_menu.get("_menu") as PopupMenu).hide()
_p(tap_menu == null or not tap_menu.is_open(), "setup: the RMB-tap context menu is closed again")
# ---- C4 / C5 ----
_map.select_brushes(PackedInt32Array([_a, _b]))
_cur.call("to_selected")
_p(_cur.global_position.is_equal_approx(Vector3(5, 300.5, 0)), "C4 to selected = the median of the centres",
_cur.global_position)
_cur.global_position = Vector3(0.37, 299.91, 0.13)
_cur.call("to_grid")
# On the lattice = snapping it again changes nothing, on every axis.
var on := not _cur.global_position.is_equal_approx(Vector3(0.37, 299.91, 0.13))
var units: float = _map.call("grid_units")
for i in 3:
var axis: Vector3 = [Vector3.RIGHT, Vector3.UP, Vector3.BACK][i]
var again: float = _map.snap_to_map_grid(_cur.global_position[i], axis, units,
_map.get("_map_center"), _map.get("_aligned_offset"))
on = on and absf(again - _cur.global_position[i]) < 1e-4
_p(on, "C5 to grid lands on the lattice", _cur.global_position)
_map.select_brushes(PackedInt32Array([_b]))
_cur.call("to_active")
_p(_cur.global_position.is_equal_approx(Vector3(10, 300.5, 0)), "C5 to active = the active brush's centre",
_cur.global_position)
# ---- C6 ----
_p(InputMap.has_action("cursor.to_origin") and InputMap.action_get_events("cursor.to_origin").is_empty(),
"C6 a menu-only action has no InputMap event")
root.get_node("/root/Keymap").run("cursor.to_origin")
7:
_p(_cur.global_position.is_zero_approx(), "C6 Keymap.run still runs it (to origin)", _cur.global_position)
_key(KEY_S, true)
8:
var cm: Node = _map.get("_context_menu")
_p(cm != null and bool(cm.is_open()) and bool(cm.has_action("cursor.to_selected")),
"C7 Shift+S (not moving) opens the 3D cursor menu")
if cm != null and cm.is_open():
(cm.get("_menu") as PopupMenu).hide()
Input.action_press("move_forward")
9:
_key(KEY_S, true)
10:
var cm2: Node = _map.get("_context_menu")
_p(cm2 == null or not bool(cm2.is_open()), "C7 with move_forward held, Shift+S does not open it")
Input.action_release("move_forward")
# ---- C8 ----
_cur.global_position = Vector3(3, 301, -2)
_cur.call("_save")
var c2 := Node3D.new()
c2.set_script(load("res://scenes/game/editor_cursor.gd"))
root.add_child(c2)
c2.call("setup", _map)
_p(c2.global_position.is_equal_approx(Vector3(3, 301, -2)), "C8 a fresh cursor loads the saved transform",
c2.global_position)
c2.queue_free()
# ---- C9 ----
gm.set_mode("play")
_p(not _cur.visible, "C9 hidden in play mode")
gm.set_mode("build")
_:
if checks < 17:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE editor_cursor: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
quit(0 if fails == 0 else 1)
return true
_step += 1
return false
func _setup() -> void:
# A fresh profile shows BuildHud's first-run card at the screen centre, and it takes
# clicks until "Got it" — dismissed here, as every returning player has.
var hud: Node = root.get_node_or_null("/root/BuildHud")
if hud != null and hud.has_method("dismiss_first_run"):
hud.dismiss_first_run()
_cur = _map.get("_cursor3d")
_p(_cur != null, "setup: the map has a 3D cursor")
_cam = Camera3D.new()
root.add_child(_cam)
_cam.global_transform = Transform3D(Basis.looking_at(Vector3(0, -1, -0.0001).normalized()), Vector3(0, 310, 0))
_cam.make_current()
var persist: Node = _map.get("_persist")
# A floor slab under the camera (top at y=301) and two boxes for the selection checks.
for spec: Array in [[Vector3(-5, 300, -5), Vector3(5, 301, 5)]]:
persist.record("brush.create", {"planes": _box(spec[0], spec[1]), "proj": [], "material_id": 0,
"level_id": 0, "sky": false})
var da: String = persist.record("brush.create", {"planes": _box(Vector3(-1, 300, -1), Vector3(1, 301, 1)),
"proj": [], "material_id": 0, "level_id": 0, "sky": false})
var db: String = persist.record("brush.create", {"planes": _box(Vector3(9, 300, -1), Vector3(11, 301, 1)),
"proj": [], "material_id": 0, "level_id": 0, "sky": false})
persist.refold()
_a = _map.level.brush_by_key(da)
_b = _map.level.brush_by_key(db)