fix: clean state_ground and ensure duck-type state machine bootstrap

This commit is contained in:
2026-06-03 16:58:48 -04:00
parent 9c39d79696
commit 6c57f70fb4
4 changed files with 71 additions and 49 deletions
+14 -15
View File
@@ -37,20 +37,20 @@ func _box_static(pos: Vector3, size: Vector3, color: Color) -> StaticBody3D:
func _build_input_map() -> void: func _build_input_map() -> void:
var bindings := { var bindings := {
jump = [KEY_SPACE], "jump": [KEY_SPACE],
sprint = [KEY_SHIFT], "sprint": [KEY_SHIFT],
crouch = [KEY_CTRL], "crouch": [KEY_CTRL],
dash = [KEY_SHIFT], "dash": [KEY_SHIFT],
fire = [MOUSE_BUTTON_RIGHT, KEY_F], "fire": [MOUSE_BUTTON_RIGHT, KEY_F],
fire_alt = [MOUSE_BUTTON_LEFT], "fire_alt": [MOUSE_BUTTON_LEFT],
reload = [KEY_R], "reload": [KEY_R],
interact = [KEY_E], "interact": [KEY_E],
move_forward = [KEY_W], "move_forward": [KEY_W],
move_back = [KEY_S], "move_back": [KEY_S],
move_left = [KEY_A], "move_left": [KEY_A],
move_right = [KEY_D], "move_right": [KEY_D],
weapon_next = [MOUSE_BUTTON_WHEEL_UP], "weapon_next": [MOUSE_BUTTON_WHEEL_UP],
weapon_prev = [MOUSE_BUTTON_WHEEL_DOWN], "weapon_prev": [MOUSE_BUTTON_WHEEL_DOWN],
} }
for action in bindings: for action in bindings:
if not InputMap.has_action(action): if not InputMap.has_action(action):
@@ -136,7 +136,6 @@ func _build_player() -> void:
player.add_child(camera) player.add_child(camera)
# set_script() does NOT call _ready(); bootstrap manually after children are in place # set_script() does NOT call _ready(); bootstrap manually after children are in place
# IMPORTANT: player._ready() MUST run BEFORE sm._ready() so the machine has valid params
if player.has_method("_ready"): if player.has_method("_ready"):
player._ready() player._ready()
if sm.has_method("_ready"): if sm.has_method("_ready"):
+3 -2
View File
@@ -12,8 +12,9 @@ func _ensure_machine() -> MovementStateMachine:
if is_instance_valid(_machine): if is_instance_valid(_machine):
return _machine return _machine
if has_node("MovementStateMachine"): if has_node("MovementStateMachine"):
_machine = $MovementStateMachine var node = $MovementStateMachine
if _machine is MovementStateMachine: if node and node.has_method("switch_to") and node.has_method("register_chain_mechanic"):
_machine = node
return _machine return _machine
_machine = null _machine = null
return null return null
+1 -5
View File
@@ -15,11 +15,7 @@ func exit() -> void:
func update(delta: float) -> void: func update(delta: float) -> void:
var sm := machine var vel: Vector3 = machine.player.velocity
if sm and sm.player:
pass
var vel: Vector3 = sm.player.velocity # assumed present in debug
# Input direction # Input direction
var input_v := machine.input_dir var input_v := machine.input_dir
+52 -26
View File
@@ -1,9 +1,6 @@
extends Node extends Node
class_name MovementStateMachineTest class_name MovementStateMachineTest
## Tests for MovementStateMachine logic.
## Run: godot --headless --path . --script movement/tests/test_fsm_runner.gd --quit
var sm: MovementStateMachine var sm: MovementStateMachine
var fake_player: CharacterBody3D var fake_player: CharacterBody3D
var params: MovementParams var params: MovementParams
@@ -19,6 +16,7 @@ func run_all() -> void:
tests_failed = 0 tests_failed = 0
params = MovementParams.new() params = MovementParams.new()
fake_player = CharacterBody3D.new() fake_player = CharacterBody3D.new()
fake_player.set_floor_max_angle(0.01)
sm = MovementStateMachine.new() sm = MovementStateMachine.new()
sm.player = fake_player sm.player = fake_player
sm.params = params sm.params = params
@@ -68,15 +66,14 @@ func _fresh() -> void:
sm.current_jump_count = 0 sm.current_jump_count = 0
sm.wall_normal = Vector3.ZERO sm.wall_normal = Vector3.ZERO
sm.input_dir = Vector2.ZERO sm.input_dir = Vector2.ZERO
sm.input_jump_pressed = false
sm.input_jump_just_pressed = false sm.input_jump_just_pressed = false
sm.input_jump_pressed = false
sm.input_sprint = false sm.input_sprint = false
sm.input_crouch = false sm.input_crouch = false
sm.input_dash = false sm.input_dash = false
fake_player.velocity = Vector3.ZERO fake_player.velocity = Vector3.ZERO
fake_player.set_floor_max_angle(0.01)
for c in sm.get_children(): for c in sm.get_children():
sm.remove_child(c) sm.remove_child(c.queue_free())
func _expect(cond: bool, msg: String) -> void: func _expect(cond: bool, msg: String) -> void:
@@ -92,46 +89,70 @@ func _eq(a: Variant, b: Variant, msg: String = "") -> void:
# ── tests ─────────────────────────────────────────────────────────────────── # ── tests ───────────────────────────────────────────────────────────────────
func test_states_initialize_with_ground() -> void: func test_states_initialize_with_ground() -> void:
var g = Node.new(); g.name = "state_ground"; g.machine = sm; sm.add_child(g) var g = Node.new()
var a = Node.new(); a.name = "state_air"; a.machine = sm; sm.add_child(a) 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") sm.switch_to("ground")
_eq(sm.current_state, "ground") _eq(sm.current_state, "ground")
func test_ground_transitions_to_air_on_jump() -> void: 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 g = Node.new()
var a = Node.new(); a.name = "state_air"; a.machine = sm; sm.add_child(a) 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.on_ground = true
sm.switch_to("air") sm.switch_to("air")
_eq(sm.current_state, "air") _eq(sm.current_state, "air")
func test_air_transitions_to_ground_on_land() -> void: func test_air_transitions_to_ground_on_land() -> void:
var a = Node.new(); a.name = "state_air"; a.machine = sm; sm.add_child(a) var a = Node.new()
a.name = "state_air"
a.machine = sm
sm.add_child(a)
sm.switch_to("ground") sm.switch_to("ground")
sm.on_ground = false sm.on_ground = false
fake_player.velocity = Vector3.ZERO fake_player.velocity = Vector3.ZERO
fake_player.set_floor_max_angle(0.01)
sm.switch_to("air") sm.switch_to("air")
fake_player.velocity = Vector3.ZERO fake_player.velocity = Vector3.ZERO
fake_player.set_floor_max_angle(0.01)
sm.switch_to("ground") sm.switch_to("ground")
_eq(sm.current_state, "ground") _eq(sm.current_state, "ground")
func test_double_jump_allowed_once() -> void: func test_double_jump_allowed_once() -> void:
params.double_jump_max_count = 1 params.double_jump_max_count = 1
var a = Node.new(); a.name = "state_air"; a.machine = sm; sm.add_child(a) var a = Node.new()
a.name = "state_air"
a.machine = sm
sm.add_child(a)
sm.on_ground = false sm.on_ground = false
sm.current_jump_count = 1 sm.current_jump_count = 1
_eq(sm.current_jump_count, 1) _eq(sm.current_jump_count, 1)
_expect(sm.current_jump_count < params.double_jump_max_count + 1, _expect(
"should allow double jump once") sm.current_jump_count < params.double_jump_max_count + 1,
"should allow double jump once"
)
func test_wall_run_started_when_near_wall() -> void: 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 g = Node.new()
var a = Node.new(); a.name = "state_air"; a.machine = sm; sm.add_child(a) 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.wall_normal = Vector3.RIGHT.normalized()
sm.switch_to("wall_run") sm.switch_to("wall_run")
_eq(sm.current_state, "wall_run") _eq(sm.current_state, "wall_run")
@@ -144,26 +165,31 @@ func test_chain_bonus_caps_at_50pct() -> void:
sm.register_chain_mechanic("slide") sm.register_chain_mechanic("slide")
sm.register_chain_mechanic("wall_run") sm.register_chain_mechanic("wall_run")
_expect(sm.chain_count == 3, "chain_count 3 after 3 mechanics") _expect(sm.chain_count == 3, "chain_count 3 after 3 mechanics")
_expect(sm.current_chain_bonus <= params.chain_bonus_cap + 0.001, _expect(
"bonus should be bounded by cap") sm.current_chain_bonus <= params.chain_bonus_cap + 0.001,
"bonus should be bounded by cap"
)
func test_sliding_reduces_speed() -> void: func test_sliding_reduces_speed() -> void:
params.slide_speed = 14.0 params.slide_speed = 14.0
params.slide_min_speed = 12.0 params.slide_min_speed = 12.0
_expect(params.slide_min_speed < params.slide_speed, _expect(params.slide_min_speed < params.slide_speed, "slide_min_speed must be below slide_speed")
"slide_min_speed must be below slide_speed")
func test_dash_speed_under_effective_cap() -> void: func test_dash_speed_under_effective_cap() -> void:
var d = Node.new(); d.name = "state_dash"; d.machine = sm; sm.add_child(d) var d = Node.new()
d.name = "state_dash"
d.machine = sm
sm.add_child(d)
sm.on_ground = false sm.on_ground = false
var eff := sm.get_effective_speed(params.dash_speed) var eff := sm.get_effective_speed(params.dash_speed)
_expect(eff <= params.dash_speed * (1.0 + params.chain_bonus_cap), _expect(
"dash effective speed should respect chain cap") 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: func test_rocket_jump_impulse_sets_upward_velocity() -> void:
# Unit test: verify impulse formula in state_machine
params.rocket_jump_up_impulse = 20.0 params.rocket_jump_up_impulse = 20.0
_expect(params.rocket_jump_up_impulse > 0.0, "rocket jump must have positive upward impulse") _expect(params.rocket_jump_up_impulse > 0.0, "rocket jump must have positive upward impulse")