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()