class_name MovementMotor extends Node const GRAVITY := 1200.0 const KNOCKBACK_FRICTION := 480.0 const ActionRuleResolverScript := preload("res://scripts/resolvers/action_rule_resolver.gd") @onready var actor: CharacterBody2D = get_parent() as CharacterBody2D @onready var state_machine: Node = get_node_or_null("../StateMachine") @onready var effect_container: Node = get_node_or_null("../EffectContainer") # 击退的水平残速还没衰减完;期间 set_heading 不得改写朝向(受击不转身)。 var _knockback_stray_active := false func handle_input() -> void: if actor == null or not can_move_freely(): return # 自主移动接管 velocity.x,击退残速语义随之结束。 _knockback_stray_active = false var direction := get_horizontal_axis() actor.velocity.x = direction * float(actor.get("speed")) * _move_speed_multiplier() if direction < 0.0: actor.set("heading", Vector2.LEFT) elif direction > 0.0: actor.set("heading", Vector2.RIGHT) func handle_air_time(delta: float) -> void: if actor == null: return _decay_stray_velocity(delta) if _presentation_state() != Character.PRESENTATION_JUMP and _ground_state() != &"Airborne": return var height := float(actor.get("height")) var height_speed := float(actor.get("height_speed")) height += height_speed * delta if height <= 0.0 and height_speed < 0.0: actor.set("height", 0.0) actor.set("height_speed", 0.0) actor.set("state", Character.PRESENTATION_LAND) actor.velocity.y = 0.0 _set_ground_state(&"Grounded") _reset_air_actions() _dispatch_effect_event(&"on_landed") else: actor.set("height", height) actor.set("height_speed", height_speed - GRAVITY * delta) _set_ground_state(&"Airborne") func _decay_stray_velocity(delta: float) -> void: var state := _presentation_state() if state == Character.PRESENTATION_ATTACK or state == Character.PRESENTATION_AIR_ATTACK: return actor.velocity.x = move_toward(actor.velocity.x, 0.0, KNOCKBACK_FRICTION * delta) actor.velocity.y = 0.0 if actor.velocity.x == 0.0: _knockback_stray_active = false func handle_movement() -> void: if actor == null: return var state := _presentation_state() if state == Character.PRESENTATION_JUMP or state == Character.PRESENTATION_ATTACK or state == Character.PRESENTATION_AIR_ATTACK: return if _ground_state() == &"Airborne" or float(actor.get("height")) > 0.0 or not is_zero_approx(float(actor.get("height_speed"))): _set_ground_state(&"Airborne") return if state == Character.PRESENTATION_LAND: actor.set("state", Character.PRESENTATION_IDLE) elif absf(actor.velocity.x) > 0.0: actor.set("state", Character.PRESENTATION_WALK) else: actor.set("state", Character.PRESENTATION_IDLE) _set_ground_state(&"Grounded") func set_heading() -> void: if actor == null: return # 受击不转身(2026-07-05 定案):Hitstun/击退残速期间保持受击前朝向。 if _knockback_stray_active or _life_state() != &"Alive": return if actor.velocity.x > 0.0: actor.set("heading", Vector2.RIGHT) elif actor.velocity.x < 0.0: actor.set("heading", Vector2.LEFT) func start_jump() -> bool: if actor == null or not can_jump(): return false actor.set("state", Character.PRESENTATION_JUMP) actor.set("height_speed", float(actor.get("jump_intensity"))) _set_ground_state(&"Airborne") return true func can_jump() -> bool: if actor == null: return false var state := _presentation_state() return state == Character.PRESENTATION_IDLE or state == Character.PRESENTATION_WALK func can_move_freely() -> bool: if not _free_movement_inputs_bound(): return false if _movement_blocked_by_effect(): return false var context := _movement_context() if bool(ActionRuleResolverScript.can_move_freely(context)): return true return _charging_movement_allowed_by_effect() func get_horizontal_axis() -> float: var axis := 0.0 if Input.is_action_pressed(&"move_left"): axis -= 1.0 if Input.is_action_pressed(&"move_right"): axis += 1.0 return axis func apply_knockback(knockback: Vector2) -> void: if actor == null: return actor.velocity.x = knockback.x _knockback_stray_active = knockback.x != 0.0 if knockback.y > 0.0: actor.set("height", maxf(float(actor.get("height")), 0.1)) actor.set("height_speed", knockback.y) _set_ground_state(&"Airborne") elif knockback.y < 0.0: actor.set("height_speed", knockback.y) else: actor.velocity.y = 0.0 ## 供覆写了 handle_movement 的宿主(Boss/小怪 AI)查询:击退残速未衰减完时 ## 不得清零 velocity.x,否则击退位移活不过一帧。 func has_knockback_stray() -> bool: return _knockback_stray_active ## 强制结束击退滑行语义(Boss 受击链强制撤退等"压倒击退"的路径专用)。 func clear_knockback_stray() -> void: _knockback_stray_active = false func _ground_state() -> StringName: if state_machine != null and state_machine.has_method("build_context"): return StringName(str(state_machine.call("build_context").get("ground_state", &"Grounded"))) return &"Grounded" func _movement_context() -> Dictionary: var context := { "life_state": &"Alive", "action_phase": &"Neutral", "ground_state": &"Grounded", } if state_machine != null and state_machine.has_method("build_context"): context = state_machine.call("build_context") context["effect_container"] = effect_container return context func _presentation_state() -> StringName: if actor == null: return Character.PRESENTATION_IDLE return StringName(str(actor.get("state"))) func _action_phase() -> StringName: if state_machine != null and state_machine.has_method("build_context"): return StringName(str(state_machine.call("build_context").get("action_phase", &"Neutral"))) return &"Neutral" func _life_state() -> StringName: if state_machine != null and state_machine.has_method("build_context"): return StringName(str(state_machine.call("build_context").get("life_state", &"Alive"))) return &"Alive" func _charging_movement_allowed_by_effect() -> bool: if _action_phase() != &"Charging": return false if effect_container == null or not effect_container.has_method("action_rule_modifiers"): return false for modifier: Resource in effect_container.call("action_rule_modifiers"): if bool(modifier.get("allow_movement_while_charging")): return true return false func _movement_blocked_by_effect() -> bool: if effect_container == null or not effect_container.has_method("action_rule_modifiers"): return false for modifier: Resource in effect_container.call("action_rule_modifiers"): if bool(modifier.get("block_movement")): return true return false func _move_speed_multiplier() -> float: if effect_container == null or not effect_container.has_method("stat_multiplier"): return 1.0 return float(effect_container.call("stat_multiplier", &"move_speed", null)) func _free_movement_inputs_bound() -> bool: for action_name: StringName in [&"move_left", &"move_right"]: if InputMap.has_action(action_name) and not InputMap.action_get_events(action_name).is_empty(): return true return false func _set_ground_state(next_state: StringName) -> void: if state_machine != null and state_machine.has_method("set_ground_state"): state_machine.call("set_ground_state", next_state) func _reset_air_actions() -> void: if state_machine != null and "air_action_count" in state_machine: state_machine.set("air_action_count", 0) func _dispatch_effect_event(event_name: StringName) -> void: if effect_container == null: effect_container = get_node_or_null("../EffectContainer") if effect_container != null and effect_container.has_method("dispatch_event"): effect_container.call("dispatch_event", event_name, {"actor": actor})