OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_tool_handles.gd

169 lines · gd ref:
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()