2025-12-31 16:24:11 +08:00
|
|
|
|
class_name Hook
|
|
|
|
|
|
extends Node2D
|
2025-12-31 13:07:31 +08:00
|
|
|
|
|
|
|
|
|
|
## ================
|
|
|
|
|
|
## Export Field
|
|
|
|
|
|
## ================
|
2025-12-31 16:24:11 +08:00
|
|
|
|
@export var min_length := 140.0
|
|
|
|
|
|
@export var max_length := 200.0
|
|
|
|
|
|
@export var stretching_speed: float = 1400.0
|
2025-12-31 13:07:31 +08:00
|
|
|
|
|
|
|
|
|
|
@onready var line_2d: Line2D = %Line2D
|
2025-12-31 16:24:11 +08:00
|
|
|
|
@onready var ray: RayCast2D = %RayCast2D
|
2025-12-31 13:07:31 +08:00
|
|
|
|
|
|
|
|
|
|
const GRAPABLE_GROUP = &"GRAPABLE"
|
|
|
|
|
|
|
2025-12-31 16:24:11 +08:00
|
|
|
|
signal stretching_finished(reach_limit: bool, anchor_node: Node2D)
|
2025-12-31 13:07:31 +08:00
|
|
|
|
|
|
|
|
|
|
## ================
|
|
|
|
|
|
## Private Field
|
|
|
|
|
|
## ================
|
|
|
|
|
|
var _binded_hook_comp
|
2025-12-31 16:24:11 +08:00
|
|
|
|
var _is_stretching := false
|
|
|
|
|
|
var _stretching_dir := Vector2.ZERO
|
|
|
|
|
|
var _cached_cancel := false
|
|
|
|
|
|
var _current_length := 0.0
|
2025-12-31 13:07:31 +08:00
|
|
|
|
var _anchor: Node2D
|
|
|
|
|
|
|
2025-12-31 16:24:11 +08:00
|
|
|
|
# =================
|
|
|
|
|
|
# Life Cycle
|
|
|
|
|
|
# =================
|
|
|
|
|
|
|
2025-12-31 13:07:31 +08:00
|
|
|
|
func _ready() -> void:
|
2025-12-31 16:24:11 +08:00
|
|
|
|
ray.enabled = true
|
|
|
|
|
|
ray.target_position = Vector2.ZERO
|
|
|
|
|
|
|
|
|
|
|
|
## 初始化
|
|
|
|
|
|
func init(hook_comp: SpawnHookComponet, reset_to_target: bool) -> void:
|
2025-12-31 13:07:31 +08:00
|
|
|
|
_binded_hook_comp = hook_comp
|
|
|
|
|
|
if reset_to_target:
|
2025-12-31 16:24:11 +08:00
|
|
|
|
global_position = hook_comp.owner.global_position
|
|
|
|
|
|
|
|
|
|
|
|
# =================
|
|
|
|
|
|
# Stretch Control
|
|
|
|
|
|
# =================
|
2025-12-31 13:07:31 +08:00
|
|
|
|
|
|
|
|
|
|
func start_stretching(direction: Vector2) -> void:
|
|
|
|
|
|
_is_stretching = true
|
|
|
|
|
|
_cached_cancel = false
|
2025-12-31 16:24:11 +08:00
|
|
|
|
_stretching_dir = direction.normalized()
|
|
|
|
|
|
_current_length = 0.0
|
2025-12-31 13:07:31 +08:00
|
|
|
|
|
|
|
|
|
|
func end_stretching(force_end: bool = false) -> bool:
|
|
|
|
|
|
if not force_end and _is_stretching:
|
2025-12-31 16:24:11 +08:00
|
|
|
|
if _current_length < min_length:
|
2025-12-31 13:07:31 +08:00
|
|
|
|
_cached_cancel = true
|
|
|
|
|
|
return false
|
2025-12-31 16:24:11 +08:00
|
|
|
|
|
2025-12-31 13:07:31 +08:00
|
|
|
|
_is_stretching = false
|
|
|
|
|
|
_stretching_dir = Vector2.ZERO
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
func is_stretching() -> bool:
|
|
|
|
|
|
return _is_stretching
|
|
|
|
|
|
|
2025-12-31 16:24:11 +08:00
|
|
|
|
# =================
|
|
|
|
|
|
# Update
|
|
|
|
|
|
# =================
|
|
|
|
|
|
|
2025-12-31 13:07:31 +08:00
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
|
|
|
|
if _is_stretching:
|
|
|
|
|
|
_update_stretching(delta)
|
|
|
|
|
|
|
2025-12-31 16:24:11 +08:00
|
|
|
|
func _process(_delta: float) -> void:
|
|
|
|
|
|
_update_line()
|
|
|
|
|
|
|
|
|
|
|
|
# =================
|
|
|
|
|
|
# Core Logic
|
|
|
|
|
|
# =================
|
2025-12-31 13:07:31 +08:00
|
|
|
|
|
|
|
|
|
|
func _update_stretching(delta: float) -> void:
|
2025-12-31 16:24:11 +08:00
|
|
|
|
# 先嘗試推進
|
|
|
|
|
|
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)
|
2025-12-31 13:07:31 +08:00
|
|
|
|
return
|
2025-12-31 16:24:11 +08:00
|
|
|
|
|
|
|
|
|
|
# ===== 沒命中,才正式推進 =====
|
|
|
|
|
|
_current_length = next_length
|
|
|
|
|
|
|
|
|
|
|
|
# 取消邏輯
|
|
|
|
|
|
if _cached_cancel and _current_length >= min_length:
|
|
|
|
|
|
stretching_finished.emit(true, null)
|
2025-12-31 13:07:31 +08:00
|
|
|
|
end_stretching(true)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2025-12-31 16:24:11 +08:00
|
|
|
|
# 超過最大距離
|
|
|
|
|
|
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
|
2025-12-31 13:07:31 +08:00
|
|
|
|
|
2025-12-31 16:24:11 +08:00
|
|
|
|
ray.target_position = to_local(hit_pos)
|
2025-12-31 13:07:31 +08:00
|
|
|
|
|
2025-12-31 16:24:11 +08:00
|
|
|
|
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
|
2025-12-31 13:07:31 +08:00
|
|
|
|
if _anchor and is_instance_valid(_anchor):
|
2025-12-31 16:24:11 +08:00
|
|
|
|
# 先脫離父節點,避免殘留引用問題
|
|
|
|
|
|
_anchor.get_parent().remove_child(_anchor)
|
2025-12-31 13:07:31 +08:00
|
|
|
|
_anchor.queue_free()
|
2025-12-31 16:24:11 +08:00
|
|
|
|
_anchor = null
|
2025-12-31 13:07:31 +08:00
|
|
|
|
|
2025-12-31 16:24:11 +08:00
|
|
|
|
# 3. 重置 Ray 與 Line(視覺清乾淨)
|
|
|
|
|
|
ray.target_position = Vector2.ZERO
|
|
|
|
|
|
_update_line()
|
2025-12-31 13:07:31 +08:00
|
|
|
|
|
2025-12-31 16:24:11 +08:00
|
|
|
|
func _update_line() -> void:
|
|
|
|
|
|
line_2d.set_point_position(0, Vector2.ZERO)
|
|
|
|
|
|
line_2d.set_point_position(1, ray.target_position)
|
2025-12-31 13:07:31 +08:00
|
|
|
|
|
2025-12-31 16:24:11 +08:00
|
|
|
|
func _create_anchor_on_node(target: Node2D, hit_global_pos: Vector2) -> Node2D:
|
|
|
|
|
|
if _anchor and is_instance_valid(_anchor):
|
|
|
|
|
|
_anchor.queue_free()
|
2025-12-31 13:07:31 +08:00
|
|
|
|
|
2025-12-31 16:24:11 +08:00
|
|
|
|
_anchor = Node2D.new()
|
|
|
|
|
|
_anchor.name = "Anchor"
|
|
|
|
|
|
target.add_child(_anchor)
|
2025-12-31 13:07:31 +08:00
|
|
|
|
|
2025-12-31 16:24:11 +08:00
|
|
|
|
# 關鍵:固定命中點偏移
|
|
|
|
|
|
_anchor.position = target.to_local(hit_global_pos)
|
2025-12-31 13:07:31 +08:00
|
|
|
|
|
|
|
|
|
|
return _anchor
|