extends SceneTree
## GATE — the create tool makes ONLY parametric shapes (docs/parametric-shapes-design.md
## §13, hyper-games #109, user 2026-10-12: "make the create tool make parametric shapes").
##
## C1 the shapes it offers are the ParamShapes types, in order (plugins after)
## C2 an old sticky pref migrates: "arch" -> tunnel, "stairs" -> param stairs
## C3 Stairs writes ONE shape.create and no brush.create; the fold makes its pieces
## C4 Tunnel (the old arch): wall follows the arch's grid rule unless set; a set wall
## is kept
## C5 Auto steps "one per grid step" stores auto_riser = the grid step in the record;
## "0.2 m risers" stores 0.2
##
## RUN: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$GODOT" --headless --path . --script res://scenes/test/gate_create_param.gd
const ParamShapes := preload("res://scenes/maps/shapes/param/param_shapes.gd")
var _frame := 0
var _map: Node = null
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 _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:
return false
_run()
if checks < 8:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE create_param: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
quit(0 if fails == 0 else 1)
return true
func _count(kind: String) -> int:
var n := 0
for r: Dictionary in (_map.get("_persist").records() as Array):
if String(r.get("kind", "")) == kind and not bool(r.get("undone", false)):
n += 1
return n
func _last(kind: String) -> Dictionary:
var best := {}
for r: Dictionary in (_map.get("_persist").records() as Array):
if String(r.get("kind", "")) == kind and (best.is_empty() or int(r["seq"]) > int(best["seq"])):
best = r
return best
## Place without aiming: the placement Adjust Last restores (origin on the floor, normal
## up, looking +x), so confirm() has bounds in a headless gate.
func _place(bx: CreateBoxOperator) -> void:
bx.adjust_restore({"origin": Vector3(0, 0, 0), "normal": Vector3.UP, "axis": 1,
"view": Vector3(1, 0, 0.01), "material": 0, "level_id": 0})
func _run() -> void:
var bx: CreateBoxOperator = _map.get("_box")
# ---- C1 ----
var want: Array = []
for t: String in ParamShapes.ids():
want.append("param:" + t)
var have: Array = bx.shape_ids()
_p(have.slice(0, want.size()) == want and have.size() == want.size(), "C1 the tool offers the parametric types, in order", have)
# ---- C2 ----
bx.save_pref("shape", "arch")
bx.load_prefs()
_p(String(bx.shape_ids()[int(bx.get("_shape"))]) == "param:tunnel", "C2 pref 'arch' -> tunnel", bx.shape_ids()[int(bx.get("_shape"))])
bx.save_pref("shape", "stairs")
bx.load_prefs()
_p(String(bx.shape_ids()[int(bx.get("_shape"))]) == "param:stairs", "C2 pref 'stairs' -> param stairs", bx.shape_ids()[int(bx.get("_shape"))])
# ---- C3 ----
bx.set_tool_setting("shape", CreateBoxOperator.Shape.STAIRS)
bx.set_tool_setting("size", 4)
bx.set_tool_setting("auto_steps", 0)
_place(bx)
var nb := _count("brush.create")
var ns := _count("shape.create")
var ok: bool = bx.confirm()
_p(ok and _count("shape.create") == ns + 1 and _count("brush.create") == nb,
"C3 Stairs writes one shape.create, no brush.create", "%s +%d shape +%d brush" % [ok, _count("shape.create") - ns, _count("brush.create") - nb])
var rec := _last("shape.create")
_p(String(rec.get("type", "")) == "stairs" and (_map.level.get_group_brush_ids(
"%s:%d" % [rec.get("author", ""), int(rec.get("seq", 0))]) as PackedInt32Array).size() > 1,
"C3 the fold made its pieces", rec.get("type", ""))
# ---- C5 ----
_p(is_equal_approx(float((rec.get("params", {}) as Dictionary).get("auto_riser", -1.0)), 0.2),
"C5 '0.2 m risers' stores auto_riser 0.2", (rec.get("params", {}) as Dictionary).get("auto_riser"))
bx.set_tool_setting("auto_steps", 1)
_place(bx)
bx.confirm()
var rec2 := _last("shape.create")
_p(is_equal_approx(float((rec2.get("params", {}) as Dictionary).get("auto_riser", -1.0)), bx.grid_step()),
"C5 'one per grid step' stores auto_riser = the grid step (%.3f)" % bx.grid_step(),
(rec2.get("params", {}) as Dictionary).get("auto_riser"))
# ---- C4 ----
bx.set_tool_setting("shape", CreateBoxOperator.Shape.ARCH)
_place(bx)
bx.confirm()
var t1 := _last("shape.create")
var c: Array = bx._corners()
var span_r: float = ((c[1] as Vector3) - (c[0] as Vector3)).z * 0.5
var height: float = ((c[1] as Vector3) - (c[0] as Vector3)).y
var rule: float = ArchShape.thickness(span_r, height, bx.grid_step())
_p(String(t1.get("type", "")) == "tunnel" and is_equal_approx(float((t1.get("params", {}) as Dictionary).get("wall", -1.0)), rule),
"C4 tunnel wall follows the arch rule (%.3f)" % rule, (t1.get("params", {}) as Dictionary).get("wall"))
bx.set_tool_setting("p.wall", 0.75)
_place(bx)
bx.confirm()
var t2 := _last("shape.create")
_p(is_equal_approx(float((t2.get("params", {}) as Dictionary).get("wall", -1.0)), 0.75), "C4 a set wall (0.75) is kept",
(t2.get("params", {}) as Dictionary).get("wall"))