extends SceneTree
## GATE — HOTSPOT ON A PARAMETRIC SHAPE SURVIVES ITS EDITS (user 2026-10-12: "fix hotspot on
## shapes so it survives edits").
##
## Before: Fit on a whole shape wrote face.tex records against its PIECES, which are named
## "<author>/<i>@<ver>:<seq>" — and every shape edit renames them, so the next edit dropped
## the hotspot. Now Fit / Re-roll on a shape write ONE shape.set {tex} (mode 4), which the
## fold copies to every solid face; and the tie-break hash ignores @<ver> (dc-terrain
## 5e31dfd), so a near-tie face keeps its rect when the shape is edited.
##
## 1. Fit on the whole shape: ONE shape.set, no face.tex; the 3 x 0.25 m face is HOTSPOT
## on one of the four 0.24 m strips (as gate_hotspot)
## 2. lengthen the shape 3 -> 5 m (shape.size): the face is STILL hotspot, on the SAME
## strip (same height, same tie, stable hash)
## 3. Re-roll: one shape.set, seed + 1, still hotspot on a near strip
## 4. Pin on a whole shape is refused (no record); Ctrl+Z undoes the re-roll
##
## Run: HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_shape_hotspot.gd
const SHEET := "composed/industrial"
const NEAR_STRIPS := [1, 2, 4, 6]
var ok := true
var checks := 0
var _frame := 0
var _map: Node
var _level: Node
var _persist: Node
func _p(c: bool, m: String, got: Variant = "") -> void:
checks += 1
print(("ok " if c else "FAIL: ") + m + ("" if str(got) == "" else " (" + str(got) + ")"))
if not c:
ok = false
func _process(_d: float) -> bool:
_frame += 1
if _frame == 1:
_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 < 10:
_p(false, "only %d checks ran" % checks)
print("GATE shape_hotspot: %s (%d checks)" % ["PASS" if ok else "FAIL", checks])
quit(0 if ok else 1)
return true
func _kinds_since(seq: int) -> Array:
var out: Array = []
for r: Dictionary in _persist.records():
if int(r["seq"]) > seq:
out.append(String(r["kind"]))
out.sort()
return out
## The shape's +z face: [brush id, face index], or [-1, -1].
func _front(dot: String) -> Array:
for b: int in (_level.get_group_brush_ids(dot) as PackedInt32Array):
var pl: PackedFloat32Array = _level.get_brush(b).get("planes", PackedFloat32Array())
for f in pl.size() / 4:
if pl[f * 4 + 2] > 0.99:
return [b, f]
return [-1, -1]
func _mode(fb: Array) -> int:
if int(fb[0]) < 0:
return -1
return int(((_level.get_brush(int(fb[0])).get("face_tex", []) as Array)[int(fb[1])] as Dictionary).get("mode", -1))
func _chosen(fb: Array) -> int:
if int(fb[0]) < 0:
return -9
var d: Dictionary = _level.face_tex_compute(int(fb[0]), int(fb[1]), "hotspot_pin", {})
return int(d.get("rect", -9)) if not d.is_empty() else -9
func _settle() -> void:
_persist.refold()
_map._process(0.0) # the pack material (re)derives the hotspot rows
func _run() -> void:
_level = _map.get_node("DCLevel")
_persist = _map.get("_persist")
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")
_p(TexturePacks.entry(SHEET).get("rects") is Array, "the composed sheet has rects")
var dot: String = _persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
"frame": SceneObjects.xform_to(Transform3D.IDENTITY), "size": [3.0, 0.25, 1.0],
"material_id": 1, "level_id": 0, "tex": {"tex": SHEET, "mode": 1}})
_settle()
var fb := _front(dot)
_p(int(fb[0]) > 0 and _mode(fb) == 1, "setup: a textured box shape, its +z face paraxial", fb)
print("-- 1. Fit on the whole shape --")
_map.select_shape(dot)
var s1: int = _persist._max_seq_seen()
_map.uv_op("hotspot")
_settle()
_p(_kinds_since(s1) == ["shape.set"], "ONE shape.set, no face.tex on the pieces", _kinds_since(s1))
fb = _front(dot)
var c0 := _chosen(fb)
_p(_mode(fb) == 4 and NEAR_STRIPS.has(c0), "the 3 x 0.25 m face is hotspot on a 0.24 m strip", [_mode(fb), c0])
print("-- 2. lengthen the shape: it must survive --")
_persist.record("shape.size", {"target": dot, "size": [5.0, 0.25, 1.0]})
_settle()
var fb2 := _front(dot)
var k_before: String = _level.get_brush_key(int(fb[0]))
var k_after: String = _level.get_brush_key(int(fb2[0])) if int(fb2[0]) > 0 else ""
_p(k_after != "" and k_after != k_before, "the shape regenerated (its piece was renamed)", [k_before, k_after])
_p(_mode(fb2) == 4, "after the edit the face is STILL hotspot", _mode(fb2))
_p(_chosen(fb2) == c0, "... on the SAME strip (stable tie-break)", [c0, _chosen(fb2)])
print("-- 3. Re-roll --")
_map.select_shape(dot)
var s3: int = _persist._max_seq_seen()
var seed0 := int(((_persist.shape_state(dot) as Dictionary).get("tex", {}) as Dictionary).get("seed", 0))
_map.uv_op("hotspot_reroll")
_settle()
var seed1 := int(((_persist.shape_state(dot) as Dictionary).get("tex", {}) as Dictionary).get("seed", 0))
_p(_kinds_since(s3) == ["shape.set"] and seed1 == seed0 + 1, "ONE shape.set, seed + 1", [_kinds_since(s3), seed0, seed1])
var fb3 := _front(dot)
_p(_mode(fb3) == 4 and NEAR_STRIPS.has(_chosen(fb3)), "still hotspot on a near strip", [_mode(fb3), _chosen(fb3)])
print("-- 4. Pin refused; undo --")
var s4: int = _persist._max_seq_seen()
_map.uv_op("hotspot_pin")
_p(_kinds_since(s4).is_empty(), "Pin on a whole shape writes nothing", _kinds_since(s4))
_persist.undo_last()
_settle()
var seed2 := int(((_persist.shape_state(dot) as Dictionary).get("tex", {}) as Dictionary).get("seed", 0))
_p(seed2 == seed0, "Ctrl+Z undoes the re-roll", [seed0, seed2])