157 lines
5.2 KiB
GDScript
157 lines
5.2 KiB
GDScript
class_name MotionExecutor
|
|
extends Node
|
|
|
|
signal motion_started(action: Resource)
|
|
signal motion_finished(action: Resource)
|
|
|
|
# player_body (layer 6) | enemy_body (layer 7): the pair that body-blocks.
|
|
const BODY_GHOST_BITS := (1 << 5) | (1 << 6)
|
|
# 幽灵态收尾嵌入时的每帧排斥步长(240px/s,快于小怪追击 120px/s,保证能拉开)。
|
|
const OVERLAP_NUDGE_PER_FRAME := 4.0
|
|
|
|
@onready var actor: CharacterBody2D = get_parent() as CharacterBody2D
|
|
|
|
var current_action: Resource
|
|
var velocity := Vector2.ZERO
|
|
var duration := 0.0
|
|
var elapsed := 0.0
|
|
var active := false
|
|
var _saved_collision_mask := -1
|
|
var _saved_collision_layer := -1
|
|
var _restore_pending := false
|
|
var _ghost_exit_sign := 1.0
|
|
|
|
|
|
func _physics_process(_delta: float) -> void:
|
|
if _restore_pending:
|
|
_try_restore_collision()
|
|
|
|
|
|
func execute(action: Resource, direction: Vector2, beat_time: float, speed := 220.0) -> void:
|
|
if active:
|
|
cancel()
|
|
current_action = action
|
|
duration = maxf(0.01, float(action.get("action_beats")) * maxf(0.01, beat_time))
|
|
elapsed = 0.0
|
|
active = true
|
|
# Vertical motion is owned by the fake-height system (height/height_speed);
|
|
# feeding move_mult_y into the body velocity leaves the CharacterBody drifting off the ground plane.
|
|
var move_x := float(action.get("move_mult_x"))
|
|
var horizontal := direction.x if direction.x != 0.0 else signf(move_x)
|
|
velocity = Vector2(signf(horizontal) * speed, 0.0) if horizontal != 0.0 else Vector2.ZERO
|
|
if _has_action_tag(action, &"dash_through"):
|
|
_begin_dash_through()
|
|
motion_started.emit(action)
|
|
|
|
|
|
func tick(delta: float) -> Vector2:
|
|
if not active:
|
|
return Vector2.ZERO
|
|
elapsed += delta
|
|
if elapsed >= duration:
|
|
active = false
|
|
velocity = Vector2.ZERO
|
|
_request_restore_collision()
|
|
motion_finished.emit(current_action)
|
|
return velocity
|
|
|
|
|
|
func cancel() -> void:
|
|
active = false
|
|
velocity = Vector2.ZERO
|
|
_request_restore_collision()
|
|
current_action = null
|
|
|
|
|
|
func is_ghosting() -> bool:
|
|
return _saved_collision_mask != -1
|
|
|
|
|
|
func _has_action_tag(action: Resource, tag: StringName) -> bool:
|
|
if action == null:
|
|
return false
|
|
var tags: Array = action.get("action_tags")
|
|
for item: Variant in tags:
|
|
if StringName(str(item)) == tag:
|
|
return true
|
|
return false
|
|
|
|
|
|
func _begin_dash_through() -> void:
|
|
if actor == null:
|
|
actor = get_parent() as CharacterBody2D
|
|
if actor == null:
|
|
return
|
|
# A new dash always cancels any deferred restore from the previous one, so
|
|
# the ghost state cannot snap back mid-dash.
|
|
_restore_pending = false
|
|
# 记录冲刺方向:收尾嵌入时沿该方向推出(保留"从远侧穿出"的手感)。
|
|
if velocity.x != 0.0:
|
|
_ghost_exit_sign = signf(velocity.x)
|
|
if _saved_collision_mask != -1:
|
|
return
|
|
# Ghost both directions: stop colliding with enemy/player bodies AND stop
|
|
# being collidable by them, so neither side's move_and_slide recovery can
|
|
# shove anyone while the dash overlaps a body.
|
|
_saved_collision_mask = actor.collision_mask
|
|
_saved_collision_layer = actor.collision_layer
|
|
actor.collision_mask = actor.collision_mask & ~BODY_GHOST_BITS
|
|
actor.collision_layer = actor.collision_layer & ~BODY_GHOST_BITS
|
|
|
|
|
|
func _request_restore_collision() -> void:
|
|
if _saved_collision_mask == -1:
|
|
return
|
|
# Never snap collision back while still inside another body: restoring
|
|
# mid-overlap lets depenetration shove the actor back out the entry side,
|
|
# which reads as "the dash did not pierce". Retry every physics frame.
|
|
_restore_pending = true
|
|
_try_restore_collision()
|
|
|
|
|
|
func _try_restore_collision() -> void:
|
|
if actor == null or _saved_collision_mask == -1:
|
|
_restore_pending = false
|
|
return
|
|
if Engine.is_in_physics_frame() and _overlaps_ghosted_bodies():
|
|
# 2026-07-06 策划:除冲刺穿人过程外不得与敌人重叠。嵌入时不再无限期
|
|
# 干等分离,而是沿冲刺方向温和推出,推清后下一帧恢复碰撞。
|
|
_nudge_out_of_overlap()
|
|
return
|
|
actor.collision_mask = _saved_collision_mask
|
|
actor.collision_layer = _saved_collision_layer
|
|
_saved_collision_mask = -1
|
|
_saved_collision_layer = -1
|
|
_restore_pending = false
|
|
|
|
|
|
## 幽灵态结束但仍嵌在敌人体内:每物理帧沿冲刺方向推 4px(幽灵掩码只剩
|
|
## world 位,绝不会被推进地形);顶到边界墙推不动时反向从入口侧退出。
|
|
func _nudge_out_of_overlap() -> void:
|
|
var motion := Vector2(_ghost_exit_sign * OVERLAP_NUDGE_PER_FRAME, 0.0)
|
|
var collision := actor.move_and_collide(motion)
|
|
if collision != null and collision.get_travel().length() < OVERLAP_NUDGE_PER_FRAME * 0.5:
|
|
_ghost_exit_sign = -_ghost_exit_sign
|
|
|
|
|
|
func _overlaps_ghosted_bodies() -> bool:
|
|
if actor == null or not actor.is_inside_tree():
|
|
return false
|
|
var query_mask := _saved_collision_mask & BODY_GHOST_BITS
|
|
if query_mask == 0:
|
|
return false
|
|
var shape_node := actor.get_node_or_null("CollisionShape2D") as CollisionShape2D
|
|
if shape_node == null or shape_node.shape == null:
|
|
return false
|
|
var params := PhysicsShapeQueryParameters2D.new()
|
|
params.shape = shape_node.shape
|
|
params.transform = shape_node.global_transform
|
|
params.collision_mask = query_mask
|
|
params.collide_with_bodies = true
|
|
params.collide_with_areas = false
|
|
params.exclude = [actor.get_rid()]
|
|
var space := actor.get_world_2d().direct_space_state
|
|
if space == null:
|
|
return false
|
|
return not space.intersect_shape(params, 1).is_empty()
|