extends SceneTree
## GATE — editor redesign E4 (docs/editor-redesign.md §8b), on a real map_flat.
## E4a every tool has a settings page (tool_settings() with a commit action);
## E4b the TOOL SETTINGS ROW is generated from it, follows the keys, and drives the tool:
## ] shows in the row, a step button changes what ] turns by, a typed angle sets it,
## the Rotate button commits like Enter; the crosshair readout says what the tool will do;
## E4c sticky settings: a fresh operator starts with the last step the player chose;
## E4d a typed move distance HOLDS (the aim stops driving it) and commits that distance;
## E4e box settings: shape, size, height (re-links to size), the shape's own params;
## E4f a shape REGISTERED BY THIS GATE appears in the shape list and creates brushes,
## with no edit to the operator.
##
## Run: HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_editor_e4.gd
## A plugin shape: N horizontal slabs filling the bounds. Lives in the gate on purpose —
## proving a shape needs nothing from the operator but register().
class SlabShape extends BrushShape:
func id() -> String:
return "gate_slabs"
func label() -> String:
return "Slabs"
func params() -> Array:
return [{"id": "layers", "label": "Layers", "type": "int", "default": 2, "min": 1, "max": 8}]
func pieces(lo: Vector3, hi: Vector3, ctx: Dictionary) -> Array:
var n := int(param(ctx, "layers"))
var vert := int(ctx["axis"])
var out: Array = []
for i in n:
var a := lo
var b := hi
a[vert] = lerpf(lo[vert], hi[vert], float(i) / n)
b[vert] = lerpf(lo[vert], hi[vert], (float(i) + 0.5) / n)
out.append(ShapeKit.box_piece(a, b))
return out
const PINNED := ["tool.box.shape", "tool.box.size", "tool.box.height", "tool.box.p.stairs.steps",
"tool.box.p.arch.segments", "tool.box.p.gate_slabs.layers", "tool.rotate.step", "snap", "grid"]
var ok := true
var checks := 0
var _frame := 0
var _map: Node
var _level: Node
var _persist: Node
var _dock: Node
var _stack: Node
var _bar: Node
var _saved: Dictionary = {}
var _b := -1
var _bb := AABB()
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 _key(code: int, shift := false) -> InputEventKey:
var e := InputEventKey.new()
e.physical_keycode = code
e.keycode = code
e.shift_pressed = shift
e.pressed = true
return e
func _box(lo: Vector3, hi: Vector3) -> Array:
return [1, 0, 0, hi.x, -1, 0, 0, -lo.x, 0, 1, 0, hi.y, 0, -1, 0, -lo.y, 0, 0, 1, hi.z, 0, 0, -1, -lo.z]
func _make(lo: Vector3, hi: Vector3) -> int:
var ft: Array = []
for i in 6:
ft.append({"tex": "kenney-prototype/dark_01", "mode": 1})
var dot: String = _persist.record("brush.create", {"planes": _box(lo, hi), "proj": [], "material_id": 1,
"level_id": 0, "sky": false, "face_tex": ft})
_persist.refold()
return _level.brush_by_key(dot)
func _bounds(b: int) -> AABB:
var d: Dictionary = _level.get_brush(b)
return AABB(d["aabb_min"], (d["aabb_max"] as Vector3) - (d["aabb_min"] as Vector3))
func _row(op: LevelOperator, sid: String) -> Dictionary:
for d: Dictionary in op.tool_settings():
if String(d.get("id")) == sid:
return d
return {}
func _process(_d: float) -> bool:
_frame += 1
match _frame:
1:
# Gates share the installed game's user:// — pin the tool prefs, restore at the end.
_dock = root.get_node("/root/BuildDock")
for k: String in PINNED:
_saved[k] = _dock._prefs.get(k, null)
_dock._prefs.erase(k)
_dock.set_pref("snap", true)
_dock.set_pref("grid", 5)
_map = (load("res://scenes/maps/map_flat.tscn") as PackedScene).instantiate()
root.add_child(_map)
30:
_setup()
32:
_e4a()
_e4b_start()
34:
_e4b_sync()
36:
_e4b_drive()
38:
_e4b_commit()
40:
_e4c()
_e4d()
_e4e()
_e4f()
return _finish()
return false
func _setup() -> 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")
root.get_node("/root/GameMode").set_mode("build")
_stack = _map.call("_ensure_ops")
_bar = _map.get("_tool_settings")
_b = _make(Vector3(0, 0, 0), Vector3(2, 1, 1))
func _e4a() -> void:
print("-- E4a: every tool has a page --")
for n: String in ["_box", "_knife", "_move", "_rotate", "_extrude", "_grab_op"]:
var op: LevelOperator = _map.get(n)
var rows: Array = op.tool_settings() if op != null else []
var has_action := false
for d: Dictionary in rows:
has_action = has_action or String(d.get("type")) == "action"
_p(rows.size() >= 2 and has_action, "%s has settings and a commit button" % n, rows.size())
func _e4b_start() -> void:
print("-- E4b: the settings row drives the rotate tool --")
_p(_bar != null, "the map registered the tool settings row")
_map.select_brushes(PackedInt32Array([_b]))
_map._unhandled_key_input(_key(KEY_R))
_p(_stack.active is RotateOperator, "R starts rotate")
func _e4b_sync() -> void:
_p(_bar.control("axis") != null and _bar.control("angle") != null and _bar.control("step") != null
and _bar.control("rotate") != null, "the row shows axis, angle, step and Rotate", _bar.setting_ids())
_map._unhandled_key_input(_key(KEY_BRACKETRIGHT))
func _spin(sid: String) -> SpinBox:
var row: Control = _bar.control(sid)
for c in row.get_children():
if c is SpinBox:
return c
return null
func _e4b_drive() -> void:
_p(is_equal_approx(_spin("angle").value, 15.0), "] shows in the row (15°)", _spin("angle").value)
var buttons: Array = _bar._controls["step"]["buttons"] if _bar._controls["step"].has("buttons") else []
var ob: OptionButton = _bar.control("step") as OptionButton
if buttons.size() == 5:
(buttons[4] as Button).pressed.emit() # 90°
elif ob != null:
ob.select(4)
ob.item_selected.emit(4)
_map._unhandled_key_input(_key(KEY_BRACKETRIGHT))
var op: RotateOperator = _stack.active
_p(is_equal_approx(float(op.get("_deg")), 105.0), "the step control changed what ] turns by (15 + 90)", op.get("_deg"))
var hud: Node = root.get_node("/root/BuildHud")
hud._render_readout()
_p(String(hud.readout_text()).contains("+105° Y"), "the crosshair readout says it", hud.readout_text())
_spin("angle").value = 90.0 # typed
_p(is_equal_approx(float(op.get("_deg")), 90.0), "a typed angle sets the tool", op.get("_deg"))
func _e4b_commit() -> void:
_bb = _bounds(_b)
(_bar.control("rotate") as Button).pressed.emit()
var after := _bounds(_b)
_p(_stack.active == null, "the Rotate button commits and leaves the tool")
_p(after.size.is_equal_approx(Vector3(1, 1, 2)), "the brush turned 90°", after)
func _e4c() -> void:
print("-- E4c: sticky settings --")
var fresh := RotateOperator.new()
fresh.map = _map
fresh.level = _level
fresh.load_prefs()
_p(int(fresh.get("_step_idx")) == 4, "a fresh rotate tool starts at the last step chosen (90°)", fresh.get("_step_idx"))
fresh.free()
func _e4d() -> void:
print("-- E4d: a typed move distance holds --")
var mv: MoveOperator = _map.get("_move")
_map.select_brushes(PackedInt32Array([_b]))
_p(_stack.activate(mv), "move starts on the selection")
var before := _bounds(_b)
mv.set_tool_setting("distance", 3)
mv.update_preview()
_p(int(mv.call("_total")) == 3, "typed 3 stays 3 after the aim updates", mv.call("_total"))
_p(String(mv.readout()).begins_with("+3"), "readout", mv.readout())
mv.set_tool_setting("move", true)
var step: float = float(_map.grid_units()) * float(_map.WORLD_SCALE)
var moved := _bounds(_b).position - before.position
_p(moved.is_equal_approx(Vector3(3.0 * step, 0, 0)), "Move commits exactly 3 grid steps along X", moved)
func _place(bx: CreateBoxOperator) -> void:
bx.set("_origin", Vector3(20, 0, 20))
bx.set("_normal", Vector3.UP)
bx.set("_axis", 1)
bx.set("_view", Vector3(0, 0, 1))
bx.set("_valid", true)
func _e4e() -> void:
print("-- E4e: box settings --")
var bx: CreateBoxOperator = _map.get("_box")
_place(bx)
bx.set_tool_setting("shape", CreateBoxOperator.Shape.STAIRS)
bx.set_tool_setting("size", 4)
_p(bx.call("_pieces").size() == 4, "stairs default: one step per grid step of height (4)", bx.call("_pieces").size())
bx.set_tool_setting("p.steps", 2)
_p(bx.call("_pieces").size() == 2, "the shape's own Steps param: 2", bx.call("_pieces").size())
_p(_row(bx, "p.steps").get("value") == 2, "and the row shows it")
var st: float = bx.grid_step()
bx.set_tool_setting("height", 8)
var c: Array = bx.call("_corners")
_p(is_equal_approx((c[1] as Vector3).y - (c[0] as Vector3).y, 8.0 * st)
and is_equal_approx((c[1] as Vector3).x - (c[0] as Vector3).x, 4.0 * st), "height 8 is independent of size 4")
bx.set_tool_setting("height", 4)
_p(int(bx.get("_height_steps")) == 0, "height set back to size re-links it")
bx.set_tool_setting("size", 6)
_p(int(_row(bx, "height").get("value")) == 6, "... so size drives it again", _row(bx, "height").get("value"))
# A setting refreshes the preview from the crosshair, and headless there is no aimed
# surface — so the hand placement goes back after the last setting.
_place(bx)
_p(String(bx.readout()).begins_with("Stairs"), "readout", bx.readout())
func _e4f() -> void:
print("-- E4f: a plugin shape --")
var idx := BrushShapes.register(SlabShape.new())
var bx: CreateBoxOperator = _map.get("_box")
_place(bx)
_p((_row(bx, "shape").get("choices", []) as Array).has("Slabs"), "the registered shape is in the shape list")
# The TOOL's index, not BrushShapes': the tool lists the parametric types first and
# plugins after them (parametric-shapes-design §13).
bx.set_tool_setting("shape", bx.shape_ids().find("gate_slabs"))
bx.set_tool_setting("size", 2)
bx.set_tool_setting("p.layers", 3)
_p(bx.call("_pieces").size() == 3, "its own param drives it (3 layers)", bx.call("_pieces").size())
var n0: int = _level.get_brush_count()
_place(bx)
_p(bx.confirm(), "it creates")
_p(_level.get_brush_count() == n0 + 3, "three brushes, through the operator unchanged", _level.get_brush_count() - n0)
BrushShapes.unregister("gate_slabs")
_p(BrushShapes.index_of("gate_slabs") < 0, "and unregisters")
func _finish() -> bool:
root.get_node("/root/GameMode").set_mode("play")
for k: String in PINNED:
if _saved.get(k) == null:
_dock._prefs.erase(k)
else:
_dock._prefs[k] = _saved[k]
_dock._save_layout()
if checks < 30:
_p(false, "only %d checks ran" % checks)
print("GATE editor_e4: %s (%d checks)" % ["PASS" if ok else "FAIL", checks])
quit(0 if ok else 1)
return true