feat(movement): add movement state machine with air/ground/wallrun/slide/dash/ground states
- MovementStateMachine: core state machine base - MovementParams: export-rich tunable resource - StateGround/StateAir: grounded & aeriel locomotion with coyote, buffered jump, bunny hop - StateWallRun / StateWallCling: wall-based traversal with wall-jump off - StateSlide: sprint-parallel slide with friction fallout - StateDash: burst dash with momentum brake - PlayerMovementController: entry point wiring to CharacterBody3D - input_map.cfg: full action map reference file - characters/player/player_base.tscn: placeholder scene - movement/tests/test_fsm_runner.gd: 9-state unit test runner Closes #1
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
extends Node
|
||||
class_name MovementStateMachine
|
||||
|
||||
## Generic state machine for player movement.
|
||||
## Each state is a Node child; the machine switches between them.
|
||||
## States must call `self.transition_to(new_state_name)` to change states.
|
||||
|
||||
signal state_changed(from_state: String, to_state: String)
|
||||
signal movement_event(event_name: String, data: Dictionary)
|
||||
|
||||
var current_state: String = ""
|
||||
var states: Dictionary = {}
|
||||
|
||||
var player: CharacterBody3D
|
||||
var params: MovementParams
|
||||
var input_dir: Vector2 = Vector2.ZERO
|
||||
var input_jump_pressed: bool = false
|
||||
var input_jump_just_pressed: bool = false
|
||||
var input_sprint: bool = false
|
||||
var input_crouch: bool = false
|
||||
var input_dash: bool = false
|
||||
var wall_normal: Vector3 = Vector3.ZERO
|
||||
var on_ground: bool = false
|
||||
var last_ground_time: float = 0.0
|
||||
var jump_buffer_time: float = 0.0
|
||||
var coyote_timer: float = 0.0
|
||||
var current_jump_count: int = 0
|
||||
var chain_timer: float = 0.0
|
||||
var chain_count: int = 0
|
||||
var current_chain_bonus: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
for child in get_children():
|
||||
if child is Node and child.name.begins_with("state_"):
|
||||
states[child.name.replace("state_", "")] = child
|
||||
child.machine = self
|
||||
if current_state.is_empty() and not states.is_empty():
|
||||
switch_to(states.keys()[0])
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
# Update coyote time
|
||||
if on_ground:
|
||||
coyote_timer = params.coyote_time
|
||||
else:
|
||||
coyote_timer -= delta
|
||||
|
||||
# Update jump buffer
|
||||
if input_jump_just_pressed:
|
||||
jump_buffer_time = params.jump_buffer
|
||||
else:
|
||||
jump_buffer_time -= delta
|
||||
|
||||
# Update chain timer
|
||||
if chain_timer > 0.0:
|
||||
chain_timer -= delta
|
||||
else:
|
||||
chain_count = 0
|
||||
current_chain_bonus = 0.0
|
||||
|
||||
if current_state.is_empty():
|
||||
return
|
||||
var state_node = states[current_state]
|
||||
if state_node.has_method("update"):
|
||||
state_node.update(delta)
|
||||
# Apply gravity that was accumulated during state update, regardless of state.
|
||||
# CharacterBody3D handles gravity automatically via move_and_slide but
|
||||
# we tune it per-position here.
|
||||
|
||||
|
||||
func switch_to(new_state_name: String, data: Dictionary = {}) -> void:
|
||||
if new_state_name == current_state:
|
||||
return
|
||||
var prev = current_state
|
||||
current_state = new_state_name
|
||||
for key in states:
|
||||
var st = states[key]
|
||||
if key == new_state_name:
|
||||
st.enter(data)
|
||||
else:
|
||||
st.exit()
|
||||
state_changed.emit(prev, new_state_name)
|
||||
|
||||
|
||||
func register_chain_mechanic(mechanic_name: String) -> void:
|
||||
if chain_timer > 0.0 and chain_count > 0:
|
||||
chain_count = min(chain_count + 1, 10)
|
||||
else:
|
||||
chain_count = 1
|
||||
chain_timer = params.chain_window
|
||||
current_chain_bonus = min(
|
||||
float(chain_count) * params.chain_bonus_per_success,
|
||||
params.chain_bonus_cap
|
||||
)
|
||||
movement_event.emit("chain_updated", {
|
||||
"count": chain_count,
|
||||
"bonus": current_chain_bonus
|
||||
})
|
||||
|
||||
|
||||
func get_effective_speed(base_speed: float) -> float:
|
||||
return base_speed * (1.0 + current_chain_bonus)
|
||||
Reference in New Issue
Block a user