From e3d86b1cb8c6629c56eadf98e0acbbb9e15958eb Mon Sep 17 00:00:00 2001 From: Voyager Date: Sat, 25 Jul 2026 12:06:19 -0400 Subject: [PATCH] Refine casting, rod feedback, and withdrawal --- fishing/fishing_presentation.gd | 374 ++++++++++++++++++++++++++++ fishing/fishing_presentation.gd.uid | 1 + fishing/fishing_spot.gd | 223 ++++++++++++++++- fishing/fishing_spot.tscn | 45 +++- player/player.gd | 26 ++ player/player.tscn | 29 ++- world/test_world.tscn | 14 +- 7 files changed, 700 insertions(+), 12 deletions(-) create mode 100644 fishing/fishing_presentation.gd create mode 100644 fishing/fishing_presentation.gd.uid diff --git a/fishing/fishing_presentation.gd b/fishing/fishing_presentation.gd new file mode 100644 index 0000000..9a84269 --- /dev/null +++ b/fishing/fishing_presentation.gd @@ -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 diff --git a/fishing/fishing_presentation.gd.uid b/fishing/fishing_presentation.gd.uid new file mode 100644 index 0000000..af86dfc --- /dev/null +++ b/fishing/fishing_presentation.gd.uid @@ -0,0 +1 @@ +uid://dmwwhk48mpxn4 diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index 6b4d3d3..99a43bc 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -2,6 +2,7 @@ class_name FishingSpot extends Area3D const FishDataType = preload("res://fish/fish_data.gd") +const FishingPresentationType = preload("res://fishing/fishing_presentation.gd") const PlayerType = preload("res://player/player.gd") signal status_changed(status: String) @@ -9,12 +10,24 @@ signal reel_progress_changed(progress: float, visible: bool) enum FishingState { READY, + AIMING_CAST, + CASTING, WAITING_FOR_BITE, BITE_ACTIVE, REELING, COOLDOWN, } +@export_category("Cast") +@export_range(0.1, 30.0, 0.05) var minimum_cast_distance: float = 0.5 +@export_range(0.1, 30.0, 0.1) var maximum_cast_distance: float = 14.0 +@export_range(0.1, 10.0, 0.1) var cast_charge_duration: float = 2.75 +@export_flags_3d_physics var fishable_surface_mask: int = 4 + +@export_category("Withdrawal") +@export_range(0.1, 20.0, 0.1) var withdrawal_rate: float = 4.9 +@export_range(0.1, 10.0, 0.1) var withdrawal_cancel_distance: float = 1.0 + @export_category("Timing") @export_range(0.1, 30.0, 0.1) var wait_time: float = 2.0 @export_range(0.1, 10.0, 0.1) var bite_window_duration: float = 1.5 @@ -26,10 +39,21 @@ enum FishingState { @export_category("Catch") @export var catchable_fish: FishDataType +@onready var _presentation: FishingPresentationType = %FishingPresentation + var state: FishingState = FishingState.READY var _nearby_player: PlayerType var _active_player: PlayerType var _state_time_remaining: float = 0.0 +var _cast_charge: float = 0.0 +var _cast_direction: Vector3 = Vector3.FORWARD +var _cast_origin_position: Vector3 +var _cast_target: Vector3 +var _cast_landing_is_fishable: bool = false +var _withdrawal_endpoint: Vector3 +var _withdrawal_progress: float = 0.0 +var _withdrawal_input_held: bool = false +var _new_cast_press_armed: bool = true var _reel_progress: float = 0.0 var _reel_input_held: bool = false var _cooldown_status: String = "" @@ -38,14 +62,30 @@ var _cooldown_status: String = "" func _ready() -> void: body_entered.connect(_on_body_entered) body_exited.connect(_on_body_exited) + _presentation.cast_completed.connect(_on_cast_completed) func _process(delta: float) -> void: + if state == FishingState.READY and not Input.is_action_pressed("fish_primary"): + _new_cast_press_armed = true + match state: + FishingState.AIMING_CAST: + _update_cast_charge(delta) + if not Input.is_action_pressed("fish_primary"): + _confirm_cast() FishingState.WAITING_FOR_BITE: _state_time_remaining -= delta if _state_time_remaining <= 0.0: _activate_bite() + else: + if ( + _withdrawal_input_held + and not Input.is_action_pressed("fish_primary") + ): + _withdrawal_input_held = false + if _withdrawal_input_held: + _update_withdrawal(delta) FishingState.BITE_ACTIVE: _state_time_remaining -= delta if _state_time_remaining <= 0.0: @@ -61,6 +101,21 @@ func _process(delta: float) -> void: func _unhandled_input(event: InputEvent) -> void: + if ( + event.is_action_pressed("ui_cancel") + and _active_player != null + and state in [ + FishingState.AIMING_CAST, + FishingState.CASTING, + FishingState.WAITING_FOR_BITE, + FishingState.BITE_ACTIVE, + FishingState.REELING, + ] + ): + _cancel_attempt() + get_viewport().set_input_as_handled() + return + if _nearby_player == null or not _nearby_player.is_local_control_enabled(): return if not event.is_action("fish_primary"): @@ -69,7 +124,11 @@ func _unhandled_input(event: InputEvent) -> void: if event.is_pressed(): match state: FishingState.READY: - _begin_fishing(_nearby_player) + if not _new_cast_press_armed: + return + _begin_aiming(_nearby_player) + FishingState.WAITING_FOR_BITE: + _withdrawal_input_held = true FishingState.BITE_ACTIVE: _confirm_hook() FishingState.REELING: @@ -77,29 +136,112 @@ func _unhandled_input(event: InputEvent) -> void: _: return get_viewport().set_input_as_handled() + elif state == FishingState.AIMING_CAST: + _confirm_cast() + get_viewport().set_input_as_handled() + elif state == FishingState.WAITING_FOR_BITE: + _withdrawal_input_held = false + get_viewport().set_input_as_handled() elif state == FishingState.REELING: _reel_input_held = false get_viewport().set_input_as_handled() -func _begin_fishing(player: PlayerType) -> void: +func _begin_aiming(player: PlayerType) -> void: if state != FishingState.READY or _active_player != null: return _active_player = player _active_player.set_movement_enabled(false) + _cast_direction = _capture_cast_direction(_active_player) + _cast_origin_position = _active_player.get_cast_origin_position() + _cast_charge = 0.0 + _cast_target = _calculate_cast_target(minimum_cast_distance) + state = FishingState.AIMING_CAST + status_changed.emit("Hold left click to aim • Release to cast") + _presentation.begin_aim( + _active_player.get_fishing_rod_tip(), + _active_player.get_fishing_rod(), + _cast_target + ) + + +func _update_cast_charge(delta: float) -> void: + if state != FishingState.AIMING_CAST: + return + + _cast_charge = minf(_cast_charge + delta / cast_charge_duration, 1.0) + var maximum_distance: float = maxf(minimum_cast_distance, maximum_cast_distance) + var distance: float = lerpf(minimum_cast_distance, maximum_distance, _cast_charge) + _cast_target = _calculate_cast_target(distance) + _presentation.update_aim_target(_cast_target, _cast_charge) + _presentation.update_rod_charge(_cast_charge) + + +func _confirm_cast() -> void: + if state != FishingState.AIMING_CAST: + return + + _cast_landing_is_fishable = _is_landing_fishable(_cast_target) + state = FishingState.CASTING + status_changed.emit("Casting...") + _presentation.begin_cast(_cast_target) + + +func _on_cast_completed() -> void: + if state != FishingState.CASTING: + return + + if not _cast_landing_is_fishable: + _cleanup_attempt("Can't fish there.", &"invalid") + return + state = FishingState.WAITING_FOR_BITE _state_time_remaining = wait_time + _withdrawal_progress = 0.0 + _withdrawal_input_held = false + _withdrawal_endpoint = Vector3( + _cast_origin_position.x + _cast_direction.x * withdrawal_cancel_distance, + _presentation.get_water_surface_height(), + _cast_origin_position.z + _cast_direction.z * withdrawal_cancel_distance + ) status_changed.emit("Waiting for a bite...") +func _update_withdrawal(delta: float) -> void: + if state != FishingState.WAITING_FOR_BITE: + return + + var landing_offset: Vector3 = _cast_target - _cast_origin_position + landing_offset.y = 0.0 + var landing_distance: float = landing_offset.length() + var withdrawable_distance: float = landing_distance - withdrawal_cancel_distance + if withdrawable_distance <= 0.0: + _cancel_from_withdrawal() + return + + _withdrawal_progress = minf( + _withdrawal_progress + withdrawal_rate * delta / withdrawable_distance, + 1.0 + ) + var current_position: Vector3 = _cast_target.lerp( + _withdrawal_endpoint, + _withdrawal_progress + ) + _presentation.show_withdrawal_position(current_position) + if is_equal_approx(_withdrawal_progress, 1.0): + _cancel_from_withdrawal() + + func _activate_bite() -> void: if state != FishingState.WAITING_FOR_BITE: return state = FishingState.BITE_ACTIVE _state_time_remaining = bite_window_duration + _withdrawal_input_held = false status_changed.emit("Bite! Left click to hook") + _presentation.show_bite() func _confirm_hook() -> void: @@ -112,6 +254,8 @@ func _confirm_hook() -> void: _reel_input_held = true status_changed.emit("Hold left click to reel") reel_progress_changed.emit(_reel_progress, true) + _presentation.begin_reeling(_withdrawal_endpoint) + _presentation.show_reel_progress(_reel_progress, _reel_input_held) func _update_reel_progress(delta: float) -> void: @@ -123,6 +267,7 @@ func _update_reel_progress(delta: float) -> void: else: _reel_progress = maxf(_reel_progress - reel_decay_rate * delta, 0.0) reel_progress_changed.emit(_reel_progress, true) + _presentation.show_reel_progress(_reel_progress, _reel_input_held) if is_equal_approx(_reel_progress, 1.0): _resolve_catch() elif is_zero_approx(_reel_progress): @@ -135,7 +280,7 @@ func _resolve_catch() -> void: return _active_player.inventory.add_fish(catchable_fish) - _cleanup_attempt("Caught %s!" % catchable_fish.display_name) + _cleanup_attempt("Caught %s!" % catchable_fish.display_name, &"catch") func _resolve_escape() -> void: @@ -145,26 +290,41 @@ func _resolve_escape() -> void: var fish_name: String = "The fish" if catchable_fish != null and not catchable_fish.display_name.is_empty(): fish_name = "The %s" % catchable_fish.display_name - _cleanup_attempt("%s got away!" % fish_name) + _cleanup_attempt("%s got away!" % fish_name, &"escape") func _miss_bite() -> void: if state != FishingState.BITE_ACTIVE: return - _cleanup_attempt("The fish got away.") + _cleanup_attempt("The fish got away.", &"miss") -func _cleanup_attempt(cooldown_message: String = "") -> void: +func _cleanup_attempt( + cooldown_message: String = "", + visual_outcome: StringName = &"", +) -> void: if _active_player != null: _active_player.set_movement_enabled(true) _active_player = null + _state_time_remaining = 0.0 + _cast_charge = 0.0 + _cast_direction = Vector3.FORWARD + _cast_origin_position = Vector3.ZERO + _cast_target = Vector3.ZERO + _cast_landing_is_fishable = false + _withdrawal_endpoint = Vector3.ZERO + _withdrawal_progress = 0.0 + _withdrawal_input_held = false _reel_input_held = false _reel_progress = 0.0 reel_progress_changed.emit(0.0, false) + if visual_outcome.is_empty(): + _presentation.cleanup() + else: + _presentation.play_outcome(visual_outcome) if cooldown_message.is_empty(): state = FishingState.READY - _state_time_remaining = 0.0 _cooldown_status = "" if _nearby_player != null: status_changed.emit("Left click to cast") @@ -178,11 +338,56 @@ func _cleanup_attempt(cooldown_message: String = "") -> void: func _return_to_ready() -> void: - _cleanup_attempt() + state = FishingState.READY + _state_time_remaining = 0.0 + _cooldown_status = "" + if _nearby_player != null: + status_changed.emit("Left click to cast") + else: + status_changed.emit("") func _cancel_attempt() -> void: - _cleanup_attempt() + _cleanup_attempt("Fishing cancelled.", &"cancel") + + +func _cancel_from_withdrawal() -> void: + if state != FishingState.WAITING_FOR_BITE: + return + _new_cast_press_armed = false + _cleanup_attempt("", &"withdrawal") + + +func _capture_cast_direction(player: PlayerType) -> Vector3: + var direction: Vector3 = player.get_facing_direction() + direction.y = 0.0 + if direction.is_zero_approx(): + direction = -player.global_basis.z + direction.y = 0.0 + if direction.is_zero_approx(): + return Vector3.FORWARD + return direction.normalized() + + +func _calculate_cast_target(distance: float) -> Vector3: + return Vector3( + _cast_origin_position.x + _cast_direction.x * distance, + _presentation.get_water_surface_height(), + _cast_origin_position.z + _cast_direction.z * distance + ) + + +func _is_landing_fishable(target: Vector3) -> bool: + var query := PhysicsPointQueryParameters3D.new() + query.position = target + query.collision_mask = fishable_surface_mask + query.collide_with_areas = true + query.collide_with_bodies = false + var results: Array[Dictionary] = get_world_3d().direct_space_state.intersect_point( + query, + 1 + ) + return not results.is_empty() func _on_body_entered(body: Node3D) -> void: diff --git a/fishing/fishing_spot.tscn b/fishing/fishing_spot.tscn index 13a22b0..5e19d62 100644 --- a/fishing/fishing_spot.tscn +++ b/fishing/fishing_spot.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=7 format=3] +[gd_scene load_steps=13 format=3] [ext_resource type="Script" path="res://fishing/fishing_spot.gd" id="1_spot"] [ext_resource type="Resource" path="res://fish/bluegill.tres" id="2_bluegill"] +[ext_resource type="Script" path="res://fishing/fishing_presentation.gd" id="3_presentation"] [sub_resource type="SphereShape3D" id="RangeShape"] radius = 3.5 @@ -22,6 +23,30 @@ emission_enabled = true emission = Color(0.3, 0.2, 0.01, 1) emission_energy_multiplier = 0.7 +[sub_resource type="SphereMesh" id="BobberMesh"] +radius = 0.12 +height = 0.24 + +[sub_resource type="StandardMaterial3D" id="BobberMaterial"] +albedo_color = Color(0.92, 0.12, 0.08, 1) +roughness = 0.45 + +[sub_resource type="StandardMaterial3D" id="LineMaterial"] +shading_mode = 0 +albedo_color = Color(0.9, 0.9, 0.82, 1) + +[sub_resource type="CylinderMesh" id="CastTargetMesh"] +top_radius = 0.28 +bottom_radius = 0.28 +height = 0.035 + +[sub_resource type="StandardMaterial3D" id="CastTargetMaterial"] +shading_mode = 0 +albedo_color = Color(0.3, 0.95, 0.72, 0.9) +emission_enabled = true +emission = Color(0.08, 0.35, 0.22, 1) +emission_energy_multiplier = 0.8 + [node name="FishingSpot" type="Area3D"] collision_layer = 0 collision_mask = 2 @@ -42,3 +67,21 @@ material_override = SubResource("MarkerMaterial") position = Vector3(0, 0.03, 0) mesh = SubResource("RangeMarkerMesh") material_override = SubResource("MarkerMaterial") + +[node name="FishingPresentation" type="Node3D" parent="."] +unique_name_in_owner = true +script = ExtResource("3_presentation") + +[node name="CastTargetMarker" type="MeshInstance3D" parent="FishingPresentation"] +unique_name_in_owner = true +mesh = SubResource("CastTargetMesh") +material_override = SubResource("CastTargetMaterial") + +[node name="FishingLine" type="MeshInstance3D" parent="FishingPresentation"] +unique_name_in_owner = true +material_override = SubResource("LineMaterial") + +[node name="Bobber" type="MeshInstance3D" parent="FishingPresentation"] +unique_name_in_owner = true +mesh = SubResource("BobberMesh") +material_override = SubResource("BobberMaterial") diff --git a/player/player.gd b/player/player.gd index 9b36022..f407b21 100644 --- a/player/player.gd +++ b/player/player.gd @@ -31,6 +31,9 @@ const FishInventoryType = preload("res://inventory/fish_inventory.gd") @onready var _spring_arm: SpringArm3D = %SpringArm3D @onready var _camera: Camera3D = %Camera3D @onready var inventory: FishInventoryType = %Inventory +@onready var _cast_origin: Marker3D = %CastOrigin +@onready var _fishing_rod: Node3D = %FishingRod +@onready var _fishing_rod_tip: Marker3D = %FishingRodTip var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity")) var _camera_dragging: bool = false @@ -171,3 +174,26 @@ func set_movement_enabled(enabled: bool) -> void: if not enabled: velocity.x = 0.0 velocity.z = 0.0 + + +func get_fishing_rod_tip() -> Marker3D: + return _fishing_rod_tip + + +func get_fishing_rod() -> Node3D: + return _fishing_rod + + +func get_cast_origin_position() -> Vector3: + return _cast_origin.global_position + + +func get_facing_direction() -> Vector3: + var facing: Vector3 = -_visuals.global_basis.z + facing.y = 0.0 + if facing.is_zero_approx(): + facing = -global_basis.z + facing.y = 0.0 + if facing.is_zero_approx(): + return Vector3.FORWARD + return facing.normalized() diff --git a/player/player.tscn b/player/player.tscn index 9d31fb0..aa59b98 100644 --- a/player/player.tscn +++ b/player/player.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=8 format=3] +[gd_scene load_steps=10 format=3] [ext_resource type="Script" path="res://player/player.gd" id="1_script"] [ext_resource type="Script" path="res://inventory/fish_inventory.gd" id="2_inventory"] @@ -20,6 +20,15 @@ size = Vector3(0.24, 0.2, 0.4) [sub_resource type="StandardMaterial3D" id="FacingMarkerMaterial"] albedo_color = Color(0.12, 0.18, 0.28, 1) +[sub_resource type="CylinderMesh" id="FishingRodMesh"] +top_radius = 0.025 +bottom_radius = 0.045 +height = 1.2 + +[sub_resource type="StandardMaterial3D" id="FishingRodMaterial"] +albedo_color = Color(0.22, 0.12, 0.06, 1) +roughness = 0.8 + [node name="Player" type="CharacterBody3D"] collision_layer = 2 collision_mask = 1 @@ -42,6 +51,24 @@ position = Vector3(0, 1.0, -0.55) mesh = SubResource("FacingMarkerMesh") material_override = SubResource("FacingMarkerMaterial") +[node name="FishingRod" type="Node3D" parent="Visuals"] +unique_name_in_owner = true +position = Vector3(0.42, 1.05, -0.2) +rotation = Vector3(-0.436332, 0, 0) + +[node name="RodMesh" type="MeshInstance3D" parent="Visuals/FishingRod"] +position = Vector3(0, 0.6, 0) +mesh = SubResource("FishingRodMesh") +material_override = SubResource("FishingRodMaterial") + +[node name="FishingRodTip" type="Marker3D" parent="Visuals/FishingRod"] +unique_name_in_owner = true +position = Vector3(0, 1.2, 0) + +[node name="CastOrigin" type="Marker3D" parent="."] +unique_name_in_owner = true +position = Vector3(0, 0.9, 0) + [node name="Inventory" type="Node" parent="."] unique_name_in_owner = true script = ExtResource("2_inventory") diff --git a/world/test_world.tscn b/world/test_world.tscn index 80c4a51..6d8893c 100644 --- a/world/test_world.tscn +++ b/world/test_world.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=10 format=3] +[gd_scene load_steps=11 format=3] [sub_resource type="BoxShape3D" id="GroundShape"] size = Vector3(30, 1, 30) @@ -21,6 +21,9 @@ albedo_color = Color(0.42, 0.25, 0.12, 1) [sub_resource type="PlaneMesh" id="WaterMesh"] size = Vector2(40, 25) +[sub_resource type="BoxShape3D" id="FishableWaterShape"] +size = Vector3(40, 0.5, 19.5) + [sub_resource type="StandardMaterial3D" id="WaterMaterial"] transparency = 1 albedo_color = Color(0.1, 0.45, 0.7, 0.72) @@ -65,3 +68,12 @@ material_override = SubResource("DockMaterial") position = Vector3(0, 0.2, -27) mesh = SubResource("WaterMesh") material_override = SubResource("WaterMaterial") + +[node name="FishableWater" type="Area3D" parent="."] +position = Vector3(0, 0.2, -29.75) +collision_layer = 4 +collision_mask = 0 +monitoring = false + +[node name="CollisionShape3D" type="CollisionShape3D" parent="FishableWater"] +shape = SubResource("FishableWaterShape")