extends SceneTree ## Visual and structural gate for first-person movement handling. ## ## Run: ## godot --path . --resolution 1280x720 \ ## --script res://debug/fp_movement_capture.gd -- const DT := 1.0 / 60.0 const SETTLE_FRAMES := 42 var _out_dir := "." var _frames := 0 var _booted := false var _player: CharacterBody3D = null var _machine: Node = null var _manager: Node = null var _case_index := 0 var _case_frame := 0 var _failures: Array[String] = [] # tag, state, velocity, wall side, crouching var _cases := [ ["idle", "idle", Vector3.ZERO, 0.0, false], ["jump", "air", Vector3(0.0, 9.0, -6.0), 0.0, false], ["fall", "air", Vector3(0.0, -11.0, -7.0), 0.0, false], ["dash", "dash", Vector3(0.0, 0.0, -18.0), 0.0, false], ["slide", "slide", Vector3(0.0, 0.0, -13.0), 0.0, true], ["wallrun_left", "wall_run", Vector3(0.0, 1.0, -11.0), -1.0, false], ["wallrun_right", "wall_run", Vector3(0.0, 1.0, -11.0), 1.0, false], ["wall_cling", "wall_cling", Vector3(0.0, 0.0, -2.0), -1.0, false], ["grapple", "grapple", Vector3(4.0, 5.0, -15.0), 0.0, false], ] func _initialize() -> void: var args := OS.get_cmdline_user_args() if not args.is_empty(): _out_dir = args[0] if not _out_dir.is_absolute_path(): _out_dir = ProjectSettings.globalize_path(_out_dir) var mkdir_error := DirAccess.make_dir_recursive_absolute(_out_dir) if mkdir_error != OK: printerr("FP_MOVEMENT could not create '%s' (error %d)" % [ _out_dir, mkdir_error]) quit(mkdir_error) return change_scene_to_file("res://ui/main_menu/main_menu.tscn") func _process(_delta: float) -> bool: _frames += 1 if not _booted and _frames == 40: _booted = true var network = root.get_node_or_null("NetworkManager") if network and network.has_method("start_singleplayer_match"): network.start_singleplayer_match(GameMode.DEATHMATCH) change_scene_to_file("res://scenes/maps/test_level/test_level.tscn") return false if _frames < 160: return false if _player == null: if not _setup(): _finish() return true var entry: Array = _cases[_case_index] _machine.current_state = entry[1] _machine.wall_side = entry[3] _machine.input_crouch = entry[4] _player.velocity = entry[2] _manager._process(DT) _case_frame += 1 if _case_frame >= SETTLE_FRAMES: _snap("fp_move_" + String(entry[0])) var pose: Dictionary = _manager.movement_pose_debug() var position: Vector3 = pose.get("position", Vector3.ZERO) var rotation: Vector3 = pose.get("rotation", Vector3.ZERO) if entry[0] != "idle" and position.length() < 0.012 \ and rotation.length() < 0.025: _failures.append("%s did not produce a readable movement pose" % entry[0]) print("FP_MOVEMENT pose %s position=%s rotation_deg=%s" % [ entry[0], position, rotation * (180.0 / PI)]) _case_index += 1 _case_frame = 0 if _case_index >= _cases.size(): _finish() return true return false func _setup() -> bool: for candidate in root.find_children("*", "CharacterBody3D", true, false): if candidate.has_method("get_visual_model") \ and candidate.is_multiplayer_authority(): _player = candidate break if _player == null: _failures.append("local player was not created") return false _machine = _player.get_node_or_null("MovementStateMachine") _manager = _player.get_node_or_null("HeadPivot/Camera3D/WeaponManager") if _machine == null: _failures.append("MovementStateMachine is missing") if _manager == null: _failures.append("WeaponManager is missing") if not _failures.is_empty(): return false _player.set_physics_process(false) _machine.set_physics_process(false) _manager.set_process(false) _player.global_position = Vector3(0.0, 1.2, 14.0) var obsolete := root.find_children( "FirstPersonBody", "Node", true, false) if not obsolete.is_empty(): _failures.append("obsolete first-person body/legs still exist") if _manager.weapons.is_empty(): _failures.append("viewmodel weapon loadout is empty") return _failures.is_empty() func _snap(tag: String) -> void: var image := root.get_viewport().get_texture().get_image() var path := _out_dir.path_join(tag + ".png") var error := image.save_png(path) if error == OK: print("FP_MOVEMENT saved ", path) else: _failures.append("could not save %s (error %d)" % [path, error]) func _finish() -> void: if _failures.is_empty() and _case_index >= _cases.size(): print("FP_MOVEMENT PASS: %d authored poses, no first-person legs" \ % _cases.size()) quit(0) else: for failure in _failures: push_error("FP_MOVEMENT FAIL: " + failure) quit(1)