extends Node class_name MovementStateMachineTest var sm: MovementStateMachine var fake_player: CharacterBody3D var params: MovementParams var fail_log := [] var tests_passed: int = 0 var tests_failed: int = 0 func run_all() -> void: print("=== MovementStateMachine tests ===") tests_passed = 0 tests_failed = 0 params = MovementParams.new() fake_player = CharacterBody3D.new() fake_player.set_floor_max_angle(0.01) sm = MovementStateMachine.new() sm.player = fake_player sm.params = params var tests := [ test_states_initialize_with_ground, test_ground_transitions_to_air_on_jump, test_air_transitions_to_ground_on_land, test_double_jump_allowed_once, test_wall_run_started_when_near_wall, test_chain_bonus_caps_at_50pct, test_sliding_reduces_speed, test_dash_speed_under_effective_cap, ] for t in tests: _fresh() call(t) var ok := fail_log.is_empty() print(" %s: %s" % ["PASS" if ok else "FAIL", t]) if ok: tests_passed += 1 else: tests_failed += 1 for msg in fail_log: print(" ", msg) fail_log.clear() print("=== Results: %d passed, %d failed ===" % [tests_passed, tests_failed]) if tests_failed > 0: print("SOME TESTS FAILED") get_tree().quit(1) else: print("ALL TESTS PASSED") get_tree().quit(0) # ── helpers ───────────────────────────────────────────────────────────────── func _fresh() -> void: sm.current_state = "" sm.chain_count = 0 sm.current_chain_bonus = 0.0 sm.chain_timer = 0.0 sm.on_ground = false sm.coyote_timer = 0.0 sm.jump_buffer_time = 0.0 sm.current_jump_count = 0 sm.wall_normal = Vector3.ZERO sm.input_dir = Vector2.ZERO sm.input_jump_just_pressed = false sm.input_jump_pressed = false sm.input_sprint = false sm.input_crouch = false sm.input_dash = false fake_player.velocity = Vector3.ZERO for c in sm.get_children(): sm.remove_child(c.queue_free()) func _expect(cond: bool, msg: String) -> void: if not cond: fail_log.append(msg) func _eq(a: Variant, b: Variant, msg: String = "") -> void: if a != b: fail_log.append(msg if not msg.is_empty() else ("expected %s == %s" % [a, b])) # ── tests ─────────────────────────────────────────────────────────────────── func test_states_initialize_with_ground() -> void: var g = Node.new() g.name = "state_ground" g.machine = sm sm.add_child(g) var a = Node.new() a.name = "state_air" a.machine = sm sm.add_child(a) sm.switch_to("ground") _eq(sm.current_state, "ground") func test_ground_transitions_to_air_on_jump() -> void: var g = Node.new() g.name = "state_ground" g.machine = sm sm.add_child(g) var a = Node.new() a.name = "state_air" a.machine = sm sm.add_child(a) sm.on_ground = true sm.switch_to("air") _eq(sm.current_state, "air") func test_air_transitions_to_ground_on_land() -> void: var a = Node.new() a.name = "state_air" a.machine = sm sm.add_child(a) sm.switch_to("ground") sm.on_ground = false fake_player.velocity = Vector3.ZERO sm.switch_to("air") fake_player.velocity = Vector3.ZERO sm.switch_to("ground") _eq(sm.current_state, "ground") func test_double_jump_allowed_once() -> void: params.double_jump_max_count = 1 var a = Node.new() a.name = "state_air" a.machine = sm sm.add_child(a) sm.on_ground = false sm.current_jump_count = 1 _eq(sm.current_jump_count, 1) _expect( sm.current_jump_count < params.double_jump_max_count + 1, "should allow double jump once" ) func test_wall_run_started_when_near_wall() -> void: var g = Node.new() g.name = "state_ground" g.machine = sm sm.add_child(g) var a = Node.new() a.name = "state_air" a.machine = sm sm.add_child(a) sm.wall_normal = Vector3.RIGHT.normalized() sm.switch_to("wall_run") _eq(sm.current_state, "wall_run") func test_chain_bonus_caps_at_50pct() -> void: params.chain_bonus_per_success = 0.05 params.chain_bonus_cap = 0.50 sm.register_chain_mechanic("jump") sm.register_chain_mechanic("slide") sm.register_chain_mechanic("wall_run") _expect(sm.chain_count == 3, "chain_count 3 after 3 mechanics") _expect( sm.current_chain_bonus <= params.chain_bonus_cap + 0.001, "bonus should be bounded by cap" ) func test_sliding_reduces_speed() -> void: params.slide_speed = 14.0 params.slide_min_speed = 12.0 _expect(params.slide_min_speed < params.slide_speed, "slide_min_speed must be below slide_speed") func test_dash_speed_under_effective_cap() -> void: var d = Node.new() d.name = "state_dash" d.machine = sm sm.add_child(d) sm.on_ground = false var eff := sm.get_effective_speed(params.dash_speed) _expect( eff <= params.dash_speed * (1.0 + params.chain_bonus_cap), "dash effective speed should respect chain cap" ) func test_rocket_jump_impulse_sets_upward_velocity() -> void: params.rocket_jump_up_impulse = 20.0 _expect(params.rocket_jump_up_impulse > 0.0, "rocket jump must have positive upward impulse")