extends SceneTree
## GATE — editor redesign E3 (docs/editor-redesign.md §5), on a real map_flat.
## E3a F3 COMMAND PALETTE: every action reachable in build mode, generated from the
## keymap (label + live key + menu), fuzzy search, Enter runs it through
## Keymap.run (the SAME path as the key), Escape closes without running,
## recent actions remembered per player.
## E3b F1 KEY LIST: the contexts active NOW come first, and a search field filters it;
## F1 / Escape still close it while the field has focus (always context).
## E3c SHIFT+SPACE TOOL POPUP: every toolbar tool with its key; pick by click or by the
## tool's key; while it is open the `menu` context holds (no tools fire, no flying).
## E3d RMB TAP CONTEXT MENU: a quick right click (no drag) with no tool up opens a menu
## built from the selection; a held / dragged RMB is still the look and opens nothing.
## E3e MODIFIER HINT: holding Ctrl / Shift / Alt ~300 ms lists in the status bar what it
## means right now (fly vs tool) and every chord with it in the active contexts.
## E3f FIRST-RUN CARD: entering build mode once shows how in (F1, RMB, F3, Shift+Space),
## dismissable, and never again for that player.
## E3g WHEEL = THE ACTIVE TOOL'S [ / ] (user decision 2026-10-02): with the box tool up,
## wheel up / down grows / shrinks it; with no tool the wheel leaves the grid alone.
##
## Run: "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_editor_e3.gd
var ok := true
var checks := 0
var _frame := 0
var _map: Node
var _km: Node
var _dock: Node
var _pal: Node
var _tp: Node
var _cm: Node
var _hud: Node
var _t_mod := 0
var _saved_recent: Variant = []
var _saved_grid: Variant = 5
var _saved_seen: Variant = false
var _saved_snap: Variant = true
const TOOL_PREFS := ["tool.box.shape", "tool.box.size", "tool.box.height", "tool.box.p.stairs.steps",
"tool.box.p.arch.segments"]
var _saved_tool: Dictionary = {}
func _p(cond: bool, what: String, note: String = "") -> void:
checks += 1
if not cond:
ok = false
print(" %-62s %s %s" % [what, "OK" if cond else "**FAIL**", note])
func _press(code: Key, shift := false) -> void:
for pressed: bool in [true, false]:
var e := InputEventKey.new()
e.keycode = code
e.physical_keycode = code
e.shift_pressed = shift
e.pressed = pressed
Input.parse_input_event(e)
func _key_state(code: Key, pressed: bool) -> void:
var e := InputEventKey.new()
e.keycode = code
e.physical_keycode = code
e.pressed = pressed
Input.parse_input_event(e)
func _wheel(button: MouseButton) -> void:
for pressed: bool in [true, false]:
var e := InputEventMouseButton.new()
e.button_index = button
e.pressed = pressed
e.position = Vector2(400, 300)
Input.parse_input_event(e)
func _rmb(pressed: bool, at: Vector2) -> void:
var e := InputEventMouseButton.new()
e.button_index = MOUSE_BUTTON_RIGHT
e.pressed = pressed
e.position = at
e.global_position = at
Input.parse_input_event(e)
func _process(_d: float) -> bool:
_frame += 1
match _frame:
1:
# A REAL window: headless is 64x64, and the clicks below at (400, 300) must land
# in the 3D view's free rectangle, not outside the window (#124 B1: everything
# outside R is an area, so an off-window point counts as covered).
root.size = Vector2i(1920, 1080)
# Gates share the installed game's user:// — keep the player's recents.
_dock = root.get_node("/root/BuildDock")
_saved_recent = _dock.pref("palette_recent", [])
_saved_grid = _dock.pref("grid", 5)
_saved_seen = _dock.pref("first_run_seen", false)
_saved_snap = _dock.pref("snap", true)
_dock.set_pref("palette_recent", [])
_dock.set_pref("grid", 5)
# The box tool's sticky size/shape (E4) would start the wheel test somewhere
# else on a machine that last used another size — start from the defaults.
for k: String in TOOL_PREFS:
_saved_tool[k] = _dock._prefs.get(k, null)
_dock._prefs.erase(k)
# Snap ON: with the player's snap off every box step is 1u, not 16u.
_dock.set_pref("snap", true)
_map = (load("res://scenes/maps/map_flat.tscn") as PackedScene).instantiate()
root.add_child(_map)
30:
_km = root.get_node("/root/Keymap")
root.get_node("/root/GameMode").set_mode("build")
print("-- E3a: the command palette --")
# Space since keymap step 3 (Blender_27x.py spacebar_action='SEARCH').
_p(_km.describe("builder.palette") == "Space", "Space opens the command palette", _km.describe("builder.palette"))
_pal = _map.get("_palette")
_p(_pal != null, "the map owns a palette")
if _pal == null:
return _finish()
_press(KEY_SPACE)
33:
_p(bool(_pal.is_open()), "Space in build mode opens it")
var all: Array = _pal.shown_actions()
_p(all.has("builder.create_box") and all.has("build_mode.csg_subtract") and all.has("global.help"),
"empty search lists builder, build-mode AND global actions", "%d actions" % all.size())
_p(not all.has("builder.palette"), "but not itself")
_p(not all.has("builder.tool_more"), "nor mouse gestures (the wheel is not a command)")
_p(String(_pal.row_text("builder.add_menu")).contains("Shift+A")
and String(_pal.row_text("builder.add_menu")).contains("Add"),
"a row shows the live key and its menu", String(_pal.row_text("builder.add_menu")))
_pal.set_query("new shape")
var hits: Array = _pal.shown_actions()
_p(not hits.is_empty() and hits[0] == "builder.create_box", "\"new shape\" -> the create tool first", str(hits.slice(0, 3)))
_pal.set_query("csgsub")
hits = _pal.shown_actions()
_p(not hits.is_empty() and hits[0] == "build_mode.csg_subtract", "fuzzy: \"csgsub\" -> CSG subtract", str(hits.slice(0, 3)))
_pal.set_query("zzqqxx")
_p(_pal.shown_actions().is_empty(), "no match -> empty list")
# RUN something whose effect needs no aim (the box tool needs a surface under
# the crosshair, which a headless gate does not have).
_map.set_grid_index(5)
_pal.set_query("coarser grid")
_press(KEY_ENTER)
38:
_p(not bool(_pal.is_open()), "Enter closes it")
_p(int(_map.get("_grid_idx")) == 6, "... and RAN it through the key path: grid 8u -> 16u",
str(_map.get("_grid_idx")))
_p((_dock.pref("palette_recent", []) as Array).size() > 0
and (_dock.pref("palette_recent", []) as Array)[0] == "builder.grid_coarser", "remembered as recent")
_press(KEY_SPACE)
41:
_p(bool(_pal.is_open()), "reopens")
_p(_pal.shown_actions().size() > 0 and _pal.shown_actions()[0] == "builder.grid_coarser",
"recent first on an empty search")
_pal.set_query("delete")
_press(KEY_ESCAPE)
44:
_p(not bool(_pal.is_open()), "Escape closes it")
_p(_km.is_context_active("builder"), "and the builder keys work again (no text field left focused)")
print("\n-- E3b: the F1 key list --")
_press(KEY_F1)
47:
var help: Node = root.get_node("/root/HelpOverlay")
_p(bool(help.visible), "F1 opens the key list")
var t: String = help.text()
# Keymap step 4 (§4a): F1 lists the contexts active NOW, under plain names — in
# build mode the walking keys are not listed at all; "show every context" adds
# them AFTER the active ones, marked not active.
var i_build := t.find("BUILD MODE")
_p(i_build >= 0 and t.find("PLAYING") < 0, "in build mode F1 lists the build keys, not the walking keys",
"BUILD MODE@%d PLAYING@%d" % [i_build, t.find("PLAYING")])
help.call("set_show_all", true)
var ta: String = help.text()
_p(ta.find("PLAYING") > ta.find("BUILD MODE") and ta.find("BUILD MODE") >= 0,
"show every context: the walking keys come AFTER the build keys",
"BUILD MODE@%d PLAYING@%d" % [ta.find("BUILD MODE"), ta.find("PLAYING")])
help.call("set_show_all", false)
help.set_query("grid")
t = help.text()
_p(t.contains("Coarser grid") and t.contains("Finer grid") and not t.contains("Jump"),
"search \"grid\" keeps the grid keys, drops the rest")
_p(_km.is_context_active("global"), "F1 / Esc stay live while the search has focus")
_press(KEY_ESCAPE)
50:
var help2: Node = root.get_node("/root/HelpOverlay")
_p(not bool(help2.visible), "Escape closes it")
_p(_km.is_context_active("builder"), "builder keys live again after closing")
print("\n-- E3c: the tool popup --")
_p(_km.describe("builder.tool_popup") == "Shift+Space", "Shift+Space opens the tool popup",
_km.describe("builder.tool_popup"))
_tp = _map.get("_tool_popup")
_p(_tp != null, "the map owns a tool popup")
if _tp == null:
return _finish()
_press(KEY_SPACE, true)
53:
_p(bool(_tp.is_open()), "Shift+Space in build mode opens it")
var tools: Array = load("res://scenes/game/editor_tool_bar.gd").TOOLS
_p(_tp.item_count() == tools.size(), "one row per toolbar tool", "%d rows" % _tp.item_count())
_p(String(_tp.item_text_of("builder.create_box")).contains("Box"), "rows are named like the toolbar",
String(_tp.item_text_of("builder.create_box")))
_p(_tp.accel_text_of("builder.move") == "G", "and show the live key", _tp.accel_text_of("builder.move"))
_p(not _km.is_context_active("builder"), "while open, builder keys do NOT fire (menu context)")
_p(not bool(_km.movement_allowed()), "and the camera does not fly")
_tp.pick("builder.move")
56:
_p(not bool(_tp.is_open()), "a pick closes it")
_p(String(_tp.last_run) == "builder.move", "and hands the tool to Keymap.run", String(_tp.last_run))
_p(_km.is_context_active("builder") and bool(_km.movement_allowed()), "menu context gone: keys + flying back")
var st: Node = _map.call("_ensure_ops")
if st.get("active") != null:
st.cancel()
_tp.last_run = ""
_press(KEY_SPACE, true)
59:
_p(bool(_tp.is_open()), "reopens")
_press(KEY_K) # the knife's key since keymap step 3
62:
_p(not bool(_tp.is_open()) and String(_tp.last_run) == "builder.knife",
"pressing a tool's key while it is open picks that tool", String(_tp.last_run))
var st2: Node = _map.call("_ensure_ops")
if st2.get("active") != null:
st2.cancel()
_tp.last_run = ""
_press(KEY_SPACE, true)
65:
_press(KEY_ESCAPE)
68:
_p(not bool(_tp.is_open()) and String(_tp.last_run) == "", "Escape closes it and runs nothing")
_p(_km.is_context_active("builder"), "builder keys live again")
print("\n-- E3d: the RMB-tap context menu --")
_cm = _map.get("_context_menu")
_p(_cm != null, "the map owns a context menu")
if _cm == null:
return _finish()
_rmb(true, Vector2(400, 300))
_rmb(false, Vector2(400, 300))
71:
_p(bool(_cm.is_open()), "a quick RMB tap opens it")
_p(_cm.has_action("builder.create_box") and _cm.has_action("builder.palette"),
"nothing selected: Add & view (new box, palette ...)")
_p(not _cm.has_action("build_mode.csg_subtract"), "no CSG without a selection")
_p(not _km.is_context_active("builder") and not bool(_km.movement_allowed()), "menu context while open")
_cm.pick("builder.grid_coarser")
_map.set_grid_index(5)
76:
_p(not bool(_cm.is_open()) and int(_map.get("_grid_idx")) == 6, "a pick runs it (grid 8u -> 16u)",
str(_map.get("_grid_idx")))
# A DRAG is the look, not a tap.
_rmb(true, Vector2(400, 300))
var mm := InputEventMouseMotion.new()
mm.relative = Vector2(40, 0)
Input.parse_input_event(mm)
_rmb(false, Vector2(400, 300))
80:
_p(not bool(_cm.is_open()), "an RMB drag (the look) opens nothing")
# Two brushes selected -> the Brush section with CSG.
_map.set("_sel_brushes", [1, 2] as Array[int])
_rmb(true, Vector2(400, 300))
_rmb(false, Vector2(400, 300))
83:
_p(bool(_cm.is_open()) and _cm.has_action("build_mode.csg_subtract") and _cm.has_action("builder.delete"),
"two brushes selected: Brush section with CSG and delete")
_press(KEY_ESCAPE)
86:
_p(not bool(_cm.is_open()), "Escape closes it")
_map.set("_sel_brushes", [] as Array[int])
print("\n-- E3e: modifier hints --")
_hud = root.get_node("/root/BuildHud")
var st: String = _hud.status_text()
_p(st.contains("RMB") and st.contains("menu") and st.contains("Space"),
"idle hints mention the RMB-tap menu and Space search")
_key_state(KEY_SHIFT, true)
_t_mod = Time.get_ticks_msec()
87:
if Time.get_ticks_msec() - _t_mod < 120:
_frame -= 1 # wait real time, not frames
return false
_p(not String(_hud.status_text()).begins_with("[b]Shift"), "a short Shift press shows nothing yet")
88:
if Time.get_ticks_msec() - _t_mod < 450:
_frame -= 1
return false
var t: String = _hud.status_text()
_p(t.begins_with("[b]Shift"), "held ~300 ms: the bar lists what Shift does", t.left(80))
# Keymap step 1: Shift flies fast only WHILE flying; alone it means nothing here.
_p(not t.contains("fly fast"), "no tool up, not flying: Shift alone means nothing")
_p(t.contains("Duplicate") and t.contains("Snap to grid"), "and every Shift chord live now (Shift+D duplicate, Shift+Tab snap)")
_p(not t.contains("Ctrl+"), "only Shift chords")
_key_state(KEY_SHIFT, false)
90:
_p(not String(_hud.status_text()).begins_with("[b]Shift"), "released: the idle hints come back")
_p(_hud.has_method("modifier_hint") and not String(_hud.modifier_hint("ctrl")).contains("fly down"),
"Ctrl with no tool up is not 'fly down' any more (only while flying)")
print("\n-- E3f: the first-run card --")
var gm: Node = root.get_node("/root/GameMode")
gm.set_mode("play")
_dock.set_pref("first_run_seen", false)
gm.set_mode("build")
92:
_p(_hud.has_method("first_run_visible") and bool(_hud.first_run_visible()),
"first time in build mode: the card shows")
if not _hud.has_method("first_run_text"):
return _finish()
var ct: String = _hud.first_run_text()
_p(ct.contains("F1") and ct.contains("search every command") and ct.contains("Shift+Space")
and ct.contains("RMB") and ct.contains("Shift+F"),
"it names the ways in, with the LIVE keys (Space search, Shift+F fly)", ct.left(120))
_hud.dismiss_first_run()
_p(not bool(_hud.first_run_visible()) and bool(_dock.pref("first_run_seen", false)),
"dismissed, and remembered")
var gm2: Node = root.get_node("/root/GameMode")
gm2.set_mode("play")
gm2.set_mode("build")
94:
_p(not bool(_hud.first_run_visible()), "never again for this player")
print("\n-- E3g: the wheel drives the active tool --")
_p(_km.describe("builder.tool_more") == "Wheel down" and _km.describe("builder.tool_less") == "Wheel up",
"wheel down = bigger, up = smaller (user: inverted)", "%s / %s" % [_km.describe("builder.tool_more"), _km.describe("builder.tool_less")])
_map.set_grid_index(5)
var cam := Camera3D.new()
root.add_child(cam)
cam.look_at_from_position(Vector3(0, 3, 2), Vector3(0, 0.5, 0))
cam.make_current()
_wheel(MOUSE_BUTTON_WHEEL_UP)
96:
_p(int(_map.get("_grid_idx")) == 5 and _map.call("_ensure_ops").get("active") == null,
"no tool up: the wheel changes nothing in the level")
_km.run("builder.create_box") # menu-only since keymap step 3 (N = sidebar)
99:
var op: Variant = _map.call("_ensure_ops").get("active")
# CONTAINS, not begins_with: the parametric box's status leads with its type
# ("Box 16u x 16u x 16u — editable after placing", parametric-shapes-design §13).
_p(op != null and String(op.status()).contains("16u x 16u x 16u"), "box tool up at 16u",
String(op.status()) if op != null else "no tool")
if op == null:
return _finish()
_wheel(MOUSE_BUTTON_WHEEL_DOWN)
102:
var op2: Variant = _map.call("_ensure_ops").get("active")
_p(String(op2.status()).contains("24u x 24u x 24u"), "wheel DOWN grows the box one step", String(op2.status()))
_wheel(MOUSE_BUTTON_WHEEL_UP)
_wheel(MOUSE_BUTTON_WHEEL_UP)
105:
var op3: Variant = _map.call("_ensure_ops").get("active")
_p(String(op3.status()).contains("8u x 8u x 8u") and not String(op3.status()).contains("18u"),
"wheel UP shrinks it", String(op3.status()))
_p(int(_map.get("_grid_idx")) == 5, "and the grid is untouched")
_map.call("_ensure_ops").cancel()
return _finish()
return false
func _finish() -> bool:
# The box tool remembers its size now (E4 sticky prefs); the wheel steps above changed
# it, so put the player's own value back (absent stays absent).
for k: String in _saved_tool:
if _saved_tool[k] == null:
_dock._prefs.erase(k)
else:
_dock._prefs[k] = _saved_tool[k]
_dock.set_pref("palette_recent", _saved_recent)
_dock.set_pref("grid", _saved_grid)
_dock.set_pref("first_run_seen", _saved_seen)
_dock.set_pref("snap", _saved_snap)
if checks < 62:
_p(false, "only %d checks ran" % checks)
print("GATE editor_e3: %s (%d checks)" % ["PASS" if ok else "FAIL", checks])
quit(0 if ok else 1)
return true