godot-plateformer/addons/reedcomponent/grap_hook/garpping_hook_v_2.gd

259 lines
6.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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