Files
rthythm_archor_game/scenes/components/motion_executor.gd
T
2026-07-06 22:59:47 -07:00

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