Refine casting, rod feedback, and withdrawal

This commit is contained in:
Alexander Sellite 2026-07-25 12:06:19 -04:00
parent bba93eb3e5
commit e3d86b1cb8
7 changed files with 700 additions and 12 deletions

View file

@ -0,0 +1,374 @@
class_name FishingPresentation
extends Node3D
signal cast_completed
enum VisualMode {
NONE,
AIMING,
CASTING,
FISHING,
OUTCOME,
}
@export_category("Water")
@export var water_surface_offset_y: float = -0.72
@export_range(0.1, 10.0, 0.1) var pickup_distance_from_player: float = 5.5
@export_category("Cast")
@export_range(0.1, 30.0, 0.1) var target_movement_smoothing: float = 14.0
@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
@onready var _target_marker: MeshInstance3D = %CastTargetMarker
@onready var _bobber: MeshInstance3D = %Bobber
@onready var _line: MeshInstance3D = %FishingLine
var _mode: VisualMode = VisualMode.NONE
var _line_mesh: ImmediateMesh = ImmediateMesh.new()
var _rod: Node3D
var _rod_tip: Marker3D
var _rod_neutral_rotation: Vector3
var _target_goal: Vector3
var _cast_position: Vector3
var _pickup_position: Vector3
var _active_tween: Tween
var _rod_tween: Tween
func _ready() -> void:
_line.mesh = _line_mesh
cleanup()
func _process(delta: float) -> void:
if _mode == VisualMode.AIMING:
var weight: float = 1.0 - exp(-target_movement_smoothing * delta)
_target_marker.global_position = _target_marker.global_position.lerp(
_target_goal,
weight
)
if _line.visible:
_update_line()
func get_water_surface_height() -> float:
return global_position.y + water_surface_offset_y
func begin_aim(
rod_tip: Marker3D,
rod: Node3D,
minimum_target: Vector3,
) -> void:
cleanup()
if rod_tip == null or rod == null:
return
_mode = VisualMode.AIMING
_rod = rod
_rod_tip = rod_tip
_rod_neutral_rotation = _rod.rotation
_target_goal = minimum_target
_target_marker.global_position = minimum_target
_target_marker.scale = Vector3.ONE
_target_marker.visible = true
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
func update_aim_target(target: Vector3, charge: float) -> void:
if _mode != VisualMode.AIMING:
return
_target_goal = target
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)
func begin_cast(target: Vector3) -> void:
if _mode != VisualMode.AIMING or _rod_tip == null:
return
_kill_active_tween()
_mode = VisualMode.CASTING
_target_marker.visible = false
_bobber.visible = true
_bobber.scale = Vector3.ONE
_line.visible = true
_cast_position = _with_water_height(target)
_pickup_position = _calculate_pickup_position(_cast_position)
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(
_set_cast_sample.bind(cast_start, _cast_position),
0.0,
1.0,
cast_travel_duration
)
_active_tween.finished.connect(_on_cast_tween_finished)
func show_bite() -> void:
if _mode != VisualMode.FISHING:
return
_kill_active_tween()
var bite_position: Vector3 = _with_water_height(_bobber.global_position)
_bobber.global_position = bite_position
_active_tween = create_tween()
_active_tween.set_trans(Tween.TRANS_QUAD)
_active_tween.tween_property(
_bobber,
"global_position:y",
get_water_surface_height() - 0.25,
0.11
)
_active_tween.tween_property(
_bobber,
"global_position:y",
get_water_surface_height() + 0.05,
0.14
)
_active_tween.tween_property(
_bobber,
"global_position:y",
bite_position.y,
0.12
)
func show_withdrawal_position(position: Vector3) -> void:
if _mode != VisualMode.FISHING:
return
_kill_active_tween()
_bobber.global_position = _with_water_height(position)
func begin_reeling(pickup_position: Vector3) -> void:
if _mode != VisualMode.FISHING:
return
_kill_active_tween()
_cast_position = _with_water_height(_bobber.global_position)
_pickup_position = _with_water_height(pickup_position)
func show_reel_progress(progress: float, _input_held: bool) -> void:
if _mode != VisualMode.FISHING:
return
_kill_active_tween()
_bobber.scale = Vector3.ONE
_bobber.global_position = _cast_position.lerp(
_pickup_position,
clampf(progress, 0.0, 1.0)
)
_bobber.global_position.y = get_water_surface_height()
func play_outcome(outcome: StringName) -> void:
if _mode == VisualMode.NONE:
cleanup()
return
_kill_active_tween()
_kill_rod_tween()
_restore_rod_neutral()
_mode = VisualMode.OUTCOME
_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":
var catch_target: Vector3 = _bobber.global_position
if _rod_tip != null:
catch_target = _rod_tip.global_position
_active_tween.set_parallel(true)
_active_tween.tween_property(_bobber, "global_position", catch_target, 0.35)
_active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.35)
&"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
)
_active_tween.tween_property(
_bobber,
"global_position:y",
get_water_surface_height() - 0.4,
0.2
)
&"miss":
_active_tween.set_parallel(true)
_active_tween.tween_property(
_bobber,
"global_position:y",
get_water_surface_height() - 0.4,
0.3
)
_active_tween.tween_property(_bobber, "scale", Vector3.ONE * 0.65, 0.3)
&"invalid":
var return_target: Vector3 = _bobber.global_position
if _rod_tip != null:
return_target = _rod_tip.global_position
_active_tween.set_parallel(true)
_active_tween.tween_property(_bobber, "global_position", return_target, 0.28)
_active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.28)
&"withdrawal":
var withdrawal_target: Vector3 = _bobber.global_position
if _rod_tip != null:
withdrawal_target = _rod_tip.global_position
_active_tween.set_parallel(true)
_active_tween.tween_property(
_bobber,
"global_position",
withdrawal_target,
0.28
)
_active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.28)
_:
_active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.18)
_active_tween.finished.connect(cleanup)
func cleanup() -> void:
_kill_active_tween()
_kill_rod_tween()
_restore_rod_neutral()
_mode = VisualMode.NONE
_rod = null
_rod_tip = null
_target_marker.visible = false
_target_marker.scale = Vector3.ONE
_bobber.visible = false
_bobber.scale = Vector3.ONE
_line.visible = false
_line_mesh.clear_surfaces()
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
_bobber.global_position = sample
func _on_cast_tween_finished() -> void:
if _mode != VisualMode.CASTING:
return
_active_tween = null
_bobber.global_position = _cast_position
_mode = VisualMode.FISHING
cast_completed.emit()
func _calculate_pickup_position(target: Vector3) -> Vector3:
var player_on_water: Vector3 = target
if _rod_tip != null:
player_on_water = _with_water_height(_rod_tip.global_position)
var outward: Vector3 = target - player_on_water
outward.y = 0.0
if outward.is_zero_approx():
outward = Vector3.FORWARD
else:
outward = outward.normalized()
var target_distance: float = player_on_water.distance_to(target)
var pickup_distance: float = minf(
pickup_distance_from_player,
target_distance * 0.8
)
return player_on_water + outward * pickup_distance
func _with_water_height(position: Vector3) -> Vector3:
return Vector3(position.x, get_water_surface_height(), position.z)
func _update_line() -> void:
if _rod_tip == null or not is_instance_valid(_rod_tip):
cleanup()
return
_line_mesh.clear_surfaces()
_line_mesh.surface_begin(Mesh.PRIMITIVE_LINES)
_line_mesh.surface_add_vertex(to_local(_rod_tip.global_position))
_line_mesh.surface_add_vertex(to_local(_bobber.global_position))
_line_mesh.surface_end()
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
func _kill_active_tween() -> void:
if _active_tween != null and _active_tween.is_valid():
_active_tween.kill()
_active_tween = null
func _kill_rod_tween() -> void:
if _rod_tween != null and _rod_tween.is_valid():
_rod_tween.kill()
_rod_tween = null