OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_editor_e4.gd

299 lines · gd ref:
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