Unify fishing surfaces and presentation
This commit is contained in:
parent
c596b2aee7
commit
070765741c
19 changed files with 1705 additions and 372 deletions
230
fishing/fishing_surface_resolver.gd
Normal file
230
fishing/fishing_surface_resolver.gd
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue