gmp(M02): extract local player movement
Work-Package: M02-GMP-MOVEMENT-001 Agent: sindo-main-codex Tests: local movement and player input regressions; asset-free scene; renderer dry-run; coordinate, streaming, documentation and coordination gates Fidelity: preserves current sandbox speed, acceleration, sprint and camera-relative flight behavior; build-12340 parity remains unverified
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
extends SceneTree
|
||||
|
||||
## Headless M02 unit and boundary regression for local player movement state.
|
||||
|
||||
const MOVEMENT_CONTROLLER_PATH := "res://src/gameplay/movement/local_player_movement_controller.gd"
|
||||
const PLAYER_CONTROLLER_PATH := "res://src/scenes/player/third_person_wow_controller.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_directional_speeds(failures)
|
||||
_verify_acceleration_and_deceleration(failures)
|
||||
_verify_sandbox_sprint(failures)
|
||||
_verify_flight_state_and_basis(failures)
|
||||
_verify_invalid_delta(failures)
|
||||
_verify_scene_boundary(failures)
|
||||
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("LOCAL_PLAYER_MOVEMENT: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("LOCAL_PLAYER_MOVEMENT PASS cases=12 state_transitions=2 scene_boundary=1")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_directional_speeds(failures: Array[String]) -> void:
|
||||
var forward_controller := _new_controller()
|
||||
var forward_displacement := forward_controller.advance(_intent(1.0, 0.0), Basis.IDENTITY, 1.0)
|
||||
_expect_vector_near(forward_controller.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -7.0), "forward velocity", failures)
|
||||
_expect_vector_near(forward_displacement, Vector3(0.0, 0.0, -7.0), "forward displacement", failures)
|
||||
|
||||
var backward_controller := _new_controller()
|
||||
backward_controller.advance(_intent(-1.0, 0.0), Basis.IDENTITY, 1.0)
|
||||
_expect_vector_near(backward_controller.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, 4.5), "backward velocity", failures)
|
||||
|
||||
var strafe_controller := _new_controller()
|
||||
strafe_controller.advance(_intent(0.0, 1.0), Basis.IDENTITY, 1.0)
|
||||
_expect_vector_near(strafe_controller.godot_world_velocity_units_per_second, Vector3(4.5, 0.0, 0.0), "strafe velocity", failures)
|
||||
|
||||
var rotated_controller := _new_controller()
|
||||
var rotated_basis := Basis(Vector3.UP, deg_to_rad(90.0))
|
||||
rotated_controller.advance(_intent(1.0, 0.0), rotated_basis, 1.0)
|
||||
_expect_vector_near(
|
||||
rotated_controller.godot_world_velocity_units_per_second,
|
||||
(-rotated_basis.z).normalized() * 7.0,
|
||||
"rotated character basis",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_acceleration_and_deceleration(failures: Array[String]) -> void:
|
||||
var controller := _new_controller()
|
||||
var forward_intent := _intent(1.0, 0.0)
|
||||
_expect_vector_near(controller.advance(forward_intent, Basis.IDENTITY, 0.1), Vector3(0.0, 0.0, -0.28), "first acceleration displacement", failures)
|
||||
_expect_vector_near(controller.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -2.8), "first acceleration velocity", failures)
|
||||
controller.advance(forward_intent, Basis.IDENTITY, 0.1)
|
||||
_expect_vector_near(controller.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -5.6), "second acceleration velocity", failures)
|
||||
controller.advance(_intent(0.0, 0.0), Basis.IDENTITY, 0.1)
|
||||
_expect_vector_near(controller.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -2.8), "deceleration velocity", failures)
|
||||
|
||||
|
||||
func _verify_sandbox_sprint(failures: Array[String]) -> void:
|
||||
var controller := _new_controller()
|
||||
controller.advance(_intent(1.0, 0.0, 0.0, true), Basis.IDENTITY, 2.0)
|
||||
_expect_vector_near(controller.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -42.0), "sandbox sprint velocity", failures)
|
||||
|
||||
|
||||
func _verify_flight_state_and_basis(failures: Array[String]) -> void:
|
||||
var controller := _new_controller()
|
||||
controller.advance(_intent(1.0, 0.0), Basis.IDENTITY, 1.0)
|
||||
_expect_true(controller.toggle_sandbox_flight(), "flight toggles on", failures)
|
||||
_expect_vector_near(controller.godot_world_velocity_units_per_second, Vector3.ZERO, "flight toggle clears velocity", failures)
|
||||
|
||||
var pitched_basis := Basis(Vector3.RIGHT, deg_to_rad(30.0))
|
||||
controller.advance(_intent(1.0, 0.0, 1.0), pitched_basis, 1.0)
|
||||
var expected_flight_velocity := (-pitched_basis.z).normalized() * 7.0 + Vector3.UP * 7.0
|
||||
_expect_vector_near(controller.godot_world_velocity_units_per_second, expected_flight_velocity, "camera-relative flight velocity", failures)
|
||||
_expect_true(not controller.toggle_sandbox_flight(), "flight toggles off", failures)
|
||||
_expect_vector_near(controller.godot_world_velocity_units_per_second, Vector3.ZERO, "landing toggle clears velocity", failures)
|
||||
|
||||
|
||||
func _verify_invalid_delta(failures: Array[String]) -> void:
|
||||
var controller := _new_controller()
|
||||
var displacement := controller.advance(_intent(1.0, 0.0), Basis.IDENTITY, -1.0)
|
||||
_expect_vector_near(displacement, Vector3.ZERO, "negative delta displacement", failures)
|
||||
_expect_vector_near(controller.godot_world_velocity_units_per_second, Vector3.ZERO, "negative delta state", failures)
|
||||
|
||||
|
||||
func _verify_scene_boundary(failures: Array[String]) -> void:
|
||||
var movement_source := _read_text(MOVEMENT_CONTROLLER_PATH, failures)
|
||||
for forbidden_text in ["extends Node", "extends Resource", "Input.", "Camera3D", "ADTLoader", "global_position"]:
|
||||
_expect_true(not movement_source.contains(forbidden_text), "movement controller omits %s" % forbidden_text, failures)
|
||||
|
||||
var player_source := _read_text(PLAYER_CONTROLLER_PATH, failures)
|
||||
_expect_true(player_source.contains("_local_movement_controller.advance"), "scene delegates movement update", failures)
|
||||
_expect_true(not player_source.contains("move_toward(target_velocity"), "scene omits velocity integration", failures)
|
||||
_expect_true(not player_source.contains("func _movement_vector"), "scene omits movement-vector calculation", failures)
|
||||
_expect_true(not player_source.contains("_horizontal_velocity"), "scene omits movement velocity state", failures)
|
||||
_expect_true(not player_source.contains("var _flight_enabled"), "scene omits flight state", failures)
|
||||
|
||||
|
||||
func _new_controller() -> LocalPlayerMovementController:
|
||||
return LocalPlayerMovementController.new(7.0, 4.5, 4.5, 7.0, 28.0, 6.0)
|
||||
|
||||
|
||||
func _intent(
|
||||
forward_axis: float,
|
||||
strafe_axis: float,
|
||||
vertical_axis: float = 0.0,
|
||||
sprint_requested: bool = false
|
||||
) -> MoveIntent:
|
||||
return MoveIntent.new(forward_axis, strafe_axis, vertical_axis, sprint_requested)
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
func _expect_vector_near(
|
||||
actual_value: Vector3,
|
||||
expected_value: Vector3,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if not actual_value.is_equal_approx(expected_value):
|
||||
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
Reference in New Issue
Block a user