extends Node
## GATE — HANDLES INSIDE A TOOL (#120 §17.9, §17.8 step 4).
## T1 BoxCutter, drawn: one depth dot (ModalControls.handle_points)
## T2 a press on it GRABS it (no confirm): the tool stays up, nothing written
## T3 dragging it 3 grid steps up makes the cut 3 steps shallower; the release lets go
## and still writes nothing
## T4 Enter then writes the cut at that depth
## T5 a press AWAY from every dot confirms, as before
## T6 the cable tool: the slack dot dragged 0.5 m down sets slack = 8 d^2 / (3 L)
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_tool_handles.tscn
var fails := 0
var checks := 0
var map: Node3D
var persist: Node
var cam: Camera3D
func _ok(what: String, cond: bool, note: Variant = "") -> void:
checks += 1
if not cond:
fails += 1
print(" %-64s %s %s" % [what, "OK" if cond else "**FAIL**", str(note)])
func _ready() -> void:
var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
gs.route("world:level:*", 1, "peer")
var keet: Node = get_tree().root.get_node_or_null("/root/KeetService")
if keet != null:
keet.current_member_id = "gate-author"
get_tree().root.get_node("/root/GameMode").set_mode("build")
get_tree().root.size = Vector2i(1920, 1080) # headless 64x64 is all panels
map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
add_child(map)
cam = Camera3D.new()
add_child(cam)
cam.current = true
for i in 6:
await get_tree().process_frame
await _run()
get_tree().root.get_node("/root/GameMode").set_mode("play")
if checks < 9:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE tool_handles: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _point(p: Vector2) -> void:
map.set("_wrap_offset", p - get_viewport().get_mouse_position())
var ev := InputEventMouseMotion.new()
ev.position = p
map._input(ev)
await get_tree().process_frame
func _button(pressed: bool) -> void:
var mb := InputEventMouseButton.new()
mb.button_index = MOUSE_BUTTON_LEFT
mb.pressed = pressed
mb.position = get_viewport().get_mouse_position() + (map.get("_wrap_offset") as Vector2)
map._input(mb)
await get_tree().process_frame
func _n(kind: String) -> int:
return (persist.records(false) as Array).filter(func(r: Dictionary) -> bool:
return String(r.get("kind", "")) == kind and not bool(r.get("undone", false))).size()
func _last(kind: String) -> Dictionary:
var best := {}
for r: Dictionary in (persist.records(false) as Array):
if String(r.get("kind", "")) == kind and (best.is_empty() or int(r["seq"]) > int(best["seq"])):
best = r
return best
func _run() -> void:
persist = map.get("_persist")
var ops: Node = map.call("_ensure_ops")
var B := Vector3(300.0, 0.0, 300.0)
persist.begin_gesture("target")
var t: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, B)), "size": [4, 2, 4],
"material_id": 2, "level_id": 0})
persist.end_gesture()
persist.refold()
# From the side and above: the top is visible (BoxCutter starts only aimed at a surface)
# and the depth axis (down) is vertical on screen.
cam.global_position = B + Vector3(2.0, 6.0, 12.0)
cam.look_at(B + Vector3(2.0, 1.0, 2.0), Vector3.UP)
await get_tree().process_frame
await _point(cam.unproject_position(B + Vector3(2.0, 2.0, 2.0)))
# ---- T1 ----
var bc: Node = map.get_node("BoxCutter")
_ok("T1 BoxCutter starts (aimed at the target's top)", ops.activate(bc))
bc.place(B + Vector3(1, 2, 1), Vector3.UP, B + Vector3(1, 2, 1), B + Vector3(3, 2, 3), t)
await get_tree().process_frame
var mc: ModalControls = bc.modal()
_ok("T1 ModalControls has handles", mc.has_method("handle_points"))
if not mc.has_method("handle_points"):
return
var hp: Array = mc.handle_points()
_ok("T1 BoxCutter drawn: one depth dot", hp.size() == 1, hp)
if hp.size() != 1:
return
# ---- T2 / T3 ----
var step: float = bc.grid_step()
var start := roundi(float(bc.call("_through")) / step)
var g0: int = (persist.gestures() as Array).size()
var dot: Vector3 = hp[0]
await _point(cam.unproject_position(dot))
await _button(true)
_ok("T2 a press on the dot grabs it; the tool stays up", mc.grabbing() and ops.get("active") == bc)
await _point(cam.unproject_position(dot + Vector3.UP * (3.0 * step)))
await _button(false)
var depth := int(bc.get("_depth_steps"))
_ok("T3 dragged 3 steps up: 3 steps shallower", depth == start - 3, [start, depth])
_ok("T3 ... the release lets go, still up, nothing written", not mc.grabbing() and ops.get("active") == bc
and (persist.gestures() as Array).size() == g0)
# ---- T4 ----
var ns := _n("shape.create")
ops.confirm()
persist.refold()
var sc := _last("shape.create")
var szv: Variant = sc.get("size", [0, 0, 0])
var sy: float = (szv as Vector3).y if szv is Vector3 else float(szv[1])
_ok("T4 Enter writes the cut at that depth", _n("shape.create") == ns + 1
and absf(sy - (float(depth) * step + step)) < 1.0e-3, [sy, float(depth) * step + step])
# ---- T5 ----
# The refold after T4 rebuilt the colliders: let physics take them before aiming.
await get_tree().physics_frame
await get_tree().physics_frame
await _point(cam.unproject_position(B + Vector3(0.5, 2.0, 3.5))) # T4 cut the middle out
var up5: bool = ops.activate(bc)
bc.place(B + Vector3(1, 2, 1), Vector3.UP, B + Vector3(1, 2, 1), B + Vector3(2, 2, 2), t)
await get_tree().process_frame
var ns2 := _n("shape.create")
await _point(Vector2(960, 200)) # nowhere near the dot
await _button(true)
_ok("T5 a press away from the dots confirms", up5 and _n("shape.create") == ns2 + 1, [up5, _n("shape.create") - ns2])
# ---- T6 ----
var co: Node = map.get("_curve_op")
ops.activate(co)
var a := B + Vector3(0, 3, 6)
var b := B + Vector3(4, 3, 6)
co.set("_pts", [CurveMath.point(a), CurveMath.point(b)])
co.set("slack", 0.0)
await get_tree().process_frame
var ch: Array = (co.modal() as ModalControls).handle_points()
_ok("T6 the cable / curve tool shows the slack dot", ch.size() == 1, ch)
if ch.size() == 1:
await _point(cam.unproject_position(ch[0]))
await _button(true)
await _point(cam.unproject_position((ch[0] as Vector3) + Vector3.DOWN * 0.5))
await _button(false)
var want := 8.0 * 0.25 / (3.0 * 4.0)
_ok("T6 dragged 0.5 m down: slack = 8 d^2 / (3 L)", absf(float(co.get("slack")) - want) < 0.02,
[co.get("slack"), want])
ops.cancel()