230 lines
6.3 KiB
GDScript
230 lines
6.3 KiB
GDScript
class_name FishingSurfaceResolver
|
|
extends RefCounted
|
|
|
|
const FishableWaterRegionType = preload(
|
|
"res://world/fishable_water_region.gd"
|
|
)
|
|
const FishingSurfaceSampleType = preload(
|
|
"res://fishing/fishing_surface_sample.gd"
|
|
)
|
|
|
|
var fishable_surface_mask: int = 4
|
|
var solid_surface_mask: int = 1
|
|
var query_radius: float = 0.08
|
|
var ray_start_height: float = 100.0
|
|
var ray_length: float = 240.0
|
|
var water_occlusion_tolerance: float = 0.04
|
|
var arc_sample_spacing: float = 0.3
|
|
|
|
var _water_query_shape: CylinderShape3D = CylinderShape3D.new()
|
|
|
|
|
|
func resolve_surface(
|
|
space_state: PhysicsDirectSpaceState3D,
|
|
query_position: Vector3,
|
|
reference_y: float,
|
|
) -> FishingSurfaceSampleType:
|
|
var sample := FishingSurfaceSampleType.new()
|
|
var ray_top: float = maxf(query_position.y, reference_y) + ray_start_height
|
|
var ray_bottom: float = ray_top - ray_length
|
|
var water_region: FishableWaterRegionType = _find_highest_water_region(
|
|
space_state,
|
|
query_position,
|
|
ray_top,
|
|
ray_bottom,
|
|
)
|
|
var solid_hit: Dictionary = _find_top_solid_surface(
|
|
space_state,
|
|
query_position,
|
|
ray_top,
|
|
ray_bottom,
|
|
)
|
|
|
|
if water_region != null:
|
|
var water_height: float = water_region.get_surface_height()
|
|
var solid_is_above_water: bool = false
|
|
if not solid_hit.is_empty():
|
|
var solid_position: Vector3 = solid_hit["position"]
|
|
solid_is_above_water = (
|
|
solid_position.y >= water_height - water_occlusion_tolerance
|
|
)
|
|
if not solid_is_above_water:
|
|
sample.has_surface = true
|
|
sample.position = Vector3(
|
|
query_position.x,
|
|
water_height,
|
|
query_position.z,
|
|
)
|
|
sample.normal = Vector3.UP
|
|
sample.water_region = water_region
|
|
sample.is_water_surface = true
|
|
return sample
|
|
|
|
if not solid_hit.is_empty():
|
|
sample.has_surface = true
|
|
sample.position = solid_hit["position"]
|
|
var hit_normal: Vector3 = solid_hit.get("normal", Vector3.UP)
|
|
sample.normal = (
|
|
hit_normal.normalized()
|
|
if not hit_normal.is_zero_approx()
|
|
else Vector3.UP
|
|
)
|
|
|
|
return sample
|
|
|
|
|
|
func find_first_cast_collision(
|
|
space_state: PhysicsDirectSpaceState3D,
|
|
origin: Vector3,
|
|
target: Vector3,
|
|
arc_height: float,
|
|
) -> Dictionary:
|
|
if origin.is_equal_approx(target):
|
|
return {}
|
|
var segment_count: int = maxi(
|
|
12,
|
|
ceili(origin.distance_to(target) / maxf(arc_sample_spacing, 0.05)),
|
|
)
|
|
var previous_position: Vector3 = origin
|
|
for segment_index: int in range(1, segment_count + 1):
|
|
var progress: float = float(segment_index) / float(segment_count)
|
|
var next_position: Vector3 = origin.lerp(target, progress)
|
|
next_position.y += sin(progress * PI) * arc_height
|
|
var hit: Dictionary = _intersect_solid_segment(
|
|
space_state,
|
|
previous_position,
|
|
next_position,
|
|
)
|
|
if not hit.is_empty():
|
|
return hit
|
|
previous_position = next_position
|
|
return {}
|
|
|
|
|
|
func resolve_withdrawal_surface(
|
|
space_state: PhysicsDirectSpaceState3D,
|
|
current_position: Vector3,
|
|
desired_position: Vector3,
|
|
toward_player: Vector3,
|
|
reference_y: float,
|
|
shore_clearance: float,
|
|
) -> FishingSurfaceSampleType:
|
|
var desired_sample: FishingSurfaceSampleType = resolve_surface(
|
|
space_state,
|
|
desired_position,
|
|
reference_y,
|
|
)
|
|
if not desired_sample.is_fishable():
|
|
return _blocked_withdrawal_sample(current_position)
|
|
|
|
var horizontal_direction: Vector3 = toward_player
|
|
horizontal_direction.y = 0.0
|
|
if not horizontal_direction.is_zero_approx() and shore_clearance > 0.0:
|
|
horizontal_direction = horizontal_direction.normalized()
|
|
var lookahead_position: Vector3 = (
|
|
desired_sample.position + horizontal_direction * shore_clearance
|
|
)
|
|
var lookahead_sample: FishingSurfaceSampleType = resolve_surface(
|
|
space_state,
|
|
lookahead_position,
|
|
reference_y,
|
|
)
|
|
if not lookahead_sample.is_fishable():
|
|
return _blocked_withdrawal_sample(current_position)
|
|
|
|
return desired_sample
|
|
|
|
|
|
func _blocked_withdrawal_sample(
|
|
current_position: Vector3,
|
|
) -> FishingSurfaceSampleType:
|
|
var sample := FishingSurfaceSampleType.new()
|
|
sample.position = current_position
|
|
return sample
|
|
|
|
|
|
func _find_highest_water_region(
|
|
space_state: PhysicsDirectSpaceState3D,
|
|
query_position: Vector3,
|
|
ray_top: float,
|
|
ray_bottom: float,
|
|
) -> FishableWaterRegionType:
|
|
_water_query_shape.radius = maxf(query_radius, 0.01)
|
|
_water_query_shape.height = maxf(ray_top - ray_bottom, 0.1)
|
|
var query := PhysicsShapeQueryParameters3D.new()
|
|
query.shape = _water_query_shape
|
|
query.transform = Transform3D(
|
|
Basis.IDENTITY,
|
|
Vector3(
|
|
query_position.x,
|
|
(ray_top + ray_bottom) * 0.5,
|
|
query_position.z,
|
|
),
|
|
)
|
|
query.collision_mask = fishable_surface_mask
|
|
query.collide_with_areas = true
|
|
query.collide_with_bodies = false
|
|
var results: Array[Dictionary] = space_state.intersect_shape(query, 64)
|
|
var selected_region: FishableWaterRegionType = null
|
|
for result: Dictionary in results:
|
|
var region: FishableWaterRegionType = (
|
|
result.get("collider") as FishableWaterRegionType
|
|
)
|
|
# A water surface remains the visible top surface even when it has no
|
|
# fish pool. Fishability is a separate policy checked by the sample.
|
|
if region == null:
|
|
continue
|
|
var surface_height: float = region.get_surface_height()
|
|
if surface_height < ray_bottom or surface_height > ray_top:
|
|
continue
|
|
if selected_region == null:
|
|
selected_region = region
|
|
continue
|
|
var selected_height: float = selected_region.get_surface_height()
|
|
if (
|
|
surface_height > selected_height + 0.001
|
|
or (
|
|
is_equal_approx(surface_height, selected_height)
|
|
and (
|
|
region.selection_priority > selected_region.selection_priority
|
|
or (
|
|
region.selection_priority
|
|
== selected_region.selection_priority
|
|
and region.get_instance_id()
|
|
< selected_region.get_instance_id()
|
|
)
|
|
)
|
|
)
|
|
):
|
|
selected_region = region
|
|
return selected_region
|
|
|
|
|
|
func _find_top_solid_surface(
|
|
space_state: PhysicsDirectSpaceState3D,
|
|
query_position: Vector3,
|
|
ray_top: float,
|
|
ray_bottom: float,
|
|
) -> Dictionary:
|
|
return _intersect_solid_segment(
|
|
space_state,
|
|
Vector3(query_position.x, ray_top, query_position.z),
|
|
Vector3(query_position.x, ray_bottom, query_position.z),
|
|
)
|
|
|
|
|
|
func _intersect_solid_segment(
|
|
space_state: PhysicsDirectSpaceState3D,
|
|
from: Vector3,
|
|
to: Vector3,
|
|
) -> Dictionary:
|
|
if from.is_equal_approx(to) or solid_surface_mask == 0:
|
|
return {}
|
|
var query := PhysicsRayQueryParameters3D.create(
|
|
from,
|
|
to,
|
|
solid_surface_mask,
|
|
)
|
|
query.collide_with_areas = false
|
|
query.collide_with_bodies = true
|
|
return space_state.intersect_ray(query)
|