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