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)