Initial project sync
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
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})
|
||||
Reference in New Issue
Block a user