extends Node
## GATE — ASSIGN A GENERATOR (#120 F2, #109 "cube -> stairs, the cube stays as the passage").
## G1 box -> stairs: the shape generates stairs, AND the box comes out as passage
## (a clearance brush with the box's corners)
## G2 params persist across a switch: stairs steps = 7, remove the generator (back to
## the box), assign stairs again -> still 7
## G3 each switch is one gesture; Ctrl+Z goes back to the previous type
## G4 a HULL made from a wedge -> stairs -> Remove generator: the same wedge corners
## G5 the picker lists the create-tool types, and "Remove generator" once assigned
## G6 Shift+D on stairs-over-a-box: the copy keeps the box passage
## G7 a flatten keeps it too
##
## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_assign_generator.tscn
var fails := 0
var checks := 0
var map: Node3D
var persist: Node
var level: Node
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")
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 < 14:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE assign_generator: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _same_corners(a: PackedVector3Array, b: PackedVector3Array) -> bool:
if a.size() != b.size():
return false
for v: Vector3 in a:
if not Array(b).any(func(w: Vector3) -> bool: return v.distance_to(w) < 1.0e-3):
return false
return true
func _pieces(dot: String, clearance: bool) -> Array:
var out: Array = []
for b: int in (level.get_group_brush_ids(dot) as PackedInt32Array):
if bool((level.get_brush(b) as Dictionary).get("clearance", false)) == clearance:
out.append(b)
return out
func _new_brush(planes: Array) -> int:
persist.begin_gesture("Add")
var dot: String = persist.record("brush.create", {"planes": planes, "proj": [], "material_id": 1,
"level_id": 0, "sky": false})
persist.end_gesture()
persist.refold()
return int(level.brush_by_key(dot))
func _run() -> void:
persist = map.get("_persist")
level = map.get("level")
persist.refold()
# A box brush made parametric: a `box` shape 4 x 2 x 2.
var b := _new_brush(Array(ShapeKit.box_piece(Vector3(40, 0, 40), Vector3(44, 2, 42))["planes"]))
var box_corners := FaceKit.corners(level.get_brush(b)["planes"])
map.select_brushes(PackedInt32Array([b]))
var s := String(map.make_parametric()[0])
_ok("setup: a box shape", String(persist.shape_state(s).get("type", "")) == "box")
# ---- G1 ----
var g0: int = (persist.gestures() as Array).size()
_ok("G1 assign stairs", map.assign_generator(s, "stairs"))
_ok("G1 ... the shape is stairs", String(persist.shape_state(s).get("type", "")) == "stairs")
_ok("G1 ... with several step pieces", _pieces(s, false).size() >= 3, _pieces(s, false).size())
var box_passage := false
for c: int in _pieces(s, true):
if _same_corners(FaceKit.corners(level.get_brush(c)["planes"]), box_corners):
box_passage = true
_ok("G1 ... and the box is a passage (clearance)", box_passage)
_ok("G3 one gesture", (persist.gestures() as Array).size() == g0 + 1)
# ---- G2 ----
persist.begin_gesture("Steps")
persist.record("shape.set", {"target": s, "params": {"steps": 7}})
persist.end_gesture()
persist.refold()
map.select_shape(s)
var rows: Array = map.generator_choices()
_ok("G5 the picker offers Remove generator first", not rows.is_empty() and String(rows[0][0]).begins_with("Remove generator"),
rows.map(func(r: Array) -> String: return String(r[0])))
_ok("G5 ... and the create-tool types (not hull, not stairs itself)", rows.any(func(r: Array) -> bool: return String(r[0]) == "Tunnel")
and not rows.any(func(r: Array) -> bool: return String(r[0]) in ["Hull", "Stairs"]))
map.assign_generator(s, "box")
_ok("G2 remove: back to the box, no passage", String(persist.shape_state(s)["type"]) == "box" and _pieces(s, true).is_empty()
and _same_corners(FaceKit.corners(level.get_brush(int(_pieces(s, false)[0]))["planes"]), box_corners))
map.assign_generator(s, "stairs")
_ok("G2 stairs again: steps is still 7", int(persist.shape_state(s)["params"].get("steps", 0)) == 7,
persist.shape_state(s)["params"])
# ---- G3 ----
persist.undo_last()
persist.refold()
_ok("G3 Ctrl+Z goes back to the box", String(persist.shape_state(s)["type"]) == "box")
# ---- G4 ----
var wedge: Array = Array(ShapeKit.box_piece(Vector3(50, 0, 50), Vector3(54, 3, 52))["planes"])
var sn := Vector3(1, 1, 0).normalized()
wedge.append_array([sn.x, sn.y, sn.z, sn.dot(Vector3(53, 3, 50))])
var w := _new_brush(wedge)
var wc := FaceKit.corners(level.get_brush(w)["planes"])
map.select_brushes(PackedInt32Array([w]))
var h := String(map.make_parametric()[0])
map.assign_generator(h, "stairs")
map.assign_generator(h, "hull")
_ok("G4 hull -> stairs -> remove: the same wedge", String(persist.shape_state(h)["type"]) == "hull"
and _pieces(h, false).size() == 1 and _same_corners(FaceKit.corners(level.get_brush(int(_pieces(h, false)[0]))["planes"]), wc))
# ---- G6 ---- (s is a box again after G3's undo: make it stairs over the box)
map.assign_generator(s, "stairs")
map.select_shape(s)
map.duplicate_selection()
(map.call("_ensure_ops") as Node).cancel()
persist.refold()
var cp := String(map.selected_shape())
_ok("G6 the copy is stairs with the box passage", cp != s and String(persist.shape_state(cp).get("type", "")) == "stairs"
and String((persist.shape_state(cp).get("input", {}) as Dictionary).get("type", "")) == "box", persist.shape_state(cp).get("input"))
# ---- G7 ----
persist.flatten_map()
var kept := 0
for d: String in (persist.live_shapes() as Dictionary):
var st: Dictionary = persist.shape_state(d)
if String(st.get("type", "")) == "stairs" and String((st.get("input", {}) as Dictionary).get("type", "")) == "box":
kept += 1
_ok("G7 after a flatten both stairs still have their box passage", kept == 2, kept)