extends Node
## GATE — ALT+D, DUPLICATE LINKED (Blender's linked data; ShapeFold._link_data).
## L1 Alt+D (a real key) on a staircase: a new staircase LINKED to it; Move picks it up
## L2 steps set on the COPY: the source changes too (the pieces of both)
## L3 steps set on the SOURCE: the copy follows
## L4 moving the copy leaves the source where it is (the frame is the object's own)
## L5 resizing the copy leaves the source's size (Blender's object scale)
## L6 a modifier on the copy is the copy's only
## L7 Shift+D's copy is NOT linked: an edit on it stays its own
## L8 Alt+D of a link links to the ROOT
## L9 the root deleted: its links still share their data
## L10 Alt+D on a plain brush: refused, nothing written
## L11 two arrival orders give the same shared params
## L12 a flatten keeps the set linked
##
## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_duplicate_linked.tscn
const ShapeFold := preload("res://scenes/maps/shapes/param/shape_fold.gd")
var fails := 0
var checks := 0
var map: Node3D
var persist: Node
var level: Node
var stack: 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 < 15:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE duplicate_linked: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
## A real key press (Input, not a call): the path the player's key takes.
func _key(k: Key, shift := false, alt := false) -> void:
var e := InputEventKey.new()
e.keycode = k
e.physical_keycode = k
e.shift_pressed = shift
e.alt_pressed = alt
e.pressed = true
Input.parse_input_event(e)
await get_tree().process_frame
var u := e.duplicate()
u.pressed = false
Input.parse_input_event(u)
await get_tree().process_frame
func _stairs(at: Vector3) -> String:
var d: String = persist.record("shape.create", {"type": "stairs", "gv": 1, "params": {"steps": 4},
"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, at)),
"size": [3.0, 2.0, 1.5], "material_id": 2, "level_id": 0})
persist.refold()
return d
func _steps(d: String) -> int:
return int((persist.shape_state(d)["params"] as Dictionary).get("steps", -1))
func _set_params(d: String, ps: Dictionary) -> void:
persist.begin_gesture("Edit shape")
persist.record("shape.set", {"target": d, "params": ps})
persist.end_gesture()
persist.refold()
func _n(d: String) -> int:
return (level.get_group_brush_ids(d) as PackedInt32Array).size()
## Alt+D on `d` (selected as the shape), the Move cancelled: the copy's dot.
func _alt_d(d: String) -> String:
map.select_shape(d)
await _key(KEY_D, false, true)
var made := String(map.get("_sel_shape"))
if stack.get("active") != null:
stack.cancel()
persist.refold()
return made if made != d else ""
func _run() -> void:
persist = map.get("_persist")
level = map.get("level")
stack = map._ensure_ops()
persist.refold()
# ---- L1 ----
var a := _stairs(Vector3(40, 0, 40))
map.select_shape(a)
await _key(KEY_D, false, true)
var b := String(map.get("_sel_shape"))
_ok("L1 Alt+D: a new staircase", b != "" and b != a and String(persist.shape_state(b).get("type", "")) == "stairs", b)
_ok("L1 ... linked to the source", String(persist.shape_state(b).get("link", "")) == a, persist.shape_state(b).get("link"))
_ok("L1 ... and Move picks it up", stack.get("active") != null)
if stack.get("active") != null:
stack.cancel()
persist.refold()
# ---- L2 / L3 ----
var n4 := _n(a)
_set_params(b, {"steps": 7})
_ok("L2 steps 7 on the COPY: the source has 7 too", _steps(a) == 7 and _steps(b) == 7, [_steps(a), _steps(b)])
_ok("L2 ... the pieces of both changed", _n(a) == _n(b) and _n(a) != n4, [n4, _n(a), _n(b)])
_set_params(a, {"steps": 5})
_ok("L3 steps 5 on the SOURCE: the copy follows", _steps(b) == 5, _steps(b))
# ---- L4 / L5 ----
persist.begin_gesture("Move")
map.translate_units([{"kind": "shape", "shape": b, "w": 1.0}], Vector3(4, 0, 0))
persist.end_gesture()
persist.refold()
_ok("L4 the copy moved, the source did not", (persist.shape_state(a)["frame"] as Transform3D).origin.is_equal_approx(Vector3(40, 0, 40))
and (persist.shape_state(b)["frame"] as Transform3D).origin.is_equal_approx(Vector3(44, 0, 40)))
persist.begin_gesture("Size")
persist.record("shape.size", {"target": b, "size": [6.0, 2.0, 1.5]})
persist.end_gesture()
persist.refold()
_ok("L5 the copy resized, the source kept its size", (persist.shape_state(a)["size"] as Vector3).is_equal_approx(Vector3(3, 2, 1.5))
and (persist.shape_state(b)["size"] as Vector3).is_equal_approx(Vector3(6, 2, 1.5)))
# ---- L6 ----
var na := _n(a)
var md: String = map.add_modifier("mirror", b)
_ok("L6 a mirror on the copy: the source has no copies", md != "" and _n(a) == na and _n(b) > na, [na, _n(a), _n(b)])
map.remove_modifier(md)
# ---- L7 ----
map.select_shape(a)
map.duplicate_selection()
var c := String(map.get("_sel_shape"))
stack.cancel()
persist.refold()
_set_params(c, {"steps": 9})
_ok("L7 Shift+D's copy is its own: steps 9 on it, the source keeps 5", _steps(c) == 9 and _steps(a) == 5,
[_steps(c), _steps(a)])
# ---- L8 ----
var b2 := await _alt_d(b)
_ok("L8 Alt+D of a link links to the ROOT", b2 != "" and String(persist.shape_state(b2).get("link", "")) == a,
persist.shape_state(b2).get("link") if b2 != "" else "no copy")
# ---- L11 ----
var recs: Array = persist.records(false).filter(func(x: Dictionary) -> bool: return ShapeFold.handles(String(x["kind"])))
var rev := recs.duplicate()
rev.reverse()
var s1 := ShapeFold.resolve_states(recs)
var s2 := ShapeFold.resolve_states(rev)
_ok("L11 another arrival order, the same shared steps", int(s1[b2]["params"]["steps"]) == int(s2[b2]["params"]["steps"])
and int(s1[b2]["params"]["steps"]) == 5)
# ---- L9 ----
persist.begin_gesture("Delete")
persist.record("shape.remove", {"target": a})
persist.end_gesture()
persist.refold()
_set_params(b, {"steps": 3})
_ok("L9 the root deleted: an edit on one link reaches the other", _steps(b2) == 3 and _n(b2) > 0, [_steps(b2), _n(b2)])
# ---- L12 ----
var r12: Dictionary = persist.flatten_map()
var links: Array = []
for d: String in (persist.live_shapes() as Dictionary):
if String(persist.shape_state(d)["type"]) == "stairs" and _steps(d) == 3:
links.append(d)
_ok("L12 a flatten keeps both links", not r12.has("error") and links.size() == 2, [links, r12.get("error", "")])
if links.size() == 2:
_set_params(links[0], {"steps": 6})
_ok("L12 ... still linked: an edit on one reaches the other", _steps(links[1]) == 6, _steps(links[1]))
# ---- L10 ----
var plain := -1
for bb: int in (level.get_live_brush_ids() as PackedInt32Array):
if map.shape_of_brush(bb) == "":
plain = bb
break
var n10: int = (persist.records(false) as Array).size()
map.select_brushes(PackedInt32Array([plain]))
await _key(KEY_D, false, true)
_ok("L10 Alt+D on a plain brush: refused, nothing written", (persist.records(false) as Array).size() == n10
and stack.get("active") == null, (persist.records(false) as Array).size() - n10)