2026-07-25 12:06:19 -04:00
|
|
|
class_name FishingPresentation
|
|
|
|
|
extends Node3D
|
|
|
|
|
|
2026-08-08 00:34:06 -04:00
|
|
|
const WaterSurfaceMotionType = preload(
|
|
|
|
|
"res://world/water_surface_motion.gd"
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-25 12:06:19 -04:00
|
|
|
signal cast_completed
|
2026-07-25 15:15:24 -04:00
|
|
|
signal outcome_completed(outcome: StringName)
|
2026-08-01 15:39:25 -04:00
|
|
|
signal presentation_interrupted
|
2026-07-25 12:06:19 -04:00
|
|
|
|
|
|
|
|
enum VisualMode {
|
|
|
|
|
NONE,
|
|
|
|
|
AIMING,
|
|
|
|
|
CASTING,
|
|
|
|
|
FISHING,
|
|
|
|
|
OUTCOME,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 13:04:23 -04:00
|
|
|
enum LineMode {
|
|
|
|
|
HIDDEN,
|
|
|
|
|
TAUT,
|
|
|
|
|
SLACK,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@export_category("Fishing Line")
|
|
|
|
|
@export_range(0.0, 0.5, 0.01) var slack_amount: float = 0.08
|
|
|
|
|
@export_range(0.0, 2.0, 0.01) var minimum_slack: float = 0.08
|
|
|
|
|
@export_range(0.0, 3.0, 0.01) var maximum_slack: float = 0.65
|
2026-08-01 11:16:21 -04:00
|
|
|
@export_range(2, 20, 1) var sag_interpolation_count: int = 8
|
2026-07-25 13:04:23 -04:00
|
|
|
@export_range(0.1, 30.0, 0.1) var line_transition_speed: float = 7.0
|
|
|
|
|
|
2026-07-25 12:06:19 -04:00
|
|
|
@export_category("Cast")
|
|
|
|
|
@export_range(0.1, 3.0, 0.05) var cast_travel_duration: float = 0.65
|
|
|
|
|
@export_range(0.1, 5.0, 0.1) var cast_arc_height: float = 2.0
|
|
|
|
|
@export_range(0.0, 90.0, 1.0) var maximum_rod_cock_degrees: float = 50.0
|
|
|
|
|
@export_range(0.0, 60.0, 1.0) var rod_forward_swing_degrees: float = 22.0
|
|
|
|
|
@export_range(0.05, 1.0, 0.01) var rod_forward_swing_duration: float = 0.12
|
|
|
|
|
@export_range(0.05, 1.0, 0.01) var rod_recovery_duration: float = 0.22
|
|
|
|
|
|
2026-08-01 15:39:25 -04:00
|
|
|
@export_category("Bobber Idle")
|
|
|
|
|
@export_range(0.0, 0.2, 0.005) var bobber_bob_amplitude: float = 0.035
|
|
|
|
|
@export_range(0.1, 3.0, 0.05) var bobber_bob_cycles_per_second: float = 0.7
|
|
|
|
|
@export_range(0.0, 12.0, 0.5) var bobber_tilt_degrees: float = 4.0
|
|
|
|
|
@onready var _target_marker: Node3D = %CastTargetMarker
|
|
|
|
|
@onready var _valid_target_marker: MeshInstance3D = %ValidTargetMarker
|
|
|
|
|
@onready var _invalid_target_marker: Node3D = %InvalidTargetMarker
|
2026-07-25 12:06:19 -04:00
|
|
|
@onready var _bobber: MeshInstance3D = %Bobber
|
|
|
|
|
@onready var _line: MeshInstance3D = %FishingLine
|
|
|
|
|
|
|
|
|
|
var _mode: VisualMode = VisualMode.NONE
|
2026-07-25 13:04:23 -04:00
|
|
|
var _line_mode: LineMode = LineMode.HIDDEN
|
2026-07-25 12:06:19 -04:00
|
|
|
var _line_mesh: ImmediateMesh = ImmediateMesh.new()
|
2026-07-25 13:04:23 -04:00
|
|
|
var _current_sag: float = 0.0
|
2026-07-25 12:06:19 -04:00
|
|
|
var _rod: Node3D
|
|
|
|
|
var _rod_tip: Marker3D
|
|
|
|
|
var _rod_neutral_rotation: Vector3
|
2026-08-01 15:39:25 -04:00
|
|
|
var _cast_arrival_position: Vector3
|
2026-08-08 00:34:06 -04:00
|
|
|
var _cast_tracks_water_surface: bool = false
|
2026-08-01 15:39:25 -04:00
|
|
|
var _bobber_surface_position: Vector3
|
|
|
|
|
var _bobber_idle_elapsed: float = 0.0
|
2026-08-08 10:25:17 -04:00
|
|
|
var _bobber_base_scale: Vector3 = Vector3.ONE
|
2026-07-25 12:06:19 -04:00
|
|
|
var _active_tween: Tween
|
|
|
|
|
var _rod_tween: Tween
|
2026-07-25 17:04:39 -04:00
|
|
|
var _bite_tween: Tween
|
2026-07-25 12:06:19 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
|
_line.mesh = _line_mesh
|
|
|
|
|
cleanup()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _process(delta: float) -> void:
|
2026-08-01 15:39:25 -04:00
|
|
|
if _mode == VisualMode.FISHING and _bobber.visible:
|
|
|
|
|
_bobber_idle_elapsed += delta
|
|
|
|
|
_apply_bobber_idle_motion()
|
2026-08-08 00:34:06 -04:00
|
|
|
var bobber_position := _bobber.global_position
|
|
|
|
|
bobber_position.y += WaterSurfaceMotionType.get_default_height_offset()
|
|
|
|
|
_bobber.global_position = bobber_position
|
2026-07-25 13:04:23 -04:00
|
|
|
if _line_mode != LineMode.HIDDEN:
|
|
|
|
|
_update_line(delta)
|
2026-07-25 12:06:19 -04:00
|
|
|
|
|
|
|
|
|
2026-07-25 13:04:23 -04:00
|
|
|
func set_line_mode(mode: LineMode) -> void:
|
|
|
|
|
_line_mode = mode
|
|
|
|
|
_line.visible = mode != LineMode.HIDDEN
|
|
|
|
|
if mode == LineMode.HIDDEN:
|
|
|
|
|
_current_sag = 0.0
|
|
|
|
|
_line_mesh.clear_surfaces()
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 12:06:19 -04:00
|
|
|
func begin_aim(
|
|
|
|
|
rod_tip: Marker3D,
|
|
|
|
|
rod: Node3D,
|
|
|
|
|
minimum_target: Vector3,
|
2026-08-01 15:39:25 -04:00
|
|
|
target_normal: Vector3,
|
2026-07-25 13:04:23 -04:00
|
|
|
target_is_fishable: bool,
|
2026-08-08 10:25:17 -04:00
|
|
|
character_scale: float = 1.0,
|
2026-07-25 12:06:19 -04:00
|
|
|
) -> void:
|
|
|
|
|
cleanup()
|
|
|
|
|
if rod_tip == null or rod == null:
|
|
|
|
|
return
|
|
|
|
|
|
2026-08-08 10:25:17 -04:00
|
|
|
_bobber_base_scale = Vector3.ONE * maxf(character_scale, 0.01)
|
2026-07-25 12:06:19 -04:00
|
|
|
_mode = VisualMode.AIMING
|
|
|
|
|
_rod = rod
|
|
|
|
|
_rod_tip = rod_tip
|
|
|
|
|
_rod_neutral_rotation = _rod.rotation
|
2026-08-01 15:39:25 -04:00
|
|
|
_set_target_surface(minimum_target, target_normal)
|
2026-07-25 12:06:19 -04:00
|
|
|
_target_marker.scale = Vector3.ONE
|
|
|
|
|
_target_marker.visible = true
|
2026-07-25 13:04:23 -04:00
|
|
|
_set_target_validity(target_is_fishable)
|
2026-07-25 12:06:19 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func update_rod_charge(charge: float) -> void:
|
|
|
|
|
if _mode != VisualMode.AIMING or _rod == null:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
var cock_rotation: Vector3 = _rod_neutral_rotation
|
|
|
|
|
cock_rotation.x += deg_to_rad(maximum_rod_cock_degrees) * clampf(charge, 0.0, 1.0)
|
|
|
|
|
_rod.rotation = cock_rotation
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 13:04:23 -04:00
|
|
|
func update_aim_target(
|
|
|
|
|
target: Vector3,
|
2026-08-01 15:39:25 -04:00
|
|
|
target_normal: Vector3,
|
2026-07-25 13:04:23 -04:00
|
|
|
charge: float,
|
|
|
|
|
target_is_fishable: bool,
|
|
|
|
|
) -> void:
|
2026-07-25 12:06:19 -04:00
|
|
|
if _mode != VisualMode.AIMING:
|
|
|
|
|
return
|
|
|
|
|
|
2026-08-01 15:39:25 -04:00
|
|
|
# The gameplay resolver already advances the horizontal target smoothly.
|
|
|
|
|
# Snap each sample to its resolved surface so interpolation can never draw
|
|
|
|
|
# the marker through a vertical shoreline between two valid endpoints.
|
|
|
|
|
_set_target_surface(target, target_normal)
|
2026-07-25 12:06:19 -04:00
|
|
|
var maximum_response: float = smoothstep(0.9, 1.0, clampf(charge, 0.0, 1.0))
|
|
|
|
|
_target_marker.scale = Vector3.ONE * lerpf(1.0, 1.15, maximum_response)
|
2026-07-25 13:04:23 -04:00
|
|
|
_set_target_validity(target_is_fishable)
|
2026-07-25 12:06:19 -04:00
|
|
|
|
|
|
|
|
|
2026-08-01 15:39:25 -04:00
|
|
|
func _set_target_surface(target: Vector3, surface_normal: Vector3) -> void:
|
|
|
|
|
_target_marker.global_position = target
|
|
|
|
|
var resolved_normal: Vector3 = surface_normal.normalized()
|
|
|
|
|
if resolved_normal.is_zero_approx():
|
|
|
|
|
resolved_normal = Vector3.UP
|
|
|
|
|
_target_marker.global_basis = Basis(
|
|
|
|
|
Quaternion(Vector3.UP, resolved_normal)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func begin_cast(
|
|
|
|
|
target: Vector3,
|
|
|
|
|
blocked_landing_position: Vector3 = Vector3.ZERO,
|
|
|
|
|
cast_is_blocked: bool = false,
|
|
|
|
|
) -> void:
|
2026-07-25 12:06:19 -04:00
|
|
|
if _mode != VisualMode.AIMING or _rod_tip == null:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_kill_active_tween()
|
|
|
|
|
_mode = VisualMode.CASTING
|
|
|
|
|
_target_marker.visible = false
|
|
|
|
|
_bobber.visible = true
|
2026-08-08 10:25:17 -04:00
|
|
|
_bobber.scale = _bobber_base_scale
|
2026-08-01 15:39:25 -04:00
|
|
|
_cast_arrival_position = (
|
|
|
|
|
blocked_landing_position if cast_is_blocked else target
|
|
|
|
|
)
|
2026-08-08 00:34:06 -04:00
|
|
|
_cast_tracks_water_surface = not cast_is_blocked
|
2026-07-25 12:06:19 -04:00
|
|
|
|
|
|
|
|
var cast_start: Vector3 = _rod_tip.global_position
|
|
|
|
|
_bobber.global_position = cast_start
|
|
|
|
|
_begin_rod_release()
|
|
|
|
|
_active_tween = create_tween()
|
|
|
|
|
_active_tween.set_trans(Tween.TRANS_SINE)
|
|
|
|
|
_active_tween.set_ease(Tween.EASE_IN_OUT)
|
|
|
|
|
_active_tween.tween_method(
|
2026-08-01 15:39:25 -04:00
|
|
|
_set_cast_sample.bind(cast_start, _cast_arrival_position),
|
2026-07-25 12:06:19 -04:00
|
|
|
0.0,
|
|
|
|
|
1.0,
|
|
|
|
|
cast_travel_duration
|
|
|
|
|
)
|
2026-08-01 15:39:25 -04:00
|
|
|
if cast_is_blocked:
|
|
|
|
|
# Let the bobber visibly settle on the blocking surface before the
|
|
|
|
|
# normal invalid-cast return animation begins.
|
|
|
|
|
_active_tween.tween_interval(0.16)
|
2026-07-25 12:06:19 -04:00
|
|
|
_active_tween.finished.connect(_on_cast_tween_finished)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func show_bite() -> void:
|
|
|
|
|
if _mode != VisualMode.FISHING:
|
|
|
|
|
return
|
|
|
|
|
|
2026-07-25 17:04:39 -04:00
|
|
|
_kill_bite_tween()
|
2026-08-08 10:25:17 -04:00
|
|
|
_bobber.scale = _bobber_base_scale
|
2026-07-25 17:04:39 -04:00
|
|
|
_bite_tween = create_tween()
|
|
|
|
|
_bite_tween.set_trans(Tween.TRANS_QUAD)
|
|
|
|
|
_bite_tween.set_ease(Tween.EASE_IN_OUT)
|
|
|
|
|
_bite_tween.tween_property(
|
2026-07-25 12:06:19 -04:00
|
|
|
_bobber,
|
2026-07-25 17:04:39 -04:00
|
|
|
"scale",
|
2026-08-08 10:25:17 -04:00
|
|
|
_bobber_base_scale * 0.68,
|
2026-07-25 17:04:39 -04:00
|
|
|
0.08
|
2026-07-25 12:06:19 -04:00
|
|
|
)
|
2026-07-25 17:04:39 -04:00
|
|
|
_bite_tween.tween_property(
|
2026-07-25 12:06:19 -04:00
|
|
|
_bobber,
|
2026-07-25 17:04:39 -04:00
|
|
|
"scale",
|
2026-08-08 10:25:17 -04:00
|
|
|
_bobber_base_scale * 1.2,
|
2026-07-25 17:04:39 -04:00
|
|
|
0.1
|
2026-07-25 12:06:19 -04:00
|
|
|
)
|
2026-07-25 17:04:39 -04:00
|
|
|
_bite_tween.tween_property(
|
2026-07-25 12:06:19 -04:00
|
|
|
_bobber,
|
2026-07-25 17:04:39 -04:00
|
|
|
"scale",
|
2026-08-08 10:25:17 -04:00
|
|
|
_bobber_base_scale,
|
2026-07-25 17:04:39 -04:00
|
|
|
0.1
|
2026-07-25 12:06:19 -04:00
|
|
|
)
|
2026-07-25 17:04:39 -04:00
|
|
|
_bite_tween.finished.connect(_on_bite_tween_finished)
|
2026-07-25 12:06:19 -04:00
|
|
|
|
|
|
|
|
|
2026-07-30 00:54:53 -04:00
|
|
|
func show_withdrawal_position(world_position: Vector3) -> void:
|
2026-07-25 12:06:19 -04:00
|
|
|
if _mode != VisualMode.FISHING:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_kill_active_tween()
|
2026-08-01 15:39:25 -04:00
|
|
|
_bobber_surface_position = world_position
|
|
|
|
|
_apply_bobber_idle_motion()
|
2026-07-25 12:06:19 -04:00
|
|
|
|
|
|
|
|
|
2026-08-01 15:39:25 -04:00
|
|
|
func begin_fight() -> void:
|
2026-07-25 12:06:19 -04:00
|
|
|
if _mode != VisualMode.FISHING:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_kill_active_tween()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func play_outcome(outcome: StringName) -> void:
|
|
|
|
|
if _mode == VisualMode.NONE:
|
|
|
|
|
cleanup()
|
2026-07-30 14:01:07 -04:00
|
|
|
# Gameplay completion must not depend on a presentation tween still
|
|
|
|
|
# being active. The caller owns idempotency for the outcome.
|
|
|
|
|
outcome_completed.emit(outcome)
|
2026-07-25 12:06:19 -04:00
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_kill_active_tween()
|
2026-07-25 17:04:39 -04:00
|
|
|
_kill_bite_tween()
|
2026-07-25 12:06:19 -04:00
|
|
|
_kill_rod_tween()
|
|
|
|
|
_restore_rod_neutral()
|
|
|
|
|
_mode = VisualMode.OUTCOME
|
2026-08-01 15:39:25 -04:00
|
|
|
_bobber.rotation = Vector3.ZERO
|
|
|
|
|
if outcome == &"withdrawal":
|
|
|
|
|
# Withdrawal is a visible return, not an instant cancellation. Keep the
|
|
|
|
|
# bobber present even if the preceding reel update hid or reset it.
|
|
|
|
|
_bobber.visible = true
|
2026-08-08 10:25:17 -04:00
|
|
|
_bobber.scale = _bobber_base_scale
|
2026-07-25 12:06:19 -04:00
|
|
|
_active_tween = create_tween()
|
|
|
|
|
_active_tween.set_trans(Tween.TRANS_QUAD)
|
|
|
|
|
_active_tween.set_ease(Tween.EASE_IN)
|
|
|
|
|
|
|
|
|
|
if _target_marker.visible and not _bobber.visible:
|
|
|
|
|
_active_tween.tween_property(_target_marker, "scale", Vector3.ZERO, 0.18)
|
|
|
|
|
else:
|
|
|
|
|
_target_marker.visible = false
|
|
|
|
|
match outcome:
|
|
|
|
|
&"catch":
|
2026-08-01 15:39:25 -04:00
|
|
|
_queue_bobber_return(0.35, 0.45)
|
2026-07-25 12:06:19 -04:00
|
|
|
&"escape":
|
|
|
|
|
var start_x: float = _bobber.global_position.x
|
|
|
|
|
_active_tween.tween_property(
|
|
|
|
|
_bobber,
|
|
|
|
|
"global_position:x",
|
|
|
|
|
start_x - 0.18,
|
|
|
|
|
0.08
|
|
|
|
|
)
|
|
|
|
|
_active_tween.tween_property(
|
|
|
|
|
_bobber,
|
|
|
|
|
"global_position:x",
|
|
|
|
|
start_x + 0.18,
|
|
|
|
|
0.08
|
|
|
|
|
)
|
2026-08-01 15:39:25 -04:00
|
|
|
_queue_bobber_return(0.42, 0.65)
|
|
|
|
|
&"invalid", &"withdrawal":
|
|
|
|
|
_queue_bobber_return(0.42, 0.65)
|
2026-07-25 12:06:19 -04:00
|
|
|
_:
|
|
|
|
|
_active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.18)
|
|
|
|
|
|
2026-07-25 15:15:24 -04:00
|
|
|
_active_tween.finished.connect(_on_outcome_finished.bind(outcome))
|
2026-07-25 12:06:19 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func cleanup() -> void:
|
|
|
|
|
_kill_active_tween()
|
2026-07-25 17:04:39 -04:00
|
|
|
_kill_bite_tween()
|
2026-07-25 12:06:19 -04:00
|
|
|
_kill_rod_tween()
|
|
|
|
|
_restore_rod_neutral()
|
|
|
|
|
_mode = VisualMode.NONE
|
|
|
|
|
_rod = null
|
|
|
|
|
_rod_tip = null
|
|
|
|
|
_target_marker.visible = false
|
|
|
|
|
_target_marker.scale = Vector3.ONE
|
2026-08-01 15:39:25 -04:00
|
|
|
_valid_target_marker.visible = true
|
|
|
|
|
_invalid_target_marker.visible = false
|
2026-07-25 12:06:19 -04:00
|
|
|
_bobber.visible = false
|
|
|
|
|
_bobber.scale = Vector3.ONE
|
2026-08-08 10:25:17 -04:00
|
|
|
_bobber_base_scale = Vector3.ONE
|
2026-08-01 15:39:25 -04:00
|
|
|
_bobber.rotation = Vector3.ZERO
|
|
|
|
|
_cast_arrival_position = Vector3.ZERO
|
2026-08-08 00:34:06 -04:00
|
|
|
_cast_tracks_water_surface = false
|
2026-08-01 15:39:25 -04:00
|
|
|
_bobber_surface_position = Vector3.ZERO
|
|
|
|
|
_bobber_idle_elapsed = 0.0
|
2026-07-25 13:04:23 -04:00
|
|
|
set_line_mode(LineMode.HIDDEN)
|
2026-07-25 12:06:19 -04:00
|
|
|
|
|
|
|
|
|
2026-07-25 15:15:24 -04:00
|
|
|
func _on_outcome_finished(outcome: StringName) -> void:
|
|
|
|
|
cleanup()
|
|
|
|
|
outcome_completed.emit(outcome)
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 12:06:19 -04:00
|
|
|
func _set_cast_sample(
|
|
|
|
|
progress: float,
|
|
|
|
|
start: Vector3,
|
|
|
|
|
target: Vector3,
|
|
|
|
|
) -> void:
|
|
|
|
|
var sample: Vector3 = start.lerp(target, progress)
|
|
|
|
|
sample.y += sin(progress * PI) * cast_arc_height
|
2026-08-08 00:34:06 -04:00
|
|
|
if _cast_tracks_water_surface:
|
|
|
|
|
sample.y += (
|
|
|
|
|
WaterSurfaceMotionType.get_default_height_offset()
|
|
|
|
|
* smoothstep(0.72, 1.0, progress)
|
|
|
|
|
)
|
2026-07-25 12:06:19 -04:00
|
|
|
_bobber.global_position = sample
|
|
|
|
|
|
|
|
|
|
|
2026-08-01 15:39:25 -04:00
|
|
|
func _queue_bobber_return(duration: float, arc_scale: float) -> void:
|
|
|
|
|
var return_start: Vector3 = _bobber.global_position
|
|
|
|
|
var return_target: Vector3 = return_start
|
|
|
|
|
if _rod_tip != null:
|
|
|
|
|
return_target = _rod_tip.global_position
|
|
|
|
|
_active_tween.tween_method(
|
|
|
|
|
_set_return_sample.bind(return_start, return_target, arc_scale),
|
|
|
|
|
0.0,
|
|
|
|
|
1.0,
|
|
|
|
|
duration,
|
|
|
|
|
)
|
|
|
|
|
_active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _set_return_sample(
|
|
|
|
|
progress: float,
|
|
|
|
|
start: Vector3,
|
|
|
|
|
target: Vector3,
|
|
|
|
|
arc_scale: float,
|
|
|
|
|
) -> void:
|
|
|
|
|
var eased_progress: float = 1.0 - pow(1.0 - progress, 3.0)
|
|
|
|
|
var sample: Vector3 = start.lerp(target, eased_progress)
|
|
|
|
|
sample.y += sin(eased_progress * PI) * maxf(
|
|
|
|
|
cast_arc_height * arc_scale,
|
|
|
|
|
0.75,
|
|
|
|
|
)
|
|
|
|
|
_bobber.global_position = sample
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 12:06:19 -04:00
|
|
|
func _on_cast_tween_finished() -> void:
|
|
|
|
|
if _mode != VisualMode.CASTING:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_active_tween = null
|
2026-08-01 15:39:25 -04:00
|
|
|
_bobber.global_position = _cast_arrival_position
|
|
|
|
|
_bobber_surface_position = _cast_arrival_position
|
|
|
|
|
_bobber_idle_elapsed = 0.0
|
2026-07-25 12:06:19 -04:00
|
|
|
_mode = VisualMode.FISHING
|
|
|
|
|
cast_completed.emit()
|
|
|
|
|
|
|
|
|
|
|
2026-08-01 15:39:25 -04:00
|
|
|
func _apply_bobber_idle_motion() -> void:
|
|
|
|
|
var phase: float = _bobber_idle_elapsed * bobber_bob_cycles_per_second * TAU
|
|
|
|
|
_bobber.global_position = (
|
|
|
|
|
_bobber_surface_position
|
|
|
|
|
+ Vector3.UP * sin(phase) * bobber_bob_amplitude
|
2026-07-25 12:06:19 -04:00
|
|
|
)
|
2026-08-01 15:39:25 -04:00
|
|
|
_bobber.rotation.z = (
|
|
|
|
|
deg_to_rad(bobber_tilt_degrees) * sin(phase * 0.73)
|
2026-07-30 00:54:53 -04:00
|
|
|
)
|
2026-07-25 12:06:19 -04:00
|
|
|
|
|
|
|
|
|
2026-07-25 13:04:23 -04:00
|
|
|
func _update_line(delta: float) -> void:
|
2026-07-25 12:06:19 -04:00
|
|
|
if _rod_tip == null or not is_instance_valid(_rod_tip):
|
2026-08-01 15:39:25 -04:00
|
|
|
var was_active: bool = _mode != VisualMode.NONE
|
2026-07-25 12:06:19 -04:00
|
|
|
cleanup()
|
2026-08-01 15:39:25 -04:00
|
|
|
if was_active:
|
|
|
|
|
presentation_interrupted.emit()
|
2026-07-25 12:06:19 -04:00
|
|
|
return
|
|
|
|
|
|
2026-07-25 13:04:23 -04:00
|
|
|
var rod_tip_position: Vector3 = _rod_tip.global_position
|
|
|
|
|
var bobber_position: Vector3 = _bobber.global_position
|
|
|
|
|
var target_sag: float = 0.0
|
|
|
|
|
if _line_mode == LineMode.SLACK:
|
|
|
|
|
var line_length: float = rod_tip_position.distance_to(bobber_position)
|
|
|
|
|
target_sag = clampf(
|
|
|
|
|
line_length * slack_amount,
|
|
|
|
|
minimum_slack,
|
|
|
|
|
maximum_slack
|
|
|
|
|
)
|
|
|
|
|
var sag_weight: float = 1.0 - exp(-line_transition_speed * delta)
|
|
|
|
|
_current_sag = lerpf(_current_sag, target_sag, sag_weight)
|
2026-08-01 11:16:21 -04:00
|
|
|
var rendered_points: PackedVector3Array = _build_sagged_line_points(
|
|
|
|
|
rod_tip_position,
|
|
|
|
|
bobber_position,
|
|
|
|
|
_current_sag,
|
2026-07-25 13:04:23 -04:00
|
|
|
)
|
|
|
|
|
|
2026-07-25 12:06:19 -04:00
|
|
|
_line_mesh.clear_surfaces()
|
2026-07-25 13:04:23 -04:00
|
|
|
if rendered_points.size() < 2:
|
|
|
|
|
return
|
2026-07-25 12:06:19 -04:00
|
|
|
_line_mesh.surface_begin(Mesh.PRIMITIVE_LINES)
|
2026-07-25 13:04:23 -04:00
|
|
|
for point_index: int in range(1, rendered_points.size()):
|
|
|
|
|
_line_mesh.surface_add_vertex(to_local(rendered_points[point_index - 1]))
|
|
|
|
|
_line_mesh.surface_add_vertex(to_local(rendered_points[point_index]))
|
2026-07-25 12:06:19 -04:00
|
|
|
_line_mesh.surface_end()
|
|
|
|
|
|
|
|
|
|
|
2026-08-01 11:16:21 -04:00
|
|
|
func _build_sagged_line_points(
|
2026-07-25 13:04:23 -04:00
|
|
|
start: Vector3,
|
|
|
|
|
end: Vector3,
|
|
|
|
|
sag: float,
|
2026-08-01 11:16:21 -04:00
|
|
|
) -> PackedVector3Array:
|
|
|
|
|
var points := PackedVector3Array()
|
|
|
|
|
var subdivisions: int = 1 if sag <= 0.001 else sag_interpolation_count
|
|
|
|
|
points.append(start)
|
2026-07-25 13:04:23 -04:00
|
|
|
for step: int in range(1, subdivisions + 1):
|
|
|
|
|
var progress: float = float(step) / float(subdivisions)
|
|
|
|
|
var point: Vector3 = start.lerp(end, progress)
|
|
|
|
|
if step < subdivisions and sag > 0.0:
|
|
|
|
|
point.y -= 4.0 * progress * (1.0 - progress) * sag
|
|
|
|
|
points.append(point)
|
2026-08-01 11:16:21 -04:00
|
|
|
return points
|
2026-07-25 13:04:23 -04:00
|
|
|
|
|
|
|
|
|
2026-07-25 12:06:19 -04:00
|
|
|
func _begin_rod_release() -> void:
|
|
|
|
|
_kill_rod_tween()
|
|
|
|
|
if _rod == null:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
var forward_rotation: Vector3 = _rod_neutral_rotation
|
|
|
|
|
forward_rotation.x -= deg_to_rad(rod_forward_swing_degrees)
|
|
|
|
|
_rod_tween = create_tween()
|
|
|
|
|
_rod_tween.set_trans(Tween.TRANS_QUAD)
|
|
|
|
|
_rod_tween.set_ease(Tween.EASE_OUT)
|
|
|
|
|
_rod_tween.tween_property(
|
|
|
|
|
_rod,
|
|
|
|
|
"rotation",
|
|
|
|
|
forward_rotation,
|
|
|
|
|
rod_forward_swing_duration
|
|
|
|
|
)
|
|
|
|
|
_rod_tween.set_ease(Tween.EASE_IN_OUT)
|
|
|
|
|
_rod_tween.tween_property(
|
|
|
|
|
_rod,
|
|
|
|
|
"rotation",
|
|
|
|
|
_rod_neutral_rotation,
|
|
|
|
|
rod_recovery_duration
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _restore_rod_neutral() -> void:
|
|
|
|
|
if _rod != null and is_instance_valid(_rod):
|
|
|
|
|
_rod.rotation = _rod_neutral_rotation
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 13:04:23 -04:00
|
|
|
func _set_target_validity(target_is_fishable: bool) -> void:
|
2026-08-01 15:39:25 -04:00
|
|
|
_valid_target_marker.visible = target_is_fishable
|
|
|
|
|
_invalid_target_marker.visible = not target_is_fishable
|
2026-07-25 13:04:23 -04:00
|
|
|
|
|
|
|
|
|
2026-07-25 12:06:19 -04:00
|
|
|
func _kill_active_tween() -> void:
|
|
|
|
|
if _active_tween != null and _active_tween.is_valid():
|
|
|
|
|
_active_tween.kill()
|
|
|
|
|
_active_tween = null
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 17:04:39 -04:00
|
|
|
func _kill_bite_tween() -> void:
|
|
|
|
|
if _bite_tween != null and _bite_tween.is_valid():
|
|
|
|
|
_bite_tween.kill()
|
|
|
|
|
_bite_tween = null
|
|
|
|
|
if is_instance_valid(_bobber):
|
2026-08-08 10:25:17 -04:00
|
|
|
_bobber.scale = _bobber_base_scale
|
2026-07-25 17:04:39 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_bite_tween_finished() -> void:
|
|
|
|
|
_bite_tween = null
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 12:06:19 -04:00
|
|
|
func _kill_rod_tween() -> void:
|
|
|
|
|
if _rod_tween != null and _rod_tween.is_valid():
|
|
|
|
|
_rod_tween.kill()
|
|
|
|
|
_rod_tween = null
|