Files
2026-07-06 22:59:47 -07:00

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})