OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_pointer_wrap.gd

159 lines · gd ref:
extends SceneTree
## GATE — G keeps going past the window edge (hyper-games #109, 2026-10-12: "when
## pressing g and trying to move something a very large distance the mouse has to wrap
## to continue moving beyond the window bounds"). Blender's continuous grab:
## map_dc_level._update_pointer_wrap_at warps the mouse to the opposite edge and adds the
## jump to a virtual-pointer offset that _crosshair_ray reads.
##
## Headless has no OS cursor, so the warp is captured (map.pointer_warp) and the mouse
## position the map would read next is the warp target (its _wrap_pending stand-in).
##
##   W1  wrap_target: right / left / corner wrap, the interior is unchanged, and the
##       landing spot does not wrap straight back
##   W2  Move up, the pointer at the right edge: warped to the left side, offset = the jump
##   W3  the aim ray reads the VIRTUAL pointer (mouse + offset)
##   W4  CONTINUITY: the aim right after the wrap equals the aim right before it
##   W5  moving on past the edge keeps increasing the move
##   W6  the offset is the tool's: cancelling it resets the offset
##   W7  Rotate (drags its angle with the mouse, §17.7 Q2) wraps too
##   W8  play mode never wraps
##
## RUN: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$GODOT" --headless --path . --script res://scenes/test/gate_pointer_wrap.gd

var _frame := 0
var _map: Node = null
var _cam: Camera3D = null
var _warps: Array = []
var fails := 0
var checks := 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 _key(code: int) -> InputEventKey:
	var e := InputEventKey.new()
	e.physical_keycode = code
	e.keycode = code
	e.pressed = true
	return e


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)       # headless is 64x64 (reference_headless-godot-window-64px)
		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
	_run()
	if checks < 12:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE pointer_wrap: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	quit(0 if fails == 0 else 1)
	return true


func _capture(p: Vector2) -> void:
	_warps.append(p)


## The aim along +X from `grab` with the map reading `mouse` as the pointer (the warp
## stand-in) plus its current offset.
func _aim_at(op: Object, grab: Vector3, mouse: Vector2) -> float:
	_map.set("_wrap_pending", mouse)
	return float(op.aim_along(grab, Vector3.RIGHT))


func _run() -> void:
	var W := Vector2(1920, 1080)
	var M: float = _map.get("WRAP_MARGIN")
	var MDL: GDScript = _map.get_script()
	# ---- W1 ----
	var r: Vector2 = MDL.wrap_target(Vector2(1918, 500), W, M)
	var l: Vector2 = MDL.wrap_target(Vector2(2, 500), W, M)
	var c: Vector2 = MDL.wrap_target(Vector2(1919, 1079), W, M)
	_p(r.x < M * 3.0 and r.x > M and r.y == 500.0, "W1 the right edge wraps to the left side", r)
	_p(l.x > W.x - M * 3.0 and l.x < W.x - M, "W1 the left edge wraps to the right side", l)
	_p(c.x < W.x * 0.5 and c.y < W.y * 0.5, "W1 a corner wraps on both axes", c)
	_p(MDL.wrap_target(Vector2(900, 500), W, M) == Vector2(900, 500), "W1 the interior is unchanged")
	_p(MDL.wrap_target(r, W, M) == r and MDL.wrap_target(l, W, M) == l, "W1 the landing spot does not wrap back")
	# ---- setup: a brush in view, G on it ----
	_cam = Camera3D.new()
	root.add_child(_cam)
	_cam.global_position = Vector3(0, 300, 10)
	_cam.make_current()
	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()
	_map.select_brushes(PackedInt32Array([_map.level.brush_by_key(dot)]))
	_map.set("pointer_warp", Callable(self, "_capture"))
	_map._unhandled_key_input(_key(KEY_G))
	var stack: Node = _map.call("_ensure_ops")
	var op: Object = stack.active
	_p(op != null and op.follows_aim(), "setup: G started a pointer-following tool", op)
	if op == null:
		return
	var grab := Vector3(0, 300, 0)
	# ---- W2 ---- (at the 3D VIEW's right edge since #124: the window's edges are areas)
	var rv: Rect2 = root.get_node("/root/BuildDock").view_rect()
	var at_edge := Vector2(rv.end.x - 2.0, 540)
	_map.call("_update_pointer_wrap_at", at_edge)   # first call adopts the tool
	var off: Vector2 = _map.get("_wrap_offset")
	_p(_warps.size() == 1 and (_warps[0] as Vector2).x < rv.position.x + M * 3.0, "W2 warped to the 3D view's left side", [_warps, rv])
	_p(is_equal_approx(off.x, at_edge.x - (_warps[0] as Vector2).x if _warps.size() > 0 else -1.0), "W2 the offset is the jump", off)
	# ---- W3 / W4 ----
	var before: float
	_map.set("_wrap_offset", Vector2.ZERO)
	before = _aim_at(op, grab, at_edge)               # where W2's wrap started
	_map.set("_wrap_offset", off)
	var after := _aim_at(op, grab, _warps[0] as Vector2)
	var ray: Dictionary = _map.call("_crosshair_ray")
	var want: Vector3 = _cam.project_ray_normal((_warps[0] as Vector2) + off)
	_p((ray.get("dir", Vector3.ZERO) as Vector3).is_equal_approx(want), "W3 the ray reads mouse + offset", ray.get("dir"))
	_p(absf(after - before) < 1.0e-3, "W4 no jump: the aim right after the wrap = right before", "%.4f vs %.4f" % [before, after])
	# ---- W5 ----
	var further := _aim_at(op, grab, (_warps[0] as Vector2) + Vector2(400, 0))
	_p(further > after + 0.5, "W5 moving on past the edge keeps increasing the move", "%.3f -> %.3f" % [after, further])
	# ---- W6 ----
	stack.cancel()
	_map.call("_update_pointer_wrap_at", Vector2(900, 540))
	_p((_map.get("_wrap_offset") as Vector2) == Vector2.ZERO, "W6 cancelling the tool resets the offset", _map.get("_wrap_offset"))
	# ---- W7 ----
	_warps.clear()
	_map._unhandled_key_input(_key(KEY_R))
	var rot: Object = stack.active
	_map.call("_update_pointer_wrap_at", Vector2(1918, 540))
	# CHANGED 2026-10-20 (#120 §17.7 Q2): Rotate DRAGS its angle with the mouse now, so it
	# wraps like Blender's continuous grab. It used to be the tool that never wrapped.
	_p(rot != null and not rot.follows_aim() and rot.drags_value() and not _warps.is_empty(),
		"W7 Rotate (drags its angle) wraps too", rot)
	stack.cancel()
	# ---- W8 ----
	_warps.clear()
	_map._unhandled_key_input(_key(KEY_G))
	root.get_node("/root/GameMode").set_mode("play")
	_map.call("_update_pointer_wrap_at", Vector2(1918, 540))
	_p(_warps.is_empty(), "W8 play mode never wraps", _warps)
	root.get_node("/root/GameMode").set_mode("build")
	stack.cancel()