OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_cable_drop.gd

225 lines · gd ref:
extends Node
## GATE — DROP A CABLE TO THE GROUND (#120 L3, docs/node-tree-and-stacks.md §14.3).
##   pure (CurveDrop, a fake flat ground at y = 0):
##   D1  interior points land at the radius above the ground, normal up; the ends stay
##   D2  step 1 on a 10 m span: 11 points, 1 m apart, the interior all dropped
##   D3  a miss leaves every point where it was
##   D4  an ALIGNED handle is laid onto the ground plane, its length kept, its type kept
##   D5  a closed curve drops every point
##   D6  a point's radius scales the rest height
##   in the map (real physics):
##   M1  a cable over a floor with a box on it: points over the box rest on the box, the
##       rest on the floor, the ends where they were
##   M2  a point above the cable's OWN lower part skips it and lands on the floor
##   M3  one cable = one curve.set; Ctrl+Z puts the curve back
##   M4  builder.drop_cable starts the tool on the selected cable; Enter writes; Adjust Last
##       with another step re-runs it as one live gesture
##   M5  the Q menu on a cable offers Drop; the key is bound
##
## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_cable_drop.tscn

var fails := 0
var checks := 0
var map: Node3D
var persist: Node
var level: Node
var CD: GDScript = null


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:
	if ResourceLoader.exists("res://scenes/maps/curves/curve_drop.gd"):
		CD = load("res://scenes/maps/curves/curve_drop.gd")
	_ok("setup: CurveDrop exists", CD != null)
	if CD != null:
		_pure()
	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")
	map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
	add_child(map)
	for i in 6:
		await get_tree().process_frame
	await _run()
	get_tree().root.get_node("/root/GameMode").set_mode("play")
	if checks < 16:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE cable_drop: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	get_tree().quit(0 if fails == 0 else 1)


func _flat(from: Vector3) -> Dictionary:
	return {"position": Vector3(from.x, 0.0, from.z), "normal": Vector3.UP}


func _miss(_from: Vector3) -> Dictionary:
	return {}


func _pts(ps: Array) -> Array:
	var out: Array = []
	for p: Vector3 in ps:
		out.append(CurveMath.point(p, Vector3.ZERO))
	return out


func _ys(pts: Array) -> Array:
	var out: Array = []
	for q: Dictionary in pts:
		out.append(snappedf((q["p"] as Vector3).y, 1.0e-4))
	return out


func _pure() -> void:
	# ---- D1 ----
	var three := CurveMath.auto_handles(_pts([Vector3(0, 5, 0), Vector3(5, 5, 0), Vector3(10, 5, 0)]), false)
	var d1: Array = CD.dropped(three, false, 0.0, 0.1, _flat)
	_ok("D1 the middle point rests 0.1 above the ground, n up; the ends stay at 5", d1.size() == 3
		and _ys(d1) == [5.0, 0.1, 5.0] and (d1[1]["n"] as Vector3).is_equal_approx(Vector3.UP), _ys(d1))
	# ---- D2 ----
	var two := CurveMath.auto_handles(_pts([Vector3(0, 5, 0), Vector3(10, 5, 0)]), false)
	var d2: Array = CD.dropped(two, false, 1.0, 0.1, _flat)
	var even := d2.size() == 11
	for i in range(1, d2.size()):
		var dx := (d2[i]["p"] as Vector3).x - (d2[i - 1]["p"] as Vector3).x
		even = even and absf(dx - 1.0) < 1.0e-2
	var low := d2.size() == 11
	for i in range(1, d2.size() - 1):
		low = low and absf((d2[i]["p"] as Vector3).y - 0.1) < 1.0e-4
	_ok("D2 step 1 on 10 m: 11 points, 1 m apart", even, d2.size())
	_ok("D2 ... the 9 interior points all dropped", low, _ys(d2))
	# ---- D3 ----
	var d3: Array = CD.dropped(three, false, 0.0, 0.1, _miss)
	_ok("D3 a miss leaves every point", _ys(d3) == [5.0, 5.0, 5.0], _ys(d3))
	# ---- D4 ----
	var al := _pts([Vector3(0, 5, 0), Vector3(5, 5, 0), Vector3(10, 5, 0)])
	al[1]["handle"] = "aligned"
	al[1]["h_out"] = Vector3(1, 1, 0)
	al[1]["h_in"] = Vector3(-1, -1, 0)
	var d4: Array = CD.dropped(CurveMath.auto_handles(al, false), false, 0.0, 0.1, _flat)
	var ho: Vector3 = d4[1]["h_out"]
	_ok("D4 an aligned handle lies on the ground, its length kept, still aligned",
		ho.distance_to(Vector3(sqrt(2.0), 0, 0)) < 1.0e-4 and String(d4[1].get("handle", "")) == "aligned", ho)
	# ---- D5 ----
	var loop := CurveMath.auto_handles(_pts([Vector3(0, 5, 0), Vector3(4, 5, 0), Vector3(4, 5, 4), Vector3(0, 5, 4)]), true)
	var d5: Array = CD.dropped(loop, true, 0.0, 0.1, _flat)
	_ok("D5 a closed curve drops every point", _ys(d5) == [0.1, 0.1, 0.1, 0.1], _ys(d5))
	# ---- D6 ----
	var fat := _pts([Vector3(0, 5, 0), Vector3(5, 5, 0), Vector3(10, 5, 0)])
	fat[1]["radius"] = 2.0
	var d6: Array = CD.dropped(CurveMath.auto_handles(fat, false), false, 0.0, 0.1, _flat)
	_ok("D6 point radius 2 x radius 0.1: rests 0.2 above", absf((d6[1]["p"] as Vector3).y - 0.2) < 1.0e-4, _ys(d6))


