OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_fly_modal.gd

130 lines · gd ref:
extends SceneTree
## GATE — keymap step 1: NO PERMANENT WASD, Shift+F fly modal, suppress while flying
## (docs/keymap-blender-279.md §2a; user 2026-10-12 "suppress while flying like blender").
##
##   1. build mode, not flying: W does NOT move the camera; the `fly` context is off
##   2. RMB held: `fly` is on (exclusive — builder suppressed), W moves; released: builder back
##   3. Shift+F: the fly modal — mouse captured, W moves, E rises; Esc CANCELS back to the
##      start (position and view); Shift+F again + Enter CONFIRMS (keeps the new view)
##   4. the freed keys: Ctrl+Numpad − / * / ÷ = subtract / intersect / slice; hotspot fit
##      is Shift+U; Shift+F is fly
##
## Run: HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_fly_modal.gd

var ok := true
var checks := 0
var _frame := 0
var _km: Node
var _gm: Node
var _ply: Node3D
var _p0 := Vector3.ZERO
var _yaw0 := 0.0


func _p(cond: bool, what: String, note: Variant = "") -> void:
	checks += 1
	if not cond:
		ok = false
	print("  %-62s %s  %s" % [what, "OK" if cond else "**FAIL**", str(note)])


func _key(code: Key, shift := false) -> InputEventKey:
	var e := InputEventKey.new()
	e.keycode = code
	e.physical_keycode = code
	e.pressed = true
	e.shift_pressed = shift
	return e


func _process(_d: float) -> bool:
	_frame += 1
	match _frame:
		1:
			_km = root.get_node("/root/Keymap")
			_gm = root.get_node("/root/GameMode")
			# GameMode finds the local player under Game/Players (as in a real session).
			var game := Node.new()
			game.name = "Game"
			root.add_child(game)
			var players := Node.new()
			players.name = "Players"
			game.add_child(players)
			_ply = (load("res://scenes/player/player_procedural.tscn") as PackedScene).instantiate()
			players.add_child(_ply)
			_ply.global_position = Vector3(0, 50, 0)
		6:
			_gm.set_mode("build")
			_km.push_context("builder")        # the level map pushes it; there is no map here
			print("-- 4. keys --")
			var sub: Dictionary = _km.binding_of("build_mode.csg_subtract")
			_p(int(sub.get("key", 0)) == KEY_KP_SUBTRACT and bool(sub.get("ctrl", false)), "Ctrl+Numpad − subtracts", sub)
			var isx: Dictionary = _km.binding_of("build_mode.csg_intersect")
			_p(int(isx.get("key", 0)) == KEY_KP_MULTIPLY and bool(isx.get("ctrl", false)), "Ctrl+Numpad * intersects", isx)
			var sl: Dictionary = _km.binding_of("build_mode.csg_slice")
			_p(int(sl.get("key", 0)) == KEY_KP_DIVIDE and bool(sl.get("ctrl", false)), "Ctrl+Numpad / slices", sl)
			var hs: Dictionary = _km.binding_of("builder.uv_hotspot")
			_p(int(hs.get("key", 0)) == KEY_U and bool(hs.get("shift", false)), "hotspot fit is Shift+U", hs)
			var fl: Dictionary = _km.binding_of("builder.fly")
			_p(int(fl.get("key", 0)) == KEY_F and bool(fl.get("shift", false)), "Shift+F is fly", fl)
			_p(_km.CONTEXTS.has("fly") and bool(_km.CONTEXTS["fly"].get("exclusive", false)), "the fly context is exclusive")
			print("\n-- 1. not flying: W does not move the camera --")
			_p(not _km.is_context_active("fly"), "fly context off in build mode")
			_p(_km.is_context_active("builder"), "builder on")
			_p0 = _ply.global_position
			Input.action_press("move_forward")
		26:
			_p(_ply.global_position.distance_to(_p0) < 0.01, "W held 20 frames: the camera did not move",
				"%.3f m" % _ply.global_position.distance_to(_p0))
			print("\n-- 2. RMB held: fly --")
			_gm.set("_temp_look", true)
			_p(_km.is_context_active("fly"), "fly context ON while RMB is held")
			_p(not _km.is_context_active("builder"), "builder SUPPRESSED while flying (Blender's modal rule)")
			_p0 = _ply.global_position
		46:
			_p(_ply.global_position.distance_to(_p0) > 0.05, "RMB + W flies", "%.3f m" % _ply.global_position.distance_to(_p0))
			Input.action_release("move_forward")
			_gm.set("_temp_look", false)
			_p(not _km.is_context_active("fly") and _km.is_context_active("builder"), "RMB released: fly off, builder back")
			print("\n-- 3. Shift+F fly modal, cancel --")
			_p0 = _ply.global_position
			_yaw0 = float(_ply.get("camera_head").rotation.y)
			_km.run("builder.fly")
		50:
			_p(bool(_gm.call("is_fly_modal")), "Shift+F entered the fly modal")
			_p(_km.is_context_active("fly") and not bool(_gm.wants_pointer()), "fly context on, mouse captured")
			_ply.get("camera_head").rotation.y = _yaw0 + 0.7          # look somewhere else
			Input.action_press("move_forward")
			Input.action_press("fly.up")
		70:
			var moved := _ply.global_position - _p0
			_p(moved.length() > 0.05 and moved.y > 0.05, "W moves and E rises inside the modal", str(moved))
			Input.action_release("move_forward")
			Input.action_release("fly.up")
			_gm._unhandled_key_input(_key(KEY_ESCAPE))
		74:
			_p(not bool(_gm.call("is_fly_modal")) and not _km.is_context_active("fly"), "Esc left the modal")
			_p(_ply.global_position.distance_to(_p0) < 0.01, "Esc CANCELS: back to the start position",
				"%.3f m" % _ply.global_position.distance_to(_p0))
			_p(absf(float(_ply.get("camera_head").rotation.y) - _yaw0) < 0.001, "... and the start view",
				[_yaw0, _ply.get("camera_head").rotation.y])
			_p(_km.is_context_active("builder") and bool(_gm.wants_pointer()), "builder back, cursor back")
			print("\n-- 3. Shift+F, confirm --")
			_km.run("builder.fly")
		78:
			Input.action_press("move_forward")
		98:
			Input.action_release("move_forward")
			var p1 := _ply.global_position
			_gm._unhandled_key_input(_key(KEY_ENTER))
			_p(not bool(_gm.call("is_fly_modal")), "Enter left the modal")
			_p(_ply.global_position.distance_to(p1) < 0.01 and p1.distance_to(_p0) > 0.05,
				"Enter CONFIRMS: the new position is kept", "%.3f m from start" % p1.distance_to(_p0))
			_gm.set_mode("play")
			if checks < 20:
				_p(false, "only %d checks ran" % checks)
			print("\nGATE fly_modal: %s (%d checks)" % ["PASS" if ok else "FAIL", checks])
			quit(0 if ok else 1)
			return true
	return false