extends SceneTree
## GATE — selection reaches as far as you can SEE (hyper-games #109, 2026-10-12: "when
## working on big maps, it seems there is a distance limit to ray based selection,
## needs to be much bigger").
##
## Both aim paths stopped at 50 m — _pick_object (scene objects) and _crosshair_hit
## (brushes, faces, placement, carve) — a number from the first K-carve commit
## (4833f1d) that nothing depends on. In BUILD MODE the reach is now the camera's far
## plane: what is drawn can be clicked. Play mode and carving keep 50 m (user).
##
## 0. controls: the same object and brush from 30 m are hit (the rig aims right)
## 1. an object 120 m away is picked
## 2. a brush 130 m away is hit by the aim ray
## 3. an object beyond the camera's far plane is NOT picked
## 4. the carve ray (PLAY_REACH) still stops at 50 m in build mode
## 5. in play mode neither path reaches 120-130 m
##
## RUN: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$GODOT" --headless --path . --script res://scenes/test/gate_pick_range.gd
var _frame := 0
var _map: Node = null
var _cam: Camera3D = null
var _far_dot := ""
var _beyond_dot := ""
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 _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.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:
_setup()
return false
if _frame < 40:
return false
_run()
if checks < 7:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE pick_range: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
quit(0 if fails == 0 else 1)
return true
## A camera high above the map looking along -Z, so nothing the map owns is in the way;
## the targets sit on that line.
func _setup() -> void:
_cam = Camera3D.new()
_cam.far = 400.0
root.add_child(_cam)
_cam.global_position = Vector3(0, 300, 0)
_cam.make_current()
_far_dot = _map.create_object("marker", Transform3D(Basis.IDENTITY, Vector3(0, 300, -120)))
_beyond_dot = _map.create_object("marker", Transform3D(Basis.IDENTITY, Vector3(0, 300, -450)))
var persist: Node = _map.get("_persist")
persist.record("brush.create", {"planes": _box(Vector3(-4, 296, -134), Vector3(4, 304, -130)),
"proj": [], "material_id": 0, "level_id": 0, "sky": false})
persist.refold()
func _run() -> void:
var at: Vector2 = root.get_visible_rect().size * 0.5
# CONTROL: the same object from 30 m — proves the rig aims at it, so a failure at
# 120 m is the range and nothing else.
_cam.global_position = Vector3(0, 300, -90)
var near: Dictionary = _map.call("_pick_object", at)
_p(String(near.get("dot", "")) == _far_dot, "0. control: the object from 30 m is picked", near)
_cam.global_position = Vector3(0, 300, 0)
var picked: Dictionary = _map.call("_pick_object", at)
_p(_far_dot != "" and String(picked.get("dot", "")) == _far_dot, "1. an object 120 m away is picked", picked)
# The near marker is in front of the brush on this line; hide it so the ray reaches it.
(_map.object_node(_far_dot) as Node3D).visible = false
_cam.global_position = Vector3(0, 300, -100)
var hit0: Dictionary = _map.call("_crosshair_hit", at)
_p(absf((hit0.get("position", Vector3.ZERO) as Vector3).z + 130.0) < 0.5,
"0. control: the brush from 30 m is hit", hit0.get("position", "miss"))
_cam.global_position = Vector3(0, 300, 0)
var hit: Dictionary = _map.call("_crosshair_hit", at)
var hp: Vector3 = hit.get("position", Vector3.ZERO)
_p(not hit.is_empty() and absf(hp.z + 130.0) < 0.5, "2. a brush 130 m away is hit by the aim ray", hp)
# BUILD MODE ONLY (user, 2026-10-12: "larger ray hits for edit mode ONLY, the carve
# limits in game should stay the same"). Carving keeps the old reach in every mode.
var carve: Dictionary = _map.call("_crosshair_hit", at, _map.get("PLAY_REACH"))
_p(carve.is_empty(), "4. the carve ray still stops at PLAY_REACH (50 m) in build mode", carve.get("position", "miss"))
root.get_node("/root/GameMode").set_mode("play")
var play_hit: Dictionary = _map.call("_crosshair_hit", at)
(_map.object_node(_far_dot) as Node3D).visible = true
var play_pick: Dictionary = _map.call("_pick_object", at)
_p(play_hit.is_empty() and play_pick.is_empty(), "5. in play mode neither reaches 120-130 m",
"%s / %s" % [play_hit.get("position", "miss"), play_pick.get("dot", "miss")])
root.get_node("/root/GameMode").set_mode("build")
# From z=-200 the brush and the near marker are behind the camera; the other marker is
# 250 m ahead. With the far plane at 200 m it is not drawn, so it must not be picked.
_cam.global_position = Vector3(0, 300, -200)
_cam.far = 200.0
var beyond: Dictionary = _map.call("_pick_object", at)
_p(String(beyond.get("dot", "")) != _beyond_dot, "3. an object 250 m away, past a 200 m far plane, is not picked", beyond)