func _box(origin: Vector3, size: Vector3) -> String:
	persist.begin_gesture("Box")
	var d: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
		"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, origin)),
		"size": [size.x, size.y, size.z], "material_id": 2, "level_id": 0})
	persist.end_gesture()
	return d


func _curve_pts(cable: String) -> Array:
	var cd := String((persist.shape_state(cable)["params"] as Dictionary).get("curve", ""))
	return (persist.curve_states() as Dictionary).get(cd, {}).get("points", [])


func _run() -> void:
	persist = map.get("_persist")
	level = map.get("level")
	var ops: Node = map.call("_ensure_ops")
	_box(Vector3(295, 90, 290), Vector3(50, 10, 20))      # floor: top at y 100
	_box(Vector3(308, 100, 298), Vector3(4, 2, 4))         # a box on it: top 102, x 308..312
	persist.refold()
	for i in 4:
		await get_tree().physics_frame
	var cable := map.add_cable(_pts([Vector3(300, 105, 300), Vector3(320, 105, 300)]), false, {"radius": 0.1}) as String
	var hook := map.add_cable(_pts([Vector3(330, 105, 300), Vector3(336, 105, 300), Vector3(336, 101, 300)]),
		false, {"radius": 0.1}) as String
	for i in 4:
		await get_tree().physics_frame
	var has_drop := map.has_method("drop_cables")
	_ok("setup: map.drop_cables exists", has_drop)
	if not has_drop:
		return

	# ---- M1 + M3 ----
	var before: Array = _curve_pts(cable)
	var n0: int = (persist.records(false) as Array).size()
	var wrote: int = map.drop_cables([cable], 1.0)
	var n1: int = (persist.records(false) as Array).size()
	var after: Array = _curve_pts(cable)
	var on_box := 0
	var on_floor := 0
	var bad: Array = []
	for i in range(1, after.size() - 1):
		var p: Vector3 = after[i]["p"]
		if p.x > 308.2 and p.x < 311.8:
			if absf(p.y - 102.1) < 0.01:
				on_box += 1
			else:
				bad.append(p)
		elif p.x < 307.8 or p.x > 312.2:
			if absf(p.y - 100.1) < 0.01:
				on_floor += 1
			else:
				bad.append(p)
	_ok("M1 21 points: on the box at 102.1, on the floor at 100.1", after.size() == 21 and on_box >= 3
		and on_floor >= 14 and bad.is_empty(), [after.size(), on_box, on_floor, bad])
	_ok("M1 ... the ends where they were", after.size() >= 2 and (after[0]["p"] as Vector3).is_equal_approx(Vector3(300, 105, 300))
		and (after[after.size() - 1]["p"] as Vector3).is_equal_approx(Vector3(320, 105, 300)),
		[after[0]["p"], after[after.size() - 1]["p"]] if after.size() >= 2 else after)
	_ok("M3 one cable: one record (curve.set), drop_cables says 1", n1 == n0 + 1 and wrote == 1, [n1 - n0, wrote])
	persist.undo_last()
	persist.refold()
	_ok("M3 ... Ctrl+Z puts the curve back", _curve_pts(cable).size() == before.size()
		and (_curve_pts(cable)[0]["p"] as Vector3).is_equal_approx(before[0]["p"]), _curve_pts(cable).size())

	# ---- M2 ----
	map.drop_cables([hook], 0.0)
	var hp: Array = _curve_pts(hook)
	_ok("M2 the point above its own cable skips it and lands on the floor (100.1)", hp.size() == 3
		and absf((hp[1]["p"] as Vector3).y - 100.1) < 0.01, hp.map(func(q: Dictionary) -> Vector3: return q["p"]))
	persist.undo_last()
	persist.refold()

	# ---- M4 ----
	map.select_shape(cable)
	var ev := InputEventAction.new()
	ev.action = "builder.drop_cable"
	ev.pressed = true
	map._unhandled_input(ev)
	var op: Object = map.get("_drop_op")
	_ok("M4 builder.drop_cable: the drop tool is up", op != null and ops.get("active") == op, ops.get("active"))
	if op != null and ops.get("active") == op:
		op.set_tool_setting("step", 2.0)
		ops.confirm()
		_ok("M4 Enter with step 2: 11 points", _curve_pts(cable).size() == 11, _curve_pts(cable).size())
		var live0: int = (persist.records(false) as Array).filter(func(r: Dictionary) -> bool: return not bool(r.get("undone", false))).size()
		var adj: bool = ops.adjust("step", 1.0)
		var live1: int = (persist.records(false) as Array).filter(func(r: Dictionary) -> bool: return not bool(r.get("undone", false))).size()
		_ok("M4 Adjust Last step 1: 21 points, still one live drop", adj and _curve_pts(cable).size() == 21
			and live1 == live0, [adj, _curve_pts(cable).size(), live0, live1])

	# ---- M5 ----
	map.select_shape(cable)
	var qrows: Array = []
	for sec: Array in map.q_sections():
		for r: Array in sec[1]:
			qrows.append(r[0])
	_ok("M5 the Q menu on a cable offers Drop", qrows.has("builder.drop_cable"), qrows)
	var spec: Dictionary = Keymap.DEFAULTS.get("builder", {}).get("builder.drop_cable", {})
	_ok("M5 builder.drop_cable is bound (Ctrl+Shift+D)", int(spec.get("key", 0)) == KEY_D
		and bool(spec.get("ctrl", false)) and bool(spec.get("shift", false)), spec)