160 lines
3.9 KiB
GDScript
160 lines
3.9 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
|
||
|
||
@onready var line_2d: Line2D = %Line2D
|
||
@onready var ray: RayCast2D = %RayCast2D
|
||
|
||
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
|
||
|
||
# =================
|
||
# 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
|
||
|
||
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 _update_line() -> void:
|
||
line_2d.set_point_position(0, Vector2.ZERO)
|
||
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
|