142 lines
4.4 KiB
GDScript
142 lines
4.4 KiB
GDScript
extends SceneTree
|
|
|
|
## Visual and structural gate for first-person movement handling.
|
|
##
|
|
## Run:
|
|
## godot --path . --resolution 1280x720 \
|
|
## --script res://debug/fp_movement_capture.gd -- <out_dir>
|
|
|
|
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)
|