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
|
|
|
|
|
2026-01-11 00:57:27 +08:00
|
|
|
|
@export_category("Hook Retract")
|
|
|
|
|
|
@export var retract_speed: float = 1800.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
|
|
|
|
|
2026-01-11 00:57:27 +08:00
|
|
|
|
var _tween: Tween
|
|
|
|
|
|
|
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)
|
2026-01-14 23:34:00 +08:00
|
|
|
|
## 钩爪击中物体信号,target 是被击中的物体,hit_pos 是击中点世界坐标
|
|
|
|
|
|
signal hook_hit(target: Node2D, hit_pos: Vector2)
|
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
|
|
|
|
|
|
|
2026-01-06 23:18:36 +08:00
|
|
|
|
var _dir_id: int = -1
|
|
|
|
|
|
|
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
|
2026-01-06 23:18:36 +08:00
|
|
|
|
|
|
|
|
|
|
_dir_id = _get_direction_id(direction,8)
|
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
|
|
|
|
|
2026-01-14 23:34:00 +08:00
|
|
|
|
# 如果 target 有 on_hook_hit 方法,调用它
|
|
|
|
|
|
if target.has_method(&"on_hook_hit"):
|
|
|
|
|
|
target.on_hook_hit(hit_pos)
|
|
|
|
|
|
|
|
|
|
|
|
if target.owner.has_method(&"on_hook_hit"):
|
|
|
|
|
|
target.owner.on_hook_hit(hit_pos)
|
|
|
|
|
|
|
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 與狀態)
|
2026-01-11 00:57:27 +08:00
|
|
|
|
func _release_hook() -> void:
|
2025-12-31 16:24:11 +08:00
|
|
|
|
# 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
|
|
|
|
|
2026-01-11 00:57:27 +08:00
|
|
|
|
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()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-31 16:24:11 +08:00
|
|
|
|
func _update_line() -> void:
|
2026-01-11 00:57:27 +08:00
|
|
|
|
if _anchor and is_instance_valid(_anchor):
|
|
|
|
|
|
# 关键:锚点是世界坐标固定的,把它换算到 Hook 的本地坐标
|
|
|
|
|
|
ray.target_position = to_local(_anchor.global_position)
|
|
|
|
|
|
|
2025-12-31 16:24:11 +08:00
|
|
|
|
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
|
2026-01-06 23:18:36 +08:00
|
|
|
|
|
|
|
|
|
|
## 计算方向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
|