class_name Hook extends Node2D ## ================ ## Export Field ## ================ @export var min_length := 140.0 @export var max_length := 200.0 @export var stretching_speed: float = 1400.0 @export_category("Hook Retract") @export var retract_speed: float = 1800.0 @onready var line_2d: Line2D = %Line2D @onready var ray: RayCast2D = %RayCast2D var _tween: Tween const GRAPABLE_GROUP = &"GRAPABLE" signal stretching_finished(reach_limit: bool, anchor_node: Node2D) ## ================ ## Private Field ## ================ var _binded_hook_comp var _is_stretching := false var _stretching_dir := Vector2.ZERO var _cached_cancel := false var _current_length := 0.0 var _anchor: Node2D var _dir_id: int = -1 # ================= # Life Cycle # ================= func _ready() -> void: ray.enabled = true ray.target_position = Vector2.ZERO ## 初始化 func init(hook_comp: SpawnHookComponet, reset_to_target: bool) -> void: _binded_hook_comp = hook_comp if reset_to_target: global_position = hook_comp.owner.global_position # ================= # Stretch Control # ================= func start_stretching(direction: Vector2) -> void: _is_stretching = true _cached_cancel = false _stretching_dir = direction.normalized() _current_length = 0.0 _dir_id = _get_direction_id(direction,8) func end_stretching(force_end: bool = false) -> bool: if not force_end and _is_stretching: if _current_length < min_length: _cached_cancel = true return false _is_stretching = false _stretching_dir = Vector2.ZERO return true func is_stretching() -> bool: return _is_stretching # ================= # Update # ================= func _physics_process(delta: float) -> void: if _is_stretching: _update_stretching(delta) func _process(_delta: float) -> void: _update_line() # ================= # Core Logic # ================= func _update_stretching(delta: float) -> void: # 先嘗試推進 var next_length := _current_length + stretching_speed * delta next_length = min(next_length, max_length) # 先用「下一幀長度」做 Ray ray.target_position = _stretching_dir * next_length ray.force_raycast_update() # ===== 命中檢測(最高優先)===== if ray.is_colliding(): var collider := ray.get_collider() if collider is Node2D and collider.is_in_group(GRAPABLE_GROUP): var hit_pos := ray.get_collision_point() _current_length = global_position.distance_to(hit_pos) ray.target_position = _stretching_dir * _current_length _handle_hit(collider as Node2D, hit_pos) return # ===== 沒命中,才正式推進 ===== _current_length = next_length # 取消邏輯 if _cached_cancel and _current_length >= min_length: stretching_finished.emit(true, null) end_stretching(true) return # 超過最大距離 if _current_length >= max_length: stretching_finished.emit(true, null) end_stretching(true) func _handle_hit(target: Node2D, hit_pos: Vector2) -> void: _is_stretching = false _stretching_dir = Vector2.ZERO ray.target_position = to_local(hit_pos) var reach_max := is_equal_approx(_current_length, max_length) var anchor := _create_anchor_on_node(target, hit_pos) stretching_finished.emit(reach_max, anchor) ## 釋放鉤爪(清理 Anchor 與狀態) func _release_hook() -> void: # 1. 停止拉伸(保險) _is_stretching = false _stretching_dir = Vector2.ZERO _cached_cancel = false _current_length = 0.0 # 2. 清掉 Anchor if _anchor and is_instance_valid(_anchor): # 先脫離父節點,避免殘留引用問題 _anchor.get_parent().remove_child(_anchor) _anchor.queue_free() _anchor = null # 3. 重置 Ray 與 Line(視覺清乾淨) ray.target_position = Vector2.ZERO _update_line() func release_hook_with_transition(has_trans: bool) -> void: # 先停止一切拉伸逻辑 _is_stretching = false _stretching_dir = Vector2.ZERO _cached_cancel = false # 如果不需要动画,直接释放 if not has_trans: _release_hook() queue_free() return # ===== 需要回收动画 ===== # 先清 Anchor(但不影响末端视觉) if _anchor and is_instance_valid(_anchor): _anchor.get_parent().remove_child(_anchor) _anchor.queue_free() _anchor = null # 当前末端位置(本地坐标) var start_pos: Vector2 = ray.target_position var distance := start_pos.length() if distance <= 0.001: queue_free() return # 根据速度算 tween 时间 var duration := distance / retract_speed if _tween and _tween.is_valid(): _tween.kill() _tween = get_tree().create_tween() _tween.set_trans(Tween.TRANS_SINE) _tween.set_ease(Tween.EASE_IN) _tween.tween_property( ray, "target_position", Vector2.ZERO, duration ) _tween.finished.connect(func(): queue_free() ) func _update_line() -> void: if _anchor and is_instance_valid(_anchor): # 关键:锚点是世界坐标固定的,把它换算到 Hook 的本地坐标 ray.target_position = to_local(_anchor.global_position) line_2d.set_point_position(1, ray.target_position) func _create_anchor_on_node(target: Node2D, hit_global_pos: Vector2) -> Node2D: if _anchor and is_instance_valid(_anchor): _anchor.queue_free() _anchor = Node2D.new() _anchor.name = "Anchor" target.add_child(_anchor) # 關鍵:固定命中點偏移 _anchor.position = target.to_local(hit_global_pos) return _anchor ## 计算方向id,私有 func _get_direction_id(direction: Vector2, sector_count: int) -> int: if direction == Vector2.ZERO: return -1 var angle = rad_to_deg(direction.angle()) angle += 90.0 var sector_size = 360.0 / sector_count angle += sector_size / 2.0 angle = fposmod(angle, 360.0) return int(angle / sector_size) ## 获取方向id func get_direction_id() -> int: return _dir_id