238 lines
7.5 KiB
GDScript
238 lines
7.5 KiB
GDScript
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})
|