259 lines
6.4 KiB
GDScript
259 lines
6.4 KiB
GDScript
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)
|
||
## 钩爪击中物体信号,target 是被击中的物体,hit_pos 是击中点世界坐标,hook 是钩爪实例
|
||
signal hook_hit(target: Node2D, hit_pos: Vector2, hook: Hook)
|
||
|
||
## ================
|
||
## 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
|
||
|
||
## 获取当前飞行方向(可被外部复写)
|
||
func get_stretching_dir() -> Vector2:
|
||
return _stretching_dir.normalized()
|
||
|
||
## 设置飞行方向(供外部物体修改)
|
||
func set_stretching_dir(dir: Vector2) -> void:
|
||
_stretching_dir = dir.normalized()
|
||
_dir_id = _get_direction_id(_stretching_dir, 8)
|
||
|
||
# =================
|
||
# 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)
|
||
|
||
# 使用 getter 获取当前方向(允许外部复写)
|
||
var current_dir := get_stretching_dir()
|
||
|
||
# 先用「下一幀長度」做 Ray
|
||
ray.target_position = current_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 = current_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)
|
||
|
||
# 如果 target 有 on_hook_hit 方法,调用它(传入钩爪实例)
|
||
if target.has_method(&"on_hook_hit"):
|
||
target.on_hook_hit(hit_pos, self)
|
||
|
||
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)
|
||
hook_hit.emit(target, hit_pos, self)
|
||
|
||
## 釋放鉤爪(清理 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
|