OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_editor_view.gd

184 lines · gd ref:
extends SceneTree
## GATE — orthographic camera + Blender's numpad views (hyper-games #109;
## docs/editor-ortho-view.md §5). Run on a bare camera AND on the real player rig
## (yaw on CameraHead, pitch on CameraPivot, the body moved so the eye lands right).
##
##   V1  Numpad 5: ortho ON with the same visible height at the pivot; OFF again
##   V2  front: looks -Z, the pivot stays on the screen centre at the same distance,
##       and auto-perspective turned ortho on
##   V3  top: looks -Y with -Z up on screen
##   V4  an orbit from that AUTO ortho view returns to perspective; it turns 15° about
##       the pivot (the pivot stays centred)
##   V5  a MANUAL ortho (5) survives an orbit
##   V6  9 from front = back (looks +Z), still an auto-ortho axis view
##   V7  Numpad . frames the selection: centred, filling FRAME_FILL of the view
##   V8  the wheel zooms the ortho height
##   V9  picking in ortho hits the brush under the screen centre
##   V10 leaving build mode restores perspective
##   R1-R3  the player rig: front and top views pose the player's camera (head yaw +
##          pivot pitch), the eye at pivot - forward * distance
##
## RUN: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$GODOT" --headless --path . --script res://scenes/test/gate_editor_view.gd

# NOT preloaded: a --script gate compiles before the autoloads exist, and editor_view.gd
# names GameMode — its constants are read off the live node instead.

var _frame := 0
var _map: Node = null
var _view: Node = null
var _cam: Camera3D = null
var fails := 0
var checks := 0
var _brush := -1


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 _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:
		return false
	if _frame == 30:
		_bare()
		return false
	if _frame == 31:
		_rig()
		return false
	if checks < 18:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE editor_view: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	quit(0 if fails == 0 else 1)
	return true


func _centre_dir() -> Vector3:
	var c: Camera3D = root.get_viewport().get_camera_3d()
	return c.project_ray_normal(root.get_visible_rect().size * 0.5)


func _bare() -> void:
	_view = _map.get("_view")
	_p(_view != null, "setup: the map has an EditorView")
	if _view == null:
		return
	var persist: Node = _map.get("_persist")
	var dot: String = persist.record("brush.create", {"planes": _box(Vector3(-1, 299, -1), Vector3(1, 301, 1)),
		"proj": [], "material_id": 0, "level_id": 0, "sky": false})
	persist.refold()
	_brush = _map.level.brush_by_key(dot)
	_map.select_brushes(PackedInt32Array([_brush]))
	_cam = Camera3D.new()
	root.add_child(_cam)
	_cam.global_transform = Transform3D(Basis.looking_at(Vector3(0, -0.3, -1).normalized()), Vector3(0, 303, 10))
	_cam.make_current()
	var pv: Vector3 = _view.pivot()
	_p(pv.is_equal_approx(Vector3(0, 300, 0)), "setup: the pivot is the selection's centre", pv)
	var d0 := _cam.global_position.distance_to(pv)
	# ---- V1 ----
	_view.run("toggle")
	var want_h := 2.0 * d0 * tan(deg_to_rad(_cam.fov) * 0.5)
	_p(_view.is_ortho() and absf(_cam.size - want_h) < 1e-3, "V1 5: ortho, same visible height at the pivot",
		"%.3f vs %.3f" % [_cam.size, want_h])
	_view.run("toggle")
	_p(not _view.is_ortho(), "V1 5 again: perspective")
	# ---- V2 ----
	_view.run("front")
	_p(_view.forward().is_equal_approx(Vector3(0, 0, -1)), "V2 front looks -Z", _view.forward())
	_p(absf(_cam.global_position.distance_to(pv) - d0) < 1e-3 and _centre_dir().dot((pv - _cam.global_position).normalized()) > 0.9999,
		"V2 the pivot stays centred at the same distance", _cam.global_position)
	_p(_view.is_ortho() and bool(_view.get("auto_ortho")), "V2 auto-perspective turned ortho on")
	# ---- V3 ----
	_view.run("top")
	_p(_view.forward().is_equal_approx(Vector3(0, -1, 0)) and _view.up().is_equal_approx(Vector3(0, 0, -1)),
		"V3 top looks -Y, -Z up on screen", "%s / %s" % [_view.forward(), _view.up()])
	# ---- V4 ----
	_view.run("front")
	var f0: Vector3 = _view.forward()
	_view.run("orbit_right")
	var ang := rad_to_deg(f0.angle_to(_view.forward()))
	_p(not _view.is_ortho(), "V4 orbiting out of an AUTO ortho view returns to perspective")
	_p(absf(ang - 15.0) < 0.01 and _centre_dir().dot((pv - _cam.global_position).normalized()) > 0.9999,
		"V4 it turned 15° about the pivot (still centred)", "%.3f°" % ang)
	# ---- V5 ----
	_view.run("toggle")
	_view.run("orbit_left")
	_p(_view.is_ortho(), "V5 a MANUAL ortho survives an orbit")
	_view.run("toggle")
	# ---- V6 ----
	_view.run("front")
	_view.run("opposite")
	_p(_view.forward().is_equal_approx(Vector3(0, 0, 1)) and _view.is_ortho(), "V6 9 from front = back, still ortho",
		_view.forward())
	_view.run("toggle")
	# ---- V7 ----
	_view.run("selected")
	var box: AABB = _view.selection_bounds()
	var r: float = box.size.length() * 0.5
	var want_d: float = r / float(_view.get("FRAME_FILL")) / tan(deg_to_rad(_cam.fov) * 0.5)
	_p(_centre_dir().dot((box.get_center() - _cam.global_position).normalized()) > 0.9999
		and absf(_cam.global_position.distance_to(box.get_center()) - want_d) < 1e-3,
		"V7 . frames the selection (centred, %.2f m)" % want_d, _cam.global_position.distance_to(box.get_center()))
	# ---- V8 ----
	_view.run("toggle")
	var s0 := _cam.size
	_view.zoom(1)
	_p(absf(_cam.size - s0 / float(_view.get("ZOOM_FACTOR"))) < 1e-4, "V8 the wheel zooms the ortho height", "%.3f -> %.3f" % [s0, _cam.size])
	# ---- V9 ----
	var hit: Dictionary = _map.call("_crosshair_hit", root.get_visible_rect().size * 0.5)
	_p(not hit.is_empty() and _map.call("_brush_from_hit", hit) == _brush, "V9 picking in ortho hits the brush at the centre",
		hit.get("position", "miss"))
	# ---- V10 ----
	root.get_node("/root/GameMode").set_mode("play")
	_p(not _view.is_ortho(), "V10 leaving build mode restores perspective")
	root.get_node("/root/GameMode").set_mode("build")
	_cam.queue_free()


## The real player scene: its camera posed through head yaw + pivot pitch.
func _rig() -> void:
	var ps: PackedScene = load("res://scenes/player/player_procedural.tscn")
	var pl: Node3D = ps.instantiate() if ps != null else null
	if pl == null:
		_p(false, "R setup: the player scene loads")
		return
	root.add_child(pl)
	pl.global_position = Vector3(0, 303, 12)
	var c: Camera3D = pl.get("camera")
	(pl.get("spring_arm") as SpringArm3D).spring_length = 0.0
	c.make_current()
	var pv: Vector3 = _view.pivot()
	var d0 := c.global_position.distance_to(pv)
	_view.run("front")
	var f := -c.global_transform.basis.z
	_p(f.is_equal_approx(Vector3(0, 0, -1)), "R1 the player's camera looks -Z (head yaw + pivot pitch)", f)
	_p(c.global_position.is_equal_approx(pv - f * d0), "R2 the eye is at pivot - forward * distance (the body moved)",
		"%s vs %s" % [c.global_position, pv - f * d0])
	_view.run("top")
	var f2 := -c.global_transform.basis.z
	_p(f2.is_equal_approx(Vector3(0, -1, 0)) and c.global_transform.basis.y.is_equal_approx(Vector3(0, 0, -1)),
		"R3 top on the rig: looks -Y, -Z up", "%s / %s" % [f2, c.global_transform.basis.y])
	_view.run("toggle")
	pl.queue_free()