Refine fishing validity, shoreline, and line routing
This commit is contained in:
parent
e3d86b1cb8
commit
ad848f9431
6 changed files with 669 additions and 120 deletions
|
|
@ -11,10 +11,33 @@ enum VisualMode {
|
||||||
OUTCOME,
|
OUTCOME,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum LineMode {
|
||||||
|
HIDDEN,
|
||||||
|
TAUT,
|
||||||
|
SLACK,
|
||||||
|
}
|
||||||
|
|
||||||
@export_category("Water")
|
@export_category("Water")
|
||||||
@export var water_surface_offset_y: float = -0.72
|
@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_range(0.1, 10.0, 0.1) var pickup_distance_from_player: float = 5.5
|
||||||
|
|
||||||
|
@export_category("Fishing Line")
|
||||||
|
@export_flags_3d_physics var line_obstacle_mask: int = 1
|
||||||
|
@export_range(1.0, 30.0, 0.5) var routing_probe_height: float = 8.0
|
||||||
|
@export_range(0.1, 2.0, 0.05) var obstruction_sample_spacing: float = 0.4
|
||||||
|
@export_range(0.01, 1.0, 0.01) var terrain_clearance: float = 0.1
|
||||||
|
@export_range(1, 3, 1) var maximum_obstruction_ranges: int = 1
|
||||||
|
@export_range(2, 12, 1) var maximum_contact_anchors_per_range: int = 5
|
||||||
|
@export_range(0.0, 3.0, 0.05) var route_lead_in_distance: float = 0.8
|
||||||
|
@export_range(0.0, 3.0, 0.05) var route_lead_out_distance: float = 0.8
|
||||||
|
@export_range(0.05, 2.0, 0.05) var fallback_arch_height: float = 0.35
|
||||||
|
@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
|
||||||
|
@export_range(2, 20, 1) var clear_span_interpolation_count: int = 8
|
||||||
|
@export_range(1, 8, 1) var contact_span_interpolation_count: int = 2
|
||||||
|
@export_range(0.1, 30.0, 0.1) var line_transition_speed: float = 7.0
|
||||||
|
|
||||||
@export_category("Cast")
|
@export_category("Cast")
|
||||||
@export_range(0.1, 30.0, 0.1) var target_movement_smoothing: float = 14.0
|
@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, 3.0, 0.05) var cast_travel_duration: float = 0.65
|
||||||
|
|
@ -23,13 +46,22 @@ enum VisualMode {
|
||||||
@export_range(0.0, 60.0, 1.0) var rod_forward_swing_degrees: float = 22.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_forward_swing_duration: float = 0.12
|
||||||
@export_range(0.05, 1.0, 0.01) var rod_recovery_duration: float = 0.22
|
@export_range(0.05, 1.0, 0.01) var rod_recovery_duration: float = 0.22
|
||||||
|
@export var valid_target_material: Material
|
||||||
|
@export var invalid_target_material: Material
|
||||||
|
|
||||||
@onready var _target_marker: MeshInstance3D = %CastTargetMarker
|
@onready var _target_marker: MeshInstance3D = %CastTargetMarker
|
||||||
|
@onready var _invalid_cross_a: MeshInstance3D = %InvalidCrossA
|
||||||
|
@onready var _invalid_cross_b: MeshInstance3D = %InvalidCrossB
|
||||||
@onready var _bobber: MeshInstance3D = %Bobber
|
@onready var _bobber: MeshInstance3D = %Bobber
|
||||||
@onready var _line: MeshInstance3D = %FishingLine
|
@onready var _line: MeshInstance3D = %FishingLine
|
||||||
|
|
||||||
var _mode: VisualMode = VisualMode.NONE
|
var _mode: VisualMode = VisualMode.NONE
|
||||||
|
var _line_mode: LineMode = LineMode.HIDDEN
|
||||||
var _line_mesh: ImmediateMesh = ImmediateMesh.new()
|
var _line_mesh: ImmediateMesh = ImmediateMesh.new()
|
||||||
|
var _route_points: PackedVector3Array = PackedVector3Array()
|
||||||
|
var _contact_start_anchor_index: int = -1
|
||||||
|
var _contact_end_anchor_index: int = -1
|
||||||
|
var _current_sag: float = 0.0
|
||||||
var _rod: Node3D
|
var _rod: Node3D
|
||||||
var _rod_tip: Marker3D
|
var _rod_tip: Marker3D
|
||||||
var _rod_neutral_rotation: Vector3
|
var _rod_neutral_rotation: Vector3
|
||||||
|
|
@ -52,18 +84,30 @@ func _process(delta: float) -> void:
|
||||||
_target_goal,
|
_target_goal,
|
||||||
weight
|
weight
|
||||||
)
|
)
|
||||||
if _line.visible:
|
if _line_mode != LineMode.HIDDEN:
|
||||||
_update_line()
|
_update_line(delta)
|
||||||
|
|
||||||
|
|
||||||
func get_water_surface_height() -> float:
|
func get_water_surface_height() -> float:
|
||||||
return global_position.y + water_surface_offset_y
|
return global_position.y + water_surface_offset_y
|
||||||
|
|
||||||
|
|
||||||
|
func set_line_mode(mode: LineMode) -> void:
|
||||||
|
_line_mode = mode
|
||||||
|
_line.visible = mode != LineMode.HIDDEN
|
||||||
|
if mode == LineMode.HIDDEN:
|
||||||
|
_current_sag = 0.0
|
||||||
|
_route_points.clear()
|
||||||
|
_contact_start_anchor_index = -1
|
||||||
|
_contact_end_anchor_index = -1
|
||||||
|
_line_mesh.clear_surfaces()
|
||||||
|
|
||||||
|
|
||||||
func begin_aim(
|
func begin_aim(
|
||||||
rod_tip: Marker3D,
|
rod_tip: Marker3D,
|
||||||
rod: Node3D,
|
rod: Node3D,
|
||||||
minimum_target: Vector3,
|
minimum_target: Vector3,
|
||||||
|
target_is_fishable: bool,
|
||||||
) -> void:
|
) -> void:
|
||||||
cleanup()
|
cleanup()
|
||||||
if rod_tip == null or rod == null:
|
if rod_tip == null or rod == null:
|
||||||
|
|
@ -77,6 +121,7 @@ func begin_aim(
|
||||||
_target_marker.global_position = minimum_target
|
_target_marker.global_position = minimum_target
|
||||||
_target_marker.scale = Vector3.ONE
|
_target_marker.scale = Vector3.ONE
|
||||||
_target_marker.visible = true
|
_target_marker.visible = true
|
||||||
|
_set_target_validity(target_is_fishable)
|
||||||
|
|
||||||
|
|
||||||
func update_rod_charge(charge: float) -> void:
|
func update_rod_charge(charge: float) -> void:
|
||||||
|
|
@ -88,13 +133,18 @@ func update_rod_charge(charge: float) -> void:
|
||||||
_rod.rotation = cock_rotation
|
_rod.rotation = cock_rotation
|
||||||
|
|
||||||
|
|
||||||
func update_aim_target(target: Vector3, charge: float) -> void:
|
func update_aim_target(
|
||||||
|
target: Vector3,
|
||||||
|
charge: float,
|
||||||
|
target_is_fishable: bool,
|
||||||
|
) -> void:
|
||||||
if _mode != VisualMode.AIMING:
|
if _mode != VisualMode.AIMING:
|
||||||
return
|
return
|
||||||
|
|
||||||
_target_goal = target
|
_target_goal = target
|
||||||
var maximum_response: float = smoothstep(0.9, 1.0, clampf(charge, 0.0, 1.0))
|
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)
|
_target_marker.scale = Vector3.ONE * lerpf(1.0, 1.15, maximum_response)
|
||||||
|
_set_target_validity(target_is_fishable)
|
||||||
|
|
||||||
|
|
||||||
func begin_cast(target: Vector3) -> void:
|
func begin_cast(target: Vector3) -> void:
|
||||||
|
|
@ -106,7 +156,6 @@ func begin_cast(target: Vector3) -> void:
|
||||||
_target_marker.visible = false
|
_target_marker.visible = false
|
||||||
_bobber.visible = true
|
_bobber.visible = true
|
||||||
_bobber.scale = Vector3.ONE
|
_bobber.scale = Vector3.ONE
|
||||||
_line.visible = true
|
|
||||||
_cast_position = _with_water_height(target)
|
_cast_position = _with_water_height(target)
|
||||||
_pickup_position = _calculate_pickup_position(_cast_position)
|
_pickup_position = _calculate_pickup_position(_cast_position)
|
||||||
|
|
||||||
|
|
@ -162,26 +211,21 @@ func show_withdrawal_position(position: Vector3) -> void:
|
||||||
_bobber.global_position = _with_water_height(position)
|
_bobber.global_position = _with_water_height(position)
|
||||||
|
|
||||||
|
|
||||||
func begin_reeling(pickup_position: Vector3) -> void:
|
func begin_reeling() -> void:
|
||||||
if _mode != VisualMode.FISHING:
|
if _mode != VisualMode.FISHING:
|
||||||
return
|
return
|
||||||
|
|
||||||
_kill_active_tween()
|
_kill_active_tween()
|
||||||
_cast_position = _with_water_height(_bobber.global_position)
|
_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:
|
func show_reel_position(position: Vector3, _input_held: bool) -> void:
|
||||||
if _mode != VisualMode.FISHING:
|
if _mode != VisualMode.FISHING:
|
||||||
return
|
return
|
||||||
|
|
||||||
_kill_active_tween()
|
_kill_active_tween()
|
||||||
_bobber.scale = Vector3.ONE
|
_bobber.scale = Vector3.ONE
|
||||||
_bobber.global_position = _cast_position.lerp(
|
_bobber.global_position = _with_water_height(position)
|
||||||
_pickup_position,
|
|
||||||
clampf(progress, 0.0, 1.0)
|
|
||||||
)
|
|
||||||
_bobber.global_position.y = get_water_surface_height()
|
|
||||||
|
|
||||||
|
|
||||||
func play_outcome(outcome: StringName) -> void:
|
func play_outcome(outcome: StringName) -> void:
|
||||||
|
|
@ -272,10 +316,11 @@ func cleanup() -> void:
|
||||||
_rod_tip = null
|
_rod_tip = null
|
||||||
_target_marker.visible = false
|
_target_marker.visible = false
|
||||||
_target_marker.scale = Vector3.ONE
|
_target_marker.scale = Vector3.ONE
|
||||||
|
_invalid_cross_a.visible = false
|
||||||
|
_invalid_cross_b.visible = false
|
||||||
_bobber.visible = false
|
_bobber.visible = false
|
||||||
_bobber.scale = Vector3.ONE
|
_bobber.scale = Vector3.ONE
|
||||||
_line.visible = false
|
set_line_mode(LineMode.HIDDEN)
|
||||||
_line_mesh.clear_surfaces()
|
|
||||||
|
|
||||||
|
|
||||||
func _set_cast_sample(
|
func _set_cast_sample(
|
||||||
|
|
@ -320,18 +365,342 @@ func _with_water_height(position: Vector3) -> Vector3:
|
||||||
return Vector3(position.x, get_water_surface_height(), position.z)
|
return Vector3(position.x, get_water_surface_height(), position.z)
|
||||||
|
|
||||||
|
|
||||||
func _update_line() -> void:
|
func _update_line(delta: float) -> void:
|
||||||
if _rod_tip == null or not is_instance_valid(_rod_tip):
|
if _rod_tip == null or not is_instance_valid(_rod_tip):
|
||||||
cleanup()
|
cleanup()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
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)
|
||||||
|
_route_points = _build_line_route(rod_tip_position, bobber_position)
|
||||||
|
var rendered_points: PackedVector3Array = _build_rendered_line_points(
|
||||||
|
_route_points,
|
||||||
|
_current_sag
|
||||||
|
)
|
||||||
|
|
||||||
_line_mesh.clear_surfaces()
|
_line_mesh.clear_surfaces()
|
||||||
|
if rendered_points.size() < 2:
|
||||||
|
return
|
||||||
_line_mesh.surface_begin(Mesh.PRIMITIVE_LINES)
|
_line_mesh.surface_begin(Mesh.PRIMITIVE_LINES)
|
||||||
_line_mesh.surface_add_vertex(to_local(_rod_tip.global_position))
|
for point_index: int in range(1, rendered_points.size()):
|
||||||
_line_mesh.surface_add_vertex(to_local(_bobber.global_position))
|
_line_mesh.surface_add_vertex(to_local(rendered_points[point_index - 1]))
|
||||||
|
_line_mesh.surface_add_vertex(to_local(rendered_points[point_index]))
|
||||||
_line_mesh.surface_end()
|
_line_mesh.surface_end()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_line_route(start: Vector3, end: Vector3) -> PackedVector3Array:
|
||||||
|
_contact_start_anchor_index = -1
|
||||||
|
_contact_end_anchor_index = -1
|
||||||
|
if _raycast_obstacle(start, end).is_empty():
|
||||||
|
return PackedVector3Array([start, end])
|
||||||
|
|
||||||
|
var horizontal_delta := Vector2(end.x - start.x, end.z - start.z)
|
||||||
|
var horizontal_length: float = horizontal_delta.length()
|
||||||
|
if horizontal_length <= 0.001:
|
||||||
|
return _build_fallback_arch(start, end)
|
||||||
|
|
||||||
|
var sample_count: int = clampi(
|
||||||
|
ceili(horizontal_length / obstruction_sample_spacing),
|
||||||
|
2,
|
||||||
|
64
|
||||||
|
)
|
||||||
|
var obstructed_samples := PackedByteArray()
|
||||||
|
for sample_index: int in range(sample_count + 1):
|
||||||
|
var progress: float = float(sample_index) / float(sample_count)
|
||||||
|
var base_point: Vector3 = start.lerp(end, progress)
|
||||||
|
var surface_hit: Dictionary = _probe_surface(base_point)
|
||||||
|
var is_obstructed: bool = false
|
||||||
|
if not surface_hit.is_empty():
|
||||||
|
var surface_position: Vector3 = surface_hit["position"]
|
||||||
|
var required_height: float = surface_position.y + terrain_clearance
|
||||||
|
if required_height > base_point.y:
|
||||||
|
is_obstructed = true
|
||||||
|
obstructed_samples.append(1 if is_obstructed else 0)
|
||||||
|
|
||||||
|
var obstruction_ranges: Array[Vector2i] = _find_obstruction_ranges(
|
||||||
|
obstructed_samples
|
||||||
|
)
|
||||||
|
if obstruction_ranges.is_empty():
|
||||||
|
return _build_fallback_arch(start, end)
|
||||||
|
if obstruction_ranges.size() > maximum_obstruction_ranges:
|
||||||
|
return _build_fallback_arch(start, end)
|
||||||
|
|
||||||
|
var first_sample: int = obstruction_ranges[0].x
|
||||||
|
var last_sample: int = obstruction_ranges[obstruction_ranges.size() - 1].y
|
||||||
|
var first_progress: float = float(first_sample) / float(sample_count)
|
||||||
|
var last_progress: float = float(last_sample) / float(sample_count)
|
||||||
|
var lead_in_progress: float = maxf(
|
||||||
|
0.0,
|
||||||
|
first_progress - route_lead_in_distance / horizontal_length
|
||||||
|
)
|
||||||
|
var lead_out_progress: float = minf(
|
||||||
|
1.0,
|
||||||
|
last_progress + route_lead_out_distance / horizontal_length
|
||||||
|
)
|
||||||
|
|
||||||
|
var route := PackedVector3Array([start])
|
||||||
|
_append_unique_route_point(route, start.lerp(end, lead_in_progress))
|
||||||
|
_contact_start_anchor_index = route.size() - 1
|
||||||
|
_append_contact_anchors(
|
||||||
|
route,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
first_sample,
|
||||||
|
last_sample,
|
||||||
|
sample_count
|
||||||
|
)
|
||||||
|
_append_unique_route_point(route, start.lerp(end, lead_out_progress))
|
||||||
|
_contact_end_anchor_index = route.size() - 1
|
||||||
|
_append_unique_route_point(route, end)
|
||||||
|
route = _remove_near_duplicate_route_points(route)
|
||||||
|
_update_contact_indices_after_reduction(route)
|
||||||
|
return _validate_reduced_route(route, start, end)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_rendered_line_points(
|
||||||
|
route: PackedVector3Array,
|
||||||
|
sag: float,
|
||||||
|
) -> PackedVector3Array:
|
||||||
|
var points := PackedVector3Array()
|
||||||
|
if route.is_empty():
|
||||||
|
return points
|
||||||
|
if (
|
||||||
|
_contact_start_anchor_index < 0
|
||||||
|
or _contact_end_anchor_index < _contact_start_anchor_index
|
||||||
|
or _contact_end_anchor_index >= route.size()
|
||||||
|
):
|
||||||
|
_append_clear_span(points, route[0], route[route.size() - 1], sag)
|
||||||
|
return points
|
||||||
|
|
||||||
|
_append_clear_span(
|
||||||
|
points,
|
||||||
|
route[0],
|
||||||
|
route[_contact_start_anchor_index],
|
||||||
|
sag
|
||||||
|
)
|
||||||
|
for anchor_index: int in range(
|
||||||
|
_contact_start_anchor_index,
|
||||||
|
_contact_end_anchor_index
|
||||||
|
):
|
||||||
|
_append_contact_span(
|
||||||
|
points,
|
||||||
|
route[anchor_index],
|
||||||
|
route[anchor_index + 1]
|
||||||
|
)
|
||||||
|
_append_clear_span(
|
||||||
|
points,
|
||||||
|
route[_contact_end_anchor_index],
|
||||||
|
route[route.size() - 1],
|
||||||
|
sag
|
||||||
|
)
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
func _raycast_obstacle(from: Vector3, to: Vector3) -> Dictionary:
|
||||||
|
if from.is_equal_approx(to):
|
||||||
|
return {}
|
||||||
|
var query := PhysicsRayQueryParameters3D.create(
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
line_obstacle_mask
|
||||||
|
)
|
||||||
|
query.collide_with_areas = false
|
||||||
|
query.collide_with_bodies = true
|
||||||
|
return get_world_3d().direct_space_state.intersect_ray(query)
|
||||||
|
|
||||||
|
|
||||||
|
func _find_obstruction_ranges(
|
||||||
|
obstructed_samples: PackedByteArray,
|
||||||
|
) -> Array[Vector2i]:
|
||||||
|
var ranges: Array[Vector2i] = []
|
||||||
|
var range_start: int = -1
|
||||||
|
for sample_index: int in range(obstructed_samples.size()):
|
||||||
|
var is_obstructed: bool = obstructed_samples[sample_index] != 0
|
||||||
|
if is_obstructed and range_start < 0:
|
||||||
|
range_start = sample_index
|
||||||
|
elif not is_obstructed and range_start >= 0:
|
||||||
|
ranges.append(Vector2i(range_start, sample_index - 1))
|
||||||
|
range_start = -1
|
||||||
|
if range_start >= 0:
|
||||||
|
ranges.append(Vector2i(range_start, obstructed_samples.size() - 1))
|
||||||
|
return ranges
|
||||||
|
|
||||||
|
|
||||||
|
func _append_contact_anchors(
|
||||||
|
route: PackedVector3Array,
|
||||||
|
start: Vector3,
|
||||||
|
end: Vector3,
|
||||||
|
first_sample: int,
|
||||||
|
last_sample: int,
|
||||||
|
sample_count: int,
|
||||||
|
) -> void:
|
||||||
|
var available_samples: int = last_sample - first_sample + 1
|
||||||
|
var anchor_count: int = mini(
|
||||||
|
available_samples,
|
||||||
|
maximum_contact_anchors_per_range
|
||||||
|
)
|
||||||
|
for anchor_offset: int in range(anchor_count):
|
||||||
|
var weight: float = 0.0
|
||||||
|
if anchor_count > 1:
|
||||||
|
weight = float(anchor_offset) / float(anchor_count - 1)
|
||||||
|
var sample_index: int = roundi(
|
||||||
|
lerpf(float(first_sample), float(last_sample), weight)
|
||||||
|
)
|
||||||
|
var progress: float = float(sample_index) / float(sample_count)
|
||||||
|
var contact: Vector3 = _raise_point_above_obstacle(
|
||||||
|
start.lerp(end, progress)
|
||||||
|
)
|
||||||
|
_append_unique_route_point(route, contact)
|
||||||
|
|
||||||
|
|
||||||
|
func _append_unique_route_point(
|
||||||
|
route: PackedVector3Array,
|
||||||
|
point: Vector3,
|
||||||
|
) -> void:
|
||||||
|
if route.is_empty() or point.distance_to(route[route.size() - 1]) > 0.03:
|
||||||
|
route.append(point)
|
||||||
|
|
||||||
|
|
||||||
|
func _remove_near_duplicate_route_points(
|
||||||
|
route: PackedVector3Array,
|
||||||
|
) -> PackedVector3Array:
|
||||||
|
var cleaned := PackedVector3Array()
|
||||||
|
for point: Vector3 in route:
|
||||||
|
_append_unique_route_point(cleaned, point)
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
|
func _update_contact_indices_after_reduction(route: PackedVector3Array) -> void:
|
||||||
|
if route.size() <= 2:
|
||||||
|
_contact_start_anchor_index = -1
|
||||||
|
_contact_end_anchor_index = -1
|
||||||
|
return
|
||||||
|
_contact_start_anchor_index = 1
|
||||||
|
_contact_end_anchor_index = route.size() - 2
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_reduced_route(
|
||||||
|
route: PackedVector3Array,
|
||||||
|
start: Vector3,
|
||||||
|
end: Vector3,
|
||||||
|
) -> PackedVector3Array:
|
||||||
|
var correction_index: int = -1
|
||||||
|
var correction_hit: Dictionary
|
||||||
|
for segment_index: int in range(route.size() - 1):
|
||||||
|
var hit: Dictionary = _raycast_obstacle(
|
||||||
|
route[segment_index],
|
||||||
|
route[segment_index + 1]
|
||||||
|
)
|
||||||
|
if not hit.is_empty():
|
||||||
|
correction_index = segment_index + 1
|
||||||
|
correction_hit = hit
|
||||||
|
break
|
||||||
|
if correction_index < 0:
|
||||||
|
return route
|
||||||
|
|
||||||
|
var hit_position: Vector3 = correction_hit["position"]
|
||||||
|
var correction: Vector3 = _raise_point_above_obstacle(hit_position)
|
||||||
|
route.insert(correction_index, correction)
|
||||||
|
_contact_start_anchor_index = mini(
|
||||||
|
_contact_start_anchor_index,
|
||||||
|
correction_index
|
||||||
|
)
|
||||||
|
_contact_end_anchor_index = maxi(
|
||||||
|
_contact_end_anchor_index + 1,
|
||||||
|
correction_index
|
||||||
|
)
|
||||||
|
for segment_index: int in range(route.size() - 1):
|
||||||
|
if not _raycast_obstacle(
|
||||||
|
route[segment_index],
|
||||||
|
route[segment_index + 1]
|
||||||
|
).is_empty():
|
||||||
|
return _build_fallback_arch(start, end)
|
||||||
|
return route
|
||||||
|
|
||||||
|
|
||||||
|
func _append_clear_span(
|
||||||
|
points: PackedVector3Array,
|
||||||
|
start: Vector3,
|
||||||
|
end: Vector3,
|
||||||
|
sag: float,
|
||||||
|
) -> void:
|
||||||
|
if points.is_empty():
|
||||||
|
points.append(start)
|
||||||
|
elif not points[points.size() - 1].is_equal_approx(start):
|
||||||
|
points.append(start)
|
||||||
|
var subdivisions: int = 1 if sag <= 0.001 else clear_span_interpolation_count
|
||||||
|
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
|
||||||
|
point.y = maxf(
|
||||||
|
point.y,
|
||||||
|
get_water_surface_height() + terrain_clearance * 0.25
|
||||||
|
)
|
||||||
|
point = _raise_point_above_obstacle(point)
|
||||||
|
points.append(point)
|
||||||
|
|
||||||
|
|
||||||
|
func _append_contact_span(
|
||||||
|
points: PackedVector3Array,
|
||||||
|
start: Vector3,
|
||||||
|
end: Vector3,
|
||||||
|
) -> void:
|
||||||
|
if points.is_empty():
|
||||||
|
points.append(start)
|
||||||
|
elif not points[points.size() - 1].is_equal_approx(start):
|
||||||
|
points.append(start)
|
||||||
|
for step: int in range(1, contact_span_interpolation_count + 1):
|
||||||
|
var progress: float = (
|
||||||
|
float(step) / float(contact_span_interpolation_count)
|
||||||
|
)
|
||||||
|
var point: Vector3 = start.lerp(end, progress)
|
||||||
|
if step < contact_span_interpolation_count:
|
||||||
|
point = _raise_point_above_obstacle(point)
|
||||||
|
points.append(point)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_fallback_arch(
|
||||||
|
start: Vector3,
|
||||||
|
end: Vector3,
|
||||||
|
) -> PackedVector3Array:
|
||||||
|
var route := PackedVector3Array([start])
|
||||||
|
for progress: float in [0.25, 0.5, 0.75]:
|
||||||
|
var point: Vector3 = start.lerp(end, progress)
|
||||||
|
point.y += sin(progress * PI) * fallback_arch_height
|
||||||
|
route.append(_raise_point_above_obstacle(point))
|
||||||
|
route.append(end)
|
||||||
|
_contact_start_anchor_index = 1
|
||||||
|
_contact_end_anchor_index = route.size() - 2
|
||||||
|
return route
|
||||||
|
|
||||||
|
|
||||||
|
func _probe_surface(point: Vector3) -> Dictionary:
|
||||||
|
var probe_start: Vector3 = point + Vector3.UP * routing_probe_height
|
||||||
|
var probe_end: Vector3 = point - Vector3.UP * routing_probe_height
|
||||||
|
return _raycast_obstacle(probe_start, probe_end)
|
||||||
|
|
||||||
|
|
||||||
|
func _raise_point_above_obstacle(point: Vector3) -> Vector3:
|
||||||
|
var hit: Dictionary = _probe_surface(point)
|
||||||
|
if hit.is_empty():
|
||||||
|
return point
|
||||||
|
var surface_position: Vector3 = hit["position"]
|
||||||
|
point.y = maxf(point.y, surface_position.y + terrain_clearance)
|
||||||
|
return point
|
||||||
|
|
||||||
|
|
||||||
func _begin_rod_release() -> void:
|
func _begin_rod_release() -> void:
|
||||||
_kill_rod_tween()
|
_kill_rod_tween()
|
||||||
if _rod == null:
|
if _rod == null:
|
||||||
|
|
@ -362,6 +731,15 @@ func _restore_rod_neutral() -> void:
|
||||||
_rod.rotation = _rod_neutral_rotation
|
_rod.rotation = _rod_neutral_rotation
|
||||||
|
|
||||||
|
|
||||||
|
func _set_target_validity(target_is_fishable: bool) -> void:
|
||||||
|
if target_is_fishable:
|
||||||
|
_target_marker.material_override = valid_target_material
|
||||||
|
else:
|
||||||
|
_target_marker.material_override = invalid_target_material
|
||||||
|
_invalid_cross_a.visible = not target_is_fishable
|
||||||
|
_invalid_cross_b.visible = not target_is_fishable
|
||||||
|
|
||||||
|
|
||||||
func _kill_active_tween() -> void:
|
func _kill_active_tween() -> void:
|
||||||
if _active_tween != null and _active_tween.is_valid():
|
if _active_tween != null and _active_tween.is_valid():
|
||||||
_active_tween.kill()
|
_active_tween.kill()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
class_name FishingSpot
|
class_name FishingSpot
|
||||||
extends Area3D
|
extends Node3D
|
||||||
|
|
||||||
const FishDataType = preload("res://fish/fish_data.gd")
|
const FishDataType = preload("res://fish/fish_data.gd")
|
||||||
const FishingPresentationType = preload("res://fishing/fishing_presentation.gd")
|
const FishingPresentationType = preload("res://fishing/fishing_presentation.gd")
|
||||||
|
|
@ -23,6 +23,13 @@ enum FishingState {
|
||||||
@export_range(0.1, 30.0, 0.1) var maximum_cast_distance: float = 14.0
|
@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_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_flags_3d_physics var fishable_surface_mask: int = 4
|
||||||
|
@export_range(0.01, 1.0, 0.01) var fishable_query_radius: float = 0.08
|
||||||
|
|
||||||
|
@export_category("Target Preview")
|
||||||
|
@export_flags_3d_physics var preview_surface_mask: int = 5
|
||||||
|
@export_range(1.0, 100.0, 1.0) var preview_ray_start_height: float = 30.0
|
||||||
|
@export_range(1.0, 200.0, 1.0) var preview_ray_length: float = 60.0
|
||||||
|
@export_range(0.01, 1.0, 0.01) var preview_marker_vertical_offset: float = 0.06
|
||||||
|
|
||||||
@export_category("Withdrawal")
|
@export_category("Withdrawal")
|
||||||
@export_range(0.1, 20.0, 0.1) var withdrawal_rate: float = 4.9
|
@export_range(0.1, 20.0, 0.1) var withdrawal_rate: float = 4.9
|
||||||
|
|
@ -42,7 +49,7 @@ enum FishingState {
|
||||||
@onready var _presentation: FishingPresentationType = %FishingPresentation
|
@onready var _presentation: FishingPresentationType = %FishingPresentation
|
||||||
|
|
||||||
var state: FishingState = FishingState.READY
|
var state: FishingState = FishingState.READY
|
||||||
var _nearby_player: PlayerType
|
var _local_player: PlayerType
|
||||||
var _active_player: PlayerType
|
var _active_player: PlayerType
|
||||||
var _state_time_remaining: float = 0.0
|
var _state_time_remaining: float = 0.0
|
||||||
var _cast_charge: float = 0.0
|
var _cast_charge: float = 0.0
|
||||||
|
|
@ -54,17 +61,22 @@ var _withdrawal_endpoint: Vector3
|
||||||
var _withdrawal_progress: float = 0.0
|
var _withdrawal_progress: float = 0.0
|
||||||
var _withdrawal_input_held: bool = false
|
var _withdrawal_input_held: bool = false
|
||||||
var _new_cast_press_armed: bool = true
|
var _new_cast_press_armed: bool = true
|
||||||
|
var _bobber_water_position: Vector3
|
||||||
|
var _reel_start_position: Vector3
|
||||||
var _reel_progress: float = 0.0
|
var _reel_progress: float = 0.0
|
||||||
var _reel_input_held: bool = false
|
var _reel_input_held: bool = false
|
||||||
var _cooldown_status: String = ""
|
var _cooldown_status: String = ""
|
||||||
|
var _fishable_query_shape: SphereShape3D = SphereShape3D.new()
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
body_entered.connect(_on_body_entered)
|
|
||||||
body_exited.connect(_on_body_exited)
|
|
||||||
_presentation.cast_completed.connect(_on_cast_completed)
|
_presentation.cast_completed.connect(_on_cast_completed)
|
||||||
|
|
||||||
|
|
||||||
|
func setup(local_player: PlayerType) -> void:
|
||||||
|
_local_player = local_player
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
if state == FishingState.READY and not Input.is_action_pressed("fish_primary"):
|
if state == FishingState.READY and not Input.is_action_pressed("fish_primary"):
|
||||||
_new_cast_press_armed = true
|
_new_cast_press_armed = true
|
||||||
|
|
@ -75,17 +87,7 @@ func _process(delta: float) -> void:
|
||||||
if not Input.is_action_pressed("fish_primary"):
|
if not Input.is_action_pressed("fish_primary"):
|
||||||
_confirm_cast()
|
_confirm_cast()
|
||||||
FishingState.WAITING_FOR_BITE:
|
FishingState.WAITING_FOR_BITE:
|
||||||
_state_time_remaining -= delta
|
_update_waiting_for_bite(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:
|
FishingState.BITE_ACTIVE:
|
||||||
_state_time_remaining -= delta
|
_state_time_remaining -= delta
|
||||||
if _state_time_remaining <= 0.0:
|
if _state_time_remaining <= 0.0:
|
||||||
|
|
@ -116,7 +118,7 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
|
||||||
if _nearby_player == null or not _nearby_player.is_local_control_enabled():
|
if _local_player == null or not _local_player.is_local_control_enabled():
|
||||||
return
|
return
|
||||||
if not event.is_action("fish_primary"):
|
if not event.is_action("fish_primary"):
|
||||||
return
|
return
|
||||||
|
|
@ -126,13 +128,19 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
FishingState.READY:
|
FishingState.READY:
|
||||||
if not _new_cast_press_armed:
|
if not _new_cast_press_armed:
|
||||||
return
|
return
|
||||||
_begin_aiming(_nearby_player)
|
_begin_aiming(_local_player)
|
||||||
FishingState.WAITING_FOR_BITE:
|
FishingState.WAITING_FOR_BITE:
|
||||||
_withdrawal_input_held = true
|
_withdrawal_input_held = true
|
||||||
|
_presentation.set_line_mode(
|
||||||
|
FishingPresentationType.LineMode.TAUT
|
||||||
|
)
|
||||||
FishingState.BITE_ACTIVE:
|
FishingState.BITE_ACTIVE:
|
||||||
_confirm_hook()
|
_confirm_hook()
|
||||||
FishingState.REELING:
|
FishingState.REELING:
|
||||||
_reel_input_held = true
|
_reel_input_held = true
|
||||||
|
_presentation.set_line_mode(
|
||||||
|
FishingPresentationType.LineMode.TAUT
|
||||||
|
)
|
||||||
_:
|
_:
|
||||||
return
|
return
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
|
@ -141,6 +149,7 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
elif state == FishingState.WAITING_FOR_BITE:
|
elif state == FishingState.WAITING_FOR_BITE:
|
||||||
_withdrawal_input_held = false
|
_withdrawal_input_held = false
|
||||||
|
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
elif state == FishingState.REELING:
|
elif state == FishingState.REELING:
|
||||||
_reel_input_held = false
|
_reel_input_held = false
|
||||||
|
|
@ -159,10 +168,13 @@ func _begin_aiming(player: PlayerType) -> void:
|
||||||
_cast_target = _calculate_cast_target(minimum_cast_distance)
|
_cast_target = _calculate_cast_target(minimum_cast_distance)
|
||||||
state = FishingState.AIMING_CAST
|
state = FishingState.AIMING_CAST
|
||||||
status_changed.emit("Hold left click to aim • Release to cast")
|
status_changed.emit("Hold left click to aim • Release to cast")
|
||||||
|
var target_is_fishable: bool = is_target_fishable(_cast_target)
|
||||||
|
var preview_position: Vector3 = _resolve_preview_surface_position(_cast_target)
|
||||||
_presentation.begin_aim(
|
_presentation.begin_aim(
|
||||||
_active_player.get_fishing_rod_tip(),
|
_active_player.get_fishing_rod_tip(),
|
||||||
_active_player.get_fishing_rod(),
|
_active_player.get_fishing_rod(),
|
||||||
_cast_target
|
preview_position,
|
||||||
|
target_is_fishable
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -174,7 +186,13 @@ func _update_cast_charge(delta: float) -> void:
|
||||||
var maximum_distance: float = maxf(minimum_cast_distance, maximum_cast_distance)
|
var maximum_distance: float = maxf(minimum_cast_distance, maximum_cast_distance)
|
||||||
var distance: float = lerpf(minimum_cast_distance, maximum_distance, _cast_charge)
|
var distance: float = lerpf(minimum_cast_distance, maximum_distance, _cast_charge)
|
||||||
_cast_target = _calculate_cast_target(distance)
|
_cast_target = _calculate_cast_target(distance)
|
||||||
_presentation.update_aim_target(_cast_target, _cast_charge)
|
var target_is_fishable: bool = is_target_fishable(_cast_target)
|
||||||
|
var preview_position: Vector3 = _resolve_preview_surface_position(_cast_target)
|
||||||
|
_presentation.update_aim_target(
|
||||||
|
preview_position,
|
||||||
|
_cast_charge,
|
||||||
|
target_is_fishable
|
||||||
|
)
|
||||||
_presentation.update_rod_charge(_cast_charge)
|
_presentation.update_rod_charge(_cast_charge)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -182,16 +200,17 @@ func _confirm_cast() -> void:
|
||||||
if state != FishingState.AIMING_CAST:
|
if state != FishingState.AIMING_CAST:
|
||||||
return
|
return
|
||||||
|
|
||||||
_cast_landing_is_fishable = _is_landing_fishable(_cast_target)
|
|
||||||
state = FishingState.CASTING
|
state = FishingState.CASTING
|
||||||
status_changed.emit("Casting...")
|
status_changed.emit("Casting...")
|
||||||
_presentation.begin_cast(_cast_target)
|
_presentation.begin_cast(_cast_target)
|
||||||
|
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
||||||
|
|
||||||
|
|
||||||
func _on_cast_completed() -> void:
|
func _on_cast_completed() -> void:
|
||||||
if state != FishingState.CASTING:
|
if state != FishingState.CASTING:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
_cast_landing_is_fishable = is_target_fishable(_cast_target)
|
||||||
if not _cast_landing_is_fishable:
|
if not _cast_landing_is_fishable:
|
||||||
_cleanup_attempt("Can't fish there.", &"invalid")
|
_cleanup_attempt("Can't fish there.", &"invalid")
|
||||||
return
|
return
|
||||||
|
|
@ -200,39 +219,114 @@ func _on_cast_completed() -> void:
|
||||||
_state_time_remaining = wait_time
|
_state_time_remaining = wait_time
|
||||||
_withdrawal_progress = 0.0
|
_withdrawal_progress = 0.0
|
||||||
_withdrawal_input_held = false
|
_withdrawal_input_held = false
|
||||||
|
_bobber_water_position = _cast_target
|
||||||
_withdrawal_endpoint = Vector3(
|
_withdrawal_endpoint = Vector3(
|
||||||
_cast_origin_position.x + _cast_direction.x * withdrawal_cancel_distance,
|
_cast_origin_position.x + _cast_direction.x * withdrawal_cancel_distance,
|
||||||
_presentation.get_water_surface_height(),
|
_presentation.get_water_surface_height(),
|
||||||
_cast_origin_position.z + _cast_direction.z * withdrawal_cancel_distance
|
_cast_origin_position.z + _cast_direction.z * withdrawal_cancel_distance
|
||||||
)
|
)
|
||||||
|
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
||||||
status_changed.emit("Waiting for a bite...")
|
status_changed.emit("Waiting for a bite...")
|
||||||
|
|
||||||
|
|
||||||
|
func _update_waiting_for_bite(delta: float) -> void:
|
||||||
|
if (
|
||||||
|
_withdrawal_input_held
|
||||||
|
and not Input.is_action_pressed("fish_primary")
|
||||||
|
):
|
||||||
|
_withdrawal_input_held = false
|
||||||
|
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
||||||
|
|
||||||
|
if not _withdrawal_input_held:
|
||||||
|
_state_time_remaining -= delta
|
||||||
|
if _state_time_remaining <= 0.0:
|
||||||
|
_activate_bite()
|
||||||
|
return
|
||||||
|
|
||||||
|
var completion_time: float = _get_withdrawal_completion_time(delta)
|
||||||
|
if _state_time_remaining <= delta and _state_time_remaining <= completion_time:
|
||||||
|
_update_withdrawal(maxf(_state_time_remaining, 0.0))
|
||||||
|
if state == FishingState.WAITING_FOR_BITE:
|
||||||
|
_activate_bite()
|
||||||
|
return
|
||||||
|
|
||||||
|
_update_withdrawal(delta)
|
||||||
|
if state != FishingState.WAITING_FOR_BITE:
|
||||||
|
return
|
||||||
|
_state_time_remaining -= delta
|
||||||
|
if _state_time_remaining <= 0.0:
|
||||||
|
_activate_bite()
|
||||||
|
|
||||||
|
|
||||||
func _update_withdrawal(delta: float) -> void:
|
func _update_withdrawal(delta: float) -> void:
|
||||||
if state != FishingState.WAITING_FOR_BITE:
|
if state != FishingState.WAITING_FOR_BITE:
|
||||||
return
|
return
|
||||||
|
|
||||||
var landing_offset: Vector3 = _cast_target - _cast_origin_position
|
var withdrawable_distance: float = _get_withdrawable_distance()
|
||||||
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:
|
if withdrawable_distance <= 0.0:
|
||||||
_cancel_from_withdrawal()
|
_cancel_from_withdrawal()
|
||||||
return
|
return
|
||||||
|
|
||||||
_withdrawal_progress = minf(
|
var next_progress: float = minf(
|
||||||
_withdrawal_progress + withdrawal_rate * delta / withdrawable_distance,
|
_withdrawal_progress + withdrawal_rate * delta / withdrawable_distance,
|
||||||
1.0
|
1.0
|
||||||
)
|
)
|
||||||
var current_position: Vector3 = _cast_target.lerp(
|
var desired_position: Vector3 = _cast_target.lerp(
|
||||||
_withdrawal_endpoint,
|
_withdrawal_endpoint,
|
||||||
_withdrawal_progress
|
next_progress
|
||||||
)
|
)
|
||||||
_presentation.show_withdrawal_position(current_position)
|
var clamped_position: Vector3 = find_last_fishable_position(
|
||||||
|
_bobber_water_position,
|
||||||
|
desired_position
|
||||||
|
)
|
||||||
|
_withdrawal_progress = next_progress
|
||||||
|
_bobber_water_position = clamped_position
|
||||||
|
_presentation.show_withdrawal_position(_bobber_water_position)
|
||||||
|
if not clamped_position.is_equal_approx(desired_position):
|
||||||
|
_resolve_withdrawal_at_shore()
|
||||||
|
return
|
||||||
if is_equal_approx(_withdrawal_progress, 1.0):
|
if is_equal_approx(_withdrawal_progress, 1.0):
|
||||||
_cancel_from_withdrawal()
|
_cancel_from_withdrawal()
|
||||||
|
|
||||||
|
|
||||||
|
func _get_withdrawal_completion_time(delta: float) -> float:
|
||||||
|
var withdrawable_distance: float = _get_withdrawable_distance()
|
||||||
|
if withdrawable_distance <= 0.0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
var progress_step: float = withdrawal_rate * delta / withdrawable_distance
|
||||||
|
if progress_step <= 0.0:
|
||||||
|
return INF
|
||||||
|
var next_progress: float = minf(_withdrawal_progress + progress_step, 1.0)
|
||||||
|
var desired_position: Vector3 = _cast_target.lerp(
|
||||||
|
_withdrawal_endpoint,
|
||||||
|
next_progress
|
||||||
|
)
|
||||||
|
var clamped_position: Vector3 = find_last_fishable_position(
|
||||||
|
_bobber_water_position,
|
||||||
|
desired_position
|
||||||
|
)
|
||||||
|
if not clamped_position.is_equal_approx(desired_position):
|
||||||
|
var segment_length: float = _bobber_water_position.distance_to(
|
||||||
|
desired_position
|
||||||
|
)
|
||||||
|
if segment_length <= 0.0:
|
||||||
|
return 0.0
|
||||||
|
return delta * (
|
||||||
|
_bobber_water_position.distance_to(clamped_position)
|
||||||
|
/ segment_length
|
||||||
|
)
|
||||||
|
if is_equal_approx(next_progress, 1.0):
|
||||||
|
return delta * (1.0 - _withdrawal_progress) / progress_step
|
||||||
|
return INF
|
||||||
|
|
||||||
|
|
||||||
|
func _get_withdrawable_distance() -> float:
|
||||||
|
var landing_offset: Vector3 = _cast_target - _cast_origin_position
|
||||||
|
landing_offset.y = 0.0
|
||||||
|
return landing_offset.length() - withdrawal_cancel_distance
|
||||||
|
|
||||||
|
|
||||||
func _activate_bite() -> void:
|
func _activate_bite() -> void:
|
||||||
if state != FishingState.WAITING_FOR_BITE:
|
if state != FishingState.WAITING_FOR_BITE:
|
||||||
return
|
return
|
||||||
|
|
@ -241,6 +335,7 @@ func _activate_bite() -> void:
|
||||||
_state_time_remaining = bite_window_duration
|
_state_time_remaining = bite_window_duration
|
||||||
_withdrawal_input_held = false
|
_withdrawal_input_held = false
|
||||||
status_changed.emit("Bite! Left click to hook")
|
status_changed.emit("Bite! Left click to hook")
|
||||||
|
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
||||||
_presentation.show_bite()
|
_presentation.show_bite()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -252,10 +347,12 @@ func _confirm_hook() -> void:
|
||||||
_state_time_remaining = 0.0
|
_state_time_remaining = 0.0
|
||||||
_reel_progress = clampf(hooked_starting_progress, 0.0, 0.95)
|
_reel_progress = clampf(hooked_starting_progress, 0.0, 0.95)
|
||||||
_reel_input_held = true
|
_reel_input_held = true
|
||||||
|
_reel_start_position = _bobber_water_position
|
||||||
status_changed.emit("Hold left click to reel")
|
status_changed.emit("Hold left click to reel")
|
||||||
reel_progress_changed.emit(_reel_progress, true)
|
reel_progress_changed.emit(_reel_progress, true)
|
||||||
_presentation.begin_reeling(_withdrawal_endpoint)
|
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
||||||
_presentation.show_reel_progress(_reel_progress, _reel_input_held)
|
_presentation.begin_reeling()
|
||||||
|
_update_reel_presentation()
|
||||||
|
|
||||||
|
|
||||||
func _update_reel_progress(delta: float) -> void:
|
func _update_reel_progress(delta: float) -> void:
|
||||||
|
|
@ -267,13 +364,32 @@ func _update_reel_progress(delta: float) -> void:
|
||||||
else:
|
else:
|
||||||
_reel_progress = maxf(_reel_progress - reel_decay_rate * delta, 0.0)
|
_reel_progress = maxf(_reel_progress - reel_decay_rate * delta, 0.0)
|
||||||
reel_progress_changed.emit(_reel_progress, true)
|
reel_progress_changed.emit(_reel_progress, true)
|
||||||
_presentation.show_reel_progress(_reel_progress, _reel_input_held)
|
_update_reel_presentation()
|
||||||
if is_equal_approx(_reel_progress, 1.0):
|
if is_equal_approx(_reel_progress, 1.0):
|
||||||
_resolve_catch()
|
_resolve_catch()
|
||||||
elif is_zero_approx(_reel_progress):
|
elif is_zero_approx(_reel_progress):
|
||||||
_resolve_escape()
|
_resolve_escape()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_reel_presentation() -> void:
|
||||||
|
var desired_position: Vector3 = _reel_start_position.lerp(
|
||||||
|
_withdrawal_endpoint,
|
||||||
|
_reel_progress
|
||||||
|
)
|
||||||
|
_bobber_water_position = _get_clamped_reel_position(desired_position)
|
||||||
|
_presentation.show_reel_position(
|
||||||
|
_bobber_water_position,
|
||||||
|
_reel_input_held
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_clamped_reel_position(desired_position: Vector3) -> Vector3:
|
||||||
|
return find_last_fishable_position(
|
||||||
|
_reel_start_position,
|
||||||
|
desired_position
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _resolve_catch() -> void:
|
func _resolve_catch() -> void:
|
||||||
if state != FishingState.REELING or _active_player == null or catchable_fish == null:
|
if state != FishingState.REELING or _active_player == null or catchable_fish == null:
|
||||||
_cancel_attempt()
|
_cancel_attempt()
|
||||||
|
|
@ -315,21 +431,28 @@ func _cleanup_attempt(
|
||||||
_withdrawal_endpoint = Vector3.ZERO
|
_withdrawal_endpoint = Vector3.ZERO
|
||||||
_withdrawal_progress = 0.0
|
_withdrawal_progress = 0.0
|
||||||
_withdrawal_input_held = false
|
_withdrawal_input_held = false
|
||||||
|
_bobber_water_position = Vector3.ZERO
|
||||||
|
_reel_start_position = Vector3.ZERO
|
||||||
_reel_input_held = false
|
_reel_input_held = false
|
||||||
_reel_progress = 0.0
|
_reel_progress = 0.0
|
||||||
reel_progress_changed.emit(0.0, false)
|
reel_progress_changed.emit(0.0, false)
|
||||||
if visual_outcome.is_empty():
|
if visual_outcome.is_empty():
|
||||||
_presentation.cleanup()
|
_presentation.cleanup()
|
||||||
else:
|
else:
|
||||||
|
if visual_outcome in [&"catch", &"invalid", &"withdrawal"]:
|
||||||
|
_presentation.set_line_mode(
|
||||||
|
FishingPresentationType.LineMode.TAUT
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_presentation.set_line_mode(
|
||||||
|
FishingPresentationType.LineMode.HIDDEN
|
||||||
|
)
|
||||||
_presentation.play_outcome(visual_outcome)
|
_presentation.play_outcome(visual_outcome)
|
||||||
|
|
||||||
if cooldown_message.is_empty():
|
if cooldown_message.is_empty():
|
||||||
state = FishingState.READY
|
state = FishingState.READY
|
||||||
_cooldown_status = ""
|
_cooldown_status = ""
|
||||||
if _nearby_player != null:
|
status_changed.emit("")
|
||||||
status_changed.emit("Left click to cast")
|
|
||||||
else:
|
|
||||||
status_changed.emit("")
|
|
||||||
else:
|
else:
|
||||||
state = FishingState.COOLDOWN
|
state = FishingState.COOLDOWN
|
||||||
_state_time_remaining = cooldown_duration
|
_state_time_remaining = cooldown_duration
|
||||||
|
|
@ -341,10 +464,7 @@ func _return_to_ready() -> void:
|
||||||
state = FishingState.READY
|
state = FishingState.READY
|
||||||
_state_time_remaining = 0.0
|
_state_time_remaining = 0.0
|
||||||
_cooldown_status = ""
|
_cooldown_status = ""
|
||||||
if _nearby_player != null:
|
status_changed.emit("")
|
||||||
status_changed.emit("Left click to cast")
|
|
||||||
else:
|
|
||||||
status_changed.emit("")
|
|
||||||
|
|
||||||
|
|
||||||
func _cancel_attempt() -> void:
|
func _cancel_attempt() -> void:
|
||||||
|
|
@ -358,6 +478,12 @@ func _cancel_from_withdrawal() -> void:
|
||||||
_cleanup_attempt("", &"withdrawal")
|
_cleanup_attempt("", &"withdrawal")
|
||||||
|
|
||||||
|
|
||||||
|
func _resolve_withdrawal_at_shore() -> void:
|
||||||
|
if state != FishingState.WAITING_FOR_BITE:
|
||||||
|
return
|
||||||
|
_cancel_from_withdrawal()
|
||||||
|
|
||||||
|
|
||||||
func _capture_cast_direction(player: PlayerType) -> Vector3:
|
func _capture_cast_direction(player: PlayerType) -> Vector3:
|
||||||
var direction: Vector3 = player.get_facing_direction()
|
var direction: Vector3 = player.get_facing_direction()
|
||||||
direction.y = 0.0
|
direction.y = 0.0
|
||||||
|
|
@ -377,36 +503,79 @@ func _calculate_cast_target(distance: float) -> Vector3:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func _is_landing_fishable(target: Vector3) -> bool:
|
func is_target_fishable(target: Vector3) -> bool:
|
||||||
var query := PhysicsPointQueryParameters3D.new()
|
_fishable_query_shape.radius = fishable_query_radius
|
||||||
query.position = target
|
var query := PhysicsShapeQueryParameters3D.new()
|
||||||
|
query.shape = _fishable_query_shape
|
||||||
|
query.transform = Transform3D(
|
||||||
|
Basis.IDENTITY,
|
||||||
|
Vector3(target.x, _presentation.get_water_surface_height(), target.z)
|
||||||
|
)
|
||||||
query.collision_mask = fishable_surface_mask
|
query.collision_mask = fishable_surface_mask
|
||||||
query.collide_with_areas = true
|
query.collide_with_areas = true
|
||||||
query.collide_with_bodies = false
|
query.collide_with_bodies = false
|
||||||
var results: Array[Dictionary] = get_world_3d().direct_space_state.intersect_point(
|
var results: Array[Dictionary] = get_world_3d().direct_space_state.intersect_shape(
|
||||||
query,
|
query,
|
||||||
1
|
1
|
||||||
)
|
)
|
||||||
return not results.is_empty()
|
return not results.is_empty()
|
||||||
|
|
||||||
|
|
||||||
func _on_body_entered(body: Node3D) -> void:
|
func find_last_fishable_position(from: Vector3, to: Vector3) -> Vector3:
|
||||||
if body is not PlayerType or not body.is_local_control_enabled():
|
if from.is_equal_approx(to):
|
||||||
return
|
return from
|
||||||
if _nearby_player != null:
|
|
||||||
return
|
|
||||||
|
|
||||||
_nearby_player = body
|
var segment_length: float = from.distance_to(to)
|
||||||
if state == FishingState.READY:
|
var sample_spacing: float = maxf(fishable_query_radius, 0.05)
|
||||||
status_changed.emit("Left click to cast")
|
var sample_count: int = maxi(1, ceili(segment_length / sample_spacing))
|
||||||
|
var last_valid_fraction: float = 0.0
|
||||||
|
|
||||||
|
for sample_index: int in range(1, sample_count + 1):
|
||||||
|
var sample_fraction: float = float(sample_index) / float(sample_count)
|
||||||
|
var sample_position: Vector3 = from.lerp(to, sample_fraction)
|
||||||
|
if is_target_fishable(sample_position):
|
||||||
|
last_valid_fraction = sample_fraction
|
||||||
|
continue
|
||||||
|
|
||||||
|
var invalid_fraction: float = sample_fraction
|
||||||
|
for _iteration: int in range(10):
|
||||||
|
var midpoint: float = (
|
||||||
|
last_valid_fraction + invalid_fraction
|
||||||
|
) * 0.5
|
||||||
|
if is_target_fishable(from.lerp(to, midpoint)):
|
||||||
|
last_valid_fraction = midpoint
|
||||||
|
else:
|
||||||
|
invalid_fraction = midpoint
|
||||||
|
return from.lerp(to, last_valid_fraction)
|
||||||
|
|
||||||
|
return to
|
||||||
|
|
||||||
|
|
||||||
func _on_body_exited(body: Node3D) -> void:
|
func _resolve_preview_surface_position(target: Vector3) -> Vector3:
|
||||||
if body != _nearby_player:
|
var ray_start := Vector3(
|
||||||
return
|
target.x,
|
||||||
|
target.y + preview_ray_start_height,
|
||||||
|
target.z
|
||||||
|
)
|
||||||
|
var ray_end := Vector3(
|
||||||
|
target.x,
|
||||||
|
ray_start.y - preview_ray_length,
|
||||||
|
target.z
|
||||||
|
)
|
||||||
|
var query := PhysicsRayQueryParameters3D.create(
|
||||||
|
ray_start,
|
||||||
|
ray_end,
|
||||||
|
preview_surface_mask
|
||||||
|
)
|
||||||
|
query.collide_with_areas = true
|
||||||
|
query.collide_with_bodies = true
|
||||||
|
var result: Dictionary = get_world_3d().direct_space_state.intersect_ray(query)
|
||||||
|
if result.is_empty():
|
||||||
|
return target
|
||||||
|
|
||||||
_nearby_player = null
|
var hit_position: Vector3 = result["position"]
|
||||||
if body == _active_player:
|
return Vector3(
|
||||||
_cancel_attempt()
|
target.x,
|
||||||
else:
|
hit_position.y + preview_marker_vertical_offset,
|
||||||
status_changed.emit("")
|
target.z
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,9 @@
|
||||||
[gd_scene load_steps=13 format=3]
|
[gd_scene load_steps=11 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://fishing/fishing_spot.gd" id="1_spot"]
|
[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="Resource" path="res://fish/bluegill.tres" id="2_bluegill"]
|
||||||
[ext_resource type="Script" path="res://fishing/fishing_presentation.gd" id="3_presentation"]
|
[ext_resource type="Script" path="res://fishing/fishing_presentation.gd" id="3_presentation"]
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="RangeShape"]
|
|
||||||
radius = 3.5
|
|
||||||
|
|
||||||
[sub_resource type="CylinderMesh" id="MarkerPostMesh"]
|
|
||||||
top_radius = 0.12
|
|
||||||
bottom_radius = 0.12
|
|
||||||
height = 2.0
|
|
||||||
|
|
||||||
[sub_resource type="TorusMesh" id="RangeMarkerMesh"]
|
|
||||||
inner_radius = 3.35
|
|
||||||
outer_radius = 3.5
|
|
||||||
rings = 32
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="MarkerMaterial"]
|
|
||||||
albedo_color = Color(0.95, 0.82, 0.18, 1)
|
|
||||||
emission_enabled = true
|
|
||||||
emission = Color(0.3, 0.2, 0.01, 1)
|
|
||||||
emission_energy_multiplier = 0.7
|
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="BobberMesh"]
|
[sub_resource type="SphereMesh" id="BobberMesh"]
|
||||||
radius = 0.12
|
radius = 0.12
|
||||||
height = 0.24
|
height = 0.24
|
||||||
|
|
@ -42,41 +23,50 @@ height = 0.035
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="CastTargetMaterial"]
|
[sub_resource type="StandardMaterial3D" id="CastTargetMaterial"]
|
||||||
shading_mode = 0
|
shading_mode = 0
|
||||||
albedo_color = Color(0.3, 0.95, 0.72, 0.9)
|
albedo_color = Color(0.18, 1, 0.72, 1)
|
||||||
emission_enabled = true
|
emission_enabled = true
|
||||||
emission = Color(0.08, 0.35, 0.22, 1)
|
emission = Color(0.08, 0.8, 0.42, 1)
|
||||||
emission_energy_multiplier = 0.8
|
emission_energy_multiplier = 1.6
|
||||||
|
|
||||||
[node name="FishingSpot" type="Area3D"]
|
[sub_resource type="StandardMaterial3D" id="InvalidTargetMaterial"]
|
||||||
collision_layer = 0
|
shading_mode = 0
|
||||||
collision_mask = 2
|
albedo_color = Color(1, 0.08, 0.22, 1)
|
||||||
monitoring = true
|
emission_enabled = true
|
||||||
monitorable = false
|
emission = Color(1, 0.01, 0.08, 1)
|
||||||
|
emission_energy_multiplier = 2.2
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="InvalidCrossMesh"]
|
||||||
|
size = Vector3(0.72, 0.06, 0.12)
|
||||||
|
|
||||||
|
[node name="FishingSpot" type="Node3D"]
|
||||||
script = ExtResource("1_spot")
|
script = ExtResource("1_spot")
|
||||||
catchable_fish = ExtResource("2_bluegill")
|
catchable_fish = ExtResource("2_bluegill")
|
||||||
|
|
||||||
[node name="RangeCollision" type="CollisionShape3D" parent="."]
|
|
||||||
shape = SubResource("RangeShape")
|
|
||||||
|
|
||||||
[node name="MarkerPost" type="MeshInstance3D" parent="."]
|
|
||||||
position = Vector3(0, 1, 0)
|
|
||||||
mesh = SubResource("MarkerPostMesh")
|
|
||||||
material_override = SubResource("MarkerMaterial")
|
|
||||||
|
|
||||||
[node name="RangeMarker" type="MeshInstance3D" parent="."]
|
|
||||||
position = Vector3(0, 0.03, 0)
|
|
||||||
mesh = SubResource("RangeMarkerMesh")
|
|
||||||
material_override = SubResource("MarkerMaterial")
|
|
||||||
|
|
||||||
[node name="FishingPresentation" type="Node3D" parent="."]
|
[node name="FishingPresentation" type="Node3D" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("3_presentation")
|
script = ExtResource("3_presentation")
|
||||||
|
valid_target_material = SubResource("CastTargetMaterial")
|
||||||
|
invalid_target_material = SubResource("InvalidTargetMaterial")
|
||||||
|
|
||||||
[node name="CastTargetMarker" type="MeshInstance3D" parent="FishingPresentation"]
|
[node name="CastTargetMarker" type="MeshInstance3D" parent="FishingPresentation"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
mesh = SubResource("CastTargetMesh")
|
mesh = SubResource("CastTargetMesh")
|
||||||
material_override = SubResource("CastTargetMaterial")
|
material_override = SubResource("CastTargetMaterial")
|
||||||
|
|
||||||
|
[node name="InvalidCrossA" type="MeshInstance3D" parent="FishingPresentation/CastTargetMarker"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
position = Vector3(0, 0.075, 0)
|
||||||
|
rotation = Vector3(0, 0.785398, 0)
|
||||||
|
mesh = SubResource("InvalidCrossMesh")
|
||||||
|
material_override = SubResource("InvalidTargetMaterial")
|
||||||
|
|
||||||
|
[node name="InvalidCrossB" type="MeshInstance3D" parent="FishingPresentation/CastTargetMarker"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
position = Vector3(0, 0.075, 0)
|
||||||
|
rotation = Vector3(0, -0.785398, 0)
|
||||||
|
mesh = SubResource("InvalidCrossMesh")
|
||||||
|
material_override = SubResource("InvalidTargetMaterial")
|
||||||
|
|
||||||
[node name="FishingLine" type="MeshInstance3D" parent="FishingPresentation"]
|
[node name="FishingLine" type="MeshInstance3D" parent="FishingPresentation"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
material_override = SubResource("LineMaterial")
|
material_override = SubResource("LineMaterial")
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,5 @@ const PlayerType = preload("res://player/player.gd")
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
_fishing_spot.setup(_player)
|
||||||
_game_ui.setup(_player.inventory, _fishing_spot)
|
_game_ui.setup(_player.inventory, _fishing_spot)
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ theme_override_constants/margin_bottom = 8
|
||||||
[node name="StatusLabel" type="Label" parent="FishingPanel/MarginContainer/VBoxContainer"]
|
[node name="StatusLabel" type="Label" parent="FishingPanel/MarginContainer/VBoxContainer"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
text = "Left click to cast"
|
text = ""
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
[node name="ReelProgress" type="ProgressBar" parent="FishingPanel/MarginContainer/VBoxContainer"]
|
[node name="ReelProgress" type="ProgressBar" parent="FishingPanel/MarginContainer/VBoxContainer"]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_scene load_steps=11 format=3]
|
[gd_scene load_steps=12 format=3]
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="GroundShape"]
|
[sub_resource type="BoxShape3D" id="GroundShape"]
|
||||||
size = Vector3(30, 1, 30)
|
size = Vector3(30, 1, 30)
|
||||||
|
|
@ -22,7 +22,10 @@ albedo_color = Color(0.42, 0.25, 0.12, 1)
|
||||||
size = Vector2(40, 25)
|
size = Vector2(40, 25)
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="FishableWaterShape"]
|
[sub_resource type="BoxShape3D" id="FishableWaterShape"]
|
||||||
size = Vector3(40, 0.5, 19.5)
|
size = Vector3(40, 0.16, 19.5)
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="FishableShoreShape"]
|
||||||
|
size = Vector3(17.5, 0.16, 5.5)
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="WaterMaterial"]
|
[sub_resource type="StandardMaterial3D" id="WaterMaterial"]
|
||||||
transparency = 1
|
transparency = 1
|
||||||
|
|
@ -77,3 +80,11 @@ monitoring = false
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="FishableWater"]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="FishableWater"]
|
||||||
shape = SubResource("FishableWaterShape")
|
shape = SubResource("FishableWaterShape")
|
||||||
|
|
||||||
|
[node name="ShoreLeft" type="CollisionShape3D" parent="FishableWater"]
|
||||||
|
position = Vector3(-11.25, 0, 12.5)
|
||||||
|
shape = SubResource("FishableShoreShape")
|
||||||
|
|
||||||
|
[node name="ShoreRight" type="CollisionShape3D" parent="FishableWater"]
|
||||||
|
position = Vector3(11.25, 0, 12.5)
|
||||||
|
shape = SubResource("FishableShoreShape")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue