OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_editor_cursor.gd

208 lines · gd ref:
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)