OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_extrude_shape_prompt.gd

146 lines · gd ref:
extends Node
## GATE — E on a PARAMETRIC SHAPE asks "Make Real or Cancel" (#109, user 2026-10-13, A).
##
## A shape's pieces are read-only (docs/parametric-shapes-design.md §7): the fold
## regenerates them from the shape's params, so a face edit on one is an edit the
## generator cannot reproduce. Extrude used to take the brush under the aim WITHOUT
## that check — the preview pushed the face along its normal, and what landed after
## Enter was not that. Now:
##   P1  E on a shape piece does NOT start Extrude; the prompt opens with the two rows
##   P2  Cancel writes nothing and the shape is still live
##   P3  E again → "Make Real, then extrude": the shape is no longer live (Make Real
##       ran) and Extrude IS the active tool, on the now-plain brush
##   P4  confirming 2 steps writes ONE face.set whose normal is the aimed face's own
##       normal and whose d moved by 2 grid steps along it — the commit follows the
##       normal the preview showed, on a TILTED face (a world axis would differ)
## Keys go through Keymap.run, the real input path (#113 §3).
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_extrude_shape_prompt.tscn

var fails := 0
var checks := 0
var _cam: Camera3D = 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:
	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")
	var map: Node3D = (load("res://scenes/maps/map_test.tscn") as PackedScene).instantiate()
	add_child(map)
	_cam = Camera3D.new()
	add_child(_cam)
	_cam.current = true
	_cam.global_position = Vector3(40, 14, 28)
	_cam.look_at(Vector3(40, 0, 40), Vector3.UP)
	for i in 6:
		await get_tree().process_frame
	await _run(map)
	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 extrude_shape_prompt: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	get_tree().quit(0 if fails == 0 else 1)


func _frames(n: int) -> void:
	for i in n:
		await get_tree().process_frame


func _kinds_since(persist: Node, since: int) -> Array:
	var out: Array = []
	for r: Dictionary in (persist.records() as Array):
		if int(r["seq"]) > since:
			out.append(r)
	return out


func _run(map: Node3D) -> void:
	var persist: Node = map.get("_persist")
	var level: Node = map.get("level")
	var stack: Node = map.call("_ensure_ops")
	var q: Node = map.get("_q_menu")

	# A TILTED box shape sitting on the map's own aim ray (whatever the headless pointer
	# reports), so the crosshair lands on one of its pieces.
	var ray: Dictionary = map.call("_crosshair_ray")
	var target: Vector3 = (ray["from"] as Vector3) + (ray["dir"] as Vector3) * 16.0
	var basis := Basis(Vector3.RIGHT, deg_to_rad(25.0))
	var dot: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
		"frame": SceneObjects.xform_to(Transform3D(basis, target - basis * Vector3(3, 3, 3))),
		"size": [6, 6, 6], "material_id": 2, "level_id": 0})
	persist.refold()
	await _frames(4)
	var hit: Dictionary = map.call("_crosshair_hit")
	var hb: int = int(map.call("_brush_from_hit", hit)) if not hit.is_empty() else -1
	_ok("setup: the aim lands on a piece of the shape", hb >= 0 and String(map.call("shape_of_brush", hb)) == dot,
		[dot, hb])
	if hb < 0:
		return

	# ---- P1 ----
	var seq0: int = persist._max_seq_seen()
	Keymap.run("builder.extrude")
	await _frames(2)
	_ok("P1 E on a shape piece does not start Extrude", stack.get("active") != map.get("_extrude"),
		stack.get("active"))
	var rows: Array = q.get("_actions")
	_ok("P1 the prompt is open with Make Real + Cancel", bool(q.call("is_open"))
		and rows.has("builder.extrude_make_real") and rows.has("builder.prompt_cancel"), rows)

	# ---- P2 ----
	q.call("pick", "builder.prompt_cancel")
	await _frames(2)
	_ok("P2 Cancel writes nothing", _kinds_since(persist, seq0).is_empty(), _kinds_since(persist, seq0).size())
	_ok("P2 the shape is still live", (persist.live_shapes() as Dictionary).has(dot))

	# ---- P3 ----
	Keymap.run("builder.extrude")
	await _frames(2)
	q.call("pick", "builder.extrude_make_real")
	await _frames(4)
	_ok("P3 Make Real ran: the shape is no longer live", not (persist.live_shapes() as Dictionary).has(dot))
	var ex: Object = map.get("_extrude")
	_ok("P3 Extrude is the active tool", stack.get("active") == ex, stack.get("active"))
	if stack.get("active") != ex:
		return

	# ---- P4 ----
	var brush: int = int(ex.get("_brush"))
	var fkey: String = String(ex.get("_face_key"))
	var n0: Vector3 = ex.get("_normal")
	var d0: float = float(ex.get("_base_d"))
	var seq1: int = persist._max_seq_seen()
	ex.call("set_tool_setting", "distance", 2)
	_ok("P4 confirming extrudes", bool(stack.call("confirm")))
	await _frames(2)
	var fs: Array = []
	for r: Dictionary in _kinds_since(persist, seq1):
		if String(r["kind"]) == "face.set":
			fs.append(r)
	_ok("P4 ONE face.set was written", fs.size() == 1, fs.size())
	if fs.size() != 1:
		return
	var n1: Vector3 = fs[0]["n"]
	var step: float = float(ex.call("grid_step"))
	_ok("P4 its normal is the aimed face's own (tilted, not a world axis)",
		n1.is_equal_approx(n0) and absf(n0.y) < 0.99 and absf(n0.y) > 0.01, [n0, n1])
	_ok("P4 and the face moved 2 grid steps along it", absf(float(fs[0]["d"]) - (d0 + 2.0 * step)) < 1e-3,
		[d0, fs[0]["d"], step])
	var pl: PackedFloat32Array = (level.get_brush(brush) as Dictionary).get("planes", PackedFloat32Array())
	var f: int = int(level.face_by_key(brush, fkey))
	_ok("P4 the brush's plane is that plane", f >= 0 and absf(pl[f * 4 + 3] - (d0 + 2.0 * step)) < 1e-3,
		[f, pl[f * 4 + 3] if f >= 0 else null])