extends SceneTree
## GATE — the builder keys survive a MAP CHANGE (user 2026-10-05: "a weird race where
## keys do not do what they should" — N, Shift+Space, Delete dead).
##
## Every map pushes the `builder` keymap context in _ready and pops it in _exit_tree.
## game.change_map() queue_frees the old map and adds the new one AT ONCE, so the new
## map's push lands first (a no-op, it is already there) and the old map's pop runs at
## the END of the frame — taking `builder` with it. Every builder key is then dead
## until the next map load; mouse picks, which do not go through the keymap, still work.
##
## This does exactly what change_map does, with the real map scene, and asserts the
## context is still there afterwards. Plus the Keymap rule itself: holds are per owner.
##
## Run: "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_map_change_keys.gd
var ok := true
var _step := 0
var _a: Node = null
var _b: Node = null
var _km: Node = null
var _rules_done := false
func _p(c: bool, m: String) -> void:
print(("ok " if c else "FAIL: ") + m)
if not c:
ok = false
func _process(_delta: float) -> bool:
_step += 1
match _step:
1:
_km = root.get_node_or_null("Keymap")
_p(_km != null, "Keymap autoload present")
if _km == null:
return _done()
_rules()
var scene := load("res://scenes/maps/map_test.tscn") as PackedScene
_a = scene.instantiate()
root.add_child(_a)
4:
_p(_km.get("_stack").has("builder"), "map A loaded: builder on the stack %s" % str(_km.get("_stack")))
# change_map: the old map is queue_freed, the new one added immediately.
_a.queue_free()
_b = (load("res://scenes/maps/map_test.tscn") as PackedScene).instantiate()
root.add_child(_b)
8:
_p(not is_instance_valid(_a), "map A is gone")
_p(_km.get("_stack").has("builder"),
"after the change, builder is STILL on the stack %s" % str(_km.get("_stack")))
_b.queue_free()
11:
_p(not _km.get("_stack").has("builder"), "and the last map leaving takes it %s" % str(_km.get("_stack")))
return _done()
return false
## The Keymap rule on its own: a context stays while ANY owner holds it; a push/pop
## with no owner behaves exactly as it always did.
func _rules() -> void:
var o1 := Node.new()
var o2 := Node.new()
_km.push_context("menu", o1)
_km.push_context("menu", o2)
_km.pop_context("menu", o1)
_p(_km.get("_stack").has("menu"), "owned: o2 still holds menu after o1 pops")
_km.pop_context("menu", o2)
_p(not _km.get("_stack").has("menu"), "owned: menu gone when the last holder pops")
_km.push_context("menu")
_km.push_context("menu")
_km.pop_context("menu")
_p(not _km.get("_stack").has("menu"), "unowned: one pop removes it, as before")
o1.free()
o2.free()
_rules_done = true
func _done() -> bool:
# A SCRIPT ERROR aborts _rules() without failing a check; only a full run passes.
_p(_rules_done, "the owner rules ran to the end (no script error aborted them)")
print("GATE map_change_keys: %s" % ("PASS" if ok else "FAIL"))
quit(0 if ok else 1)
return true