645 lines
17 KiB
GDScript
645 lines
17 KiB
GDScript
class_name RainPuddlePresentation
|
|
extends Node3D
|
|
|
|
## Client-only rain puddles. These are deliberately visual-only: they have no
|
|
## collision, water-region, fishing, persistence, or networking components.
|
|
|
|
const PUDDLE_SURFACE_MATERIALS: Array[StringName] = [
|
|
&"grass_lite",
|
|
&"grass",
|
|
&"sand",
|
|
]
|
|
const TERRAIN_COLLISION_MASK: int = 1
|
|
const MAX_PUDDLES: int = 14
|
|
const SURFACE_CELL_SIZE: float = 8.0
|
|
const SURFACE_SEARCH_CELL_RADIUS: int = 3
|
|
const SURFACE_CLEARANCE_ABOVE_WATER: float = 0.08
|
|
const MINIMUM_SURFACE_UP_DOT: float = 0.985
|
|
const MINIMUM_PLAYER_DISTANCE: float = 2.5
|
|
const MAXIMUM_PLAYER_DISTANCE: float = 17.0
|
|
const RELEASE_DISTANCE: float = 23.0
|
|
# Puddle radius is intentionally generous: the presentation should read from
|
|
# the normal gameplay camera rather than as tiny ground specks.
|
|
const MINIMUM_PUDDLE_RADIUS: float = 0.78
|
|
const MAXIMUM_PUDDLE_RADIUS: float = 2.76
|
|
const MINIMUM_PUDDLE_SPACING: float = 4.35
|
|
const MAXIMUM_PLAYER_HEIGHT_OFFSET: float = 1.0
|
|
const SPAWN_INTERVAL_SECONDS: float = 0.18
|
|
const FADE_IN_ALPHA_PER_SECOND: float = 0.85
|
|
const FADE_OUT_ALPHA_PER_SECOND: float = 0.055
|
|
const SPLASH_COOLDOWN_SECONDS: float = 0.34
|
|
const SPLASH_COLOR := Color(0.42, 0.76, 0.80, 0.92)
|
|
const PUDDLE_COLOR := Color(0.055, 0.18, 0.225, 0.42)
|
|
|
|
|
|
class PuddleState:
|
|
var visual: MeshInstance3D
|
|
var material: StandardMaterial3D
|
|
var center: Vector3 = Vector3.ZERO
|
|
var radii: Vector2 = Vector2.ONE
|
|
var rotation_y: float = 0.0
|
|
var alpha: float = 0.0
|
|
var active: bool = false
|
|
var player_was_inside: bool = false
|
|
var splash_cooldown: float = 0.0
|
|
|
|
|
|
var _test_world: TestWorld
|
|
var _weather_service: WorldWeatherService
|
|
var _local_player: Player
|
|
var _light_performance_profile: bool = false
|
|
var _is_active: bool = false
|
|
var _suppressed: bool = false
|
|
var _surface_cache_ready: bool = false
|
|
var _surface_triangles: Array[PackedVector3Array] = []
|
|
var _surface_triangle_indices_by_cell: Dictionary = {}
|
|
var _puddles: Array[PuddleState] = []
|
|
var _random := RandomNumberGenerator.new()
|
|
var _spawn_accumulator: float = 0.0
|
|
var _player_was_grounded: bool = false
|
|
var _visual_root: Node3D
|
|
var _splash_root: Node3D
|
|
var _puddle_mesh: CylinderMesh
|
|
|
|
|
|
func _ready() -> void:
|
|
set_process(false)
|
|
|
|
|
|
func _exit_tree() -> void:
|
|
if (
|
|
_weather_service != null
|
|
and _weather_service.weather_changed.is_connected(
|
|
_on_weather_changed
|
|
)
|
|
):
|
|
_weather_service.weather_changed.disconnect(_on_weather_changed)
|
|
|
|
|
|
func configure(
|
|
test_world: TestWorld,
|
|
weather_service: WorldWeatherService,
|
|
local_player: Player,
|
|
light_performance_profile: bool,
|
|
) -> void:
|
|
if (
|
|
_weather_service != null
|
|
and _weather_service.weather_changed.is_connected(
|
|
_on_weather_changed
|
|
)
|
|
):
|
|
_weather_service.weather_changed.disconnect(_on_weather_changed)
|
|
_test_world = test_world
|
|
_weather_service = weather_service
|
|
_local_player = local_player
|
|
_light_performance_profile = light_performance_profile
|
|
_random.seed = _seed_for_world()
|
|
if (
|
|
_weather_service != null
|
|
and not _weather_service.weather_changed.is_connected(
|
|
_on_weather_changed
|
|
)
|
|
):
|
|
_weather_service.weather_changed.connect(_on_weather_changed)
|
|
if not _light_performance_profile:
|
|
_prepare_visual_pool()
|
|
refresh_world()
|
|
_refresh_processing()
|
|
|
|
|
|
func set_active(active: bool) -> void:
|
|
if _is_active == active:
|
|
return
|
|
_is_active = active
|
|
if not _is_active:
|
|
_clear_puddles()
|
|
_refresh_processing()
|
|
|
|
|
|
func set_suppressed(suppressed: bool) -> void:
|
|
if _suppressed == suppressed:
|
|
return
|
|
_suppressed = suppressed
|
|
if _suppressed:
|
|
_clear_puddles()
|
|
_refresh_processing()
|
|
|
|
|
|
func refresh_world() -> void:
|
|
_surface_cache_ready = false
|
|
_surface_triangles.clear()
|
|
_surface_triangle_indices_by_cell.clear()
|
|
_spawn_accumulator = 0.0
|
|
_player_was_grounded = false
|
|
_random.seed = _seed_for_world()
|
|
_clear_puddles()
|
|
_refresh_processing()
|
|
|
|
|
|
func is_light_performance_profile() -> bool:
|
|
return _light_performance_profile
|
|
|
|
|
|
func get_active_puddle_count() -> int:
|
|
var count: int = 0
|
|
for puddle: PuddleState in _puddles:
|
|
if puddle.active and puddle.alpha > 0.0:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if not _can_present_puddles():
|
|
return
|
|
var player_position: Vector3 = _local_player.global_position
|
|
var is_raining: bool = _weather_service.is_raining()
|
|
if is_raining:
|
|
_ensure_surface_cache()
|
|
_release_distant_puddles(player_position)
|
|
_fade_puddles_in(delta)
|
|
_spawn_accumulator += delta
|
|
while _spawn_accumulator >= SPAWN_INTERVAL_SECONDS:
|
|
_spawn_accumulator -= SPAWN_INTERVAL_SECONDS
|
|
if not _spawn_puddle_near(player_position):
|
|
break
|
|
else:
|
|
_fade_puddles_out(delta)
|
|
_update_player_splashes(player_position, delta)
|
|
_refresh_processing()
|
|
|
|
|
|
func _on_weather_changed(
|
|
_weather: WorldWeatherService.Weather,
|
|
_seconds_remaining: float,
|
|
) -> void:
|
|
if _weather_service != null and _weather_service.is_raining():
|
|
_spawn_accumulator = SPAWN_INTERVAL_SECONDS
|
|
_refresh_processing()
|
|
|
|
|
|
func _can_present_puddles() -> bool:
|
|
return (
|
|
not _light_performance_profile
|
|
and _is_active
|
|
and not _suppressed
|
|
and _test_world != null
|
|
and _test_world.is_world_ready()
|
|
and _weather_service != null
|
|
and _local_player != null
|
|
and _local_player.is_local_control_enabled()
|
|
)
|
|
|
|
|
|
func _refresh_processing() -> void:
|
|
var should_process: bool = (
|
|
_can_present_puddles()
|
|
and (
|
|
_weather_service.is_raining()
|
|
or _has_active_puddles()
|
|
)
|
|
)
|
|
set_process(should_process)
|
|
|
|
|
|
func _has_active_puddles() -> bool:
|
|
for puddle: PuddleState in _puddles:
|
|
if puddle.active:
|
|
return true
|
|
return false
|
|
|
|
|
|
func _prepare_visual_pool() -> void:
|
|
if _visual_root != null:
|
|
return
|
|
_visual_root = Node3D.new()
|
|
_visual_root.name = "PuddleVisuals"
|
|
add_child(_visual_root)
|
|
_splash_root = Node3D.new()
|
|
_splash_root.name = "PuddleSplashes"
|
|
add_child(_splash_root)
|
|
_puddle_mesh = CylinderMesh.new()
|
|
_puddle_mesh.top_radius = 1.0
|
|
_puddle_mesh.bottom_radius = 1.0
|
|
_puddle_mesh.height = 0.012
|
|
_puddle_mesh.radial_segments = 12
|
|
_puddle_mesh.rings = 1
|
|
for _index: int in MAX_PUDDLES:
|
|
var material := StandardMaterial3D.new()
|
|
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
material.cull_mode = BaseMaterial3D.CULL_DISABLED
|
|
material.roughness = 0.18
|
|
material.metallic = 0.0
|
|
var visual := MeshInstance3D.new()
|
|
visual.name = "RainPuddle"
|
|
visual.mesh = _puddle_mesh
|
|
visual.material_override = material
|
|
visual.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
visual.visible = false
|
|
_visual_root.add_child(visual)
|
|
var puddle := PuddleState.new()
|
|
puddle.visual = visual
|
|
puddle.material = material
|
|
_puddles.append(puddle)
|
|
|
|
|
|
func _ensure_surface_cache() -> void:
|
|
if _surface_cache_ready or _test_world == null:
|
|
return
|
|
_surface_cache_ready = true
|
|
var saltwater_height: float = _test_world.get_saltwater_surface_height()
|
|
if is_nan(saltwater_height):
|
|
return
|
|
var candidate_triangles: Array[PackedVector3Array] = (
|
|
_test_world.get_spawn_surface_triangles(
|
|
PUDDLE_SURFACE_MATERIALS,
|
|
saltwater_height + SURFACE_CLEARANCE_ABOVE_WATER,
|
|
MINIMUM_SURFACE_UP_DOT,
|
|
)
|
|
)
|
|
for triangle: PackedVector3Array in candidate_triangles:
|
|
if not is_puddle_surface_eligible(triangle, saltwater_height):
|
|
continue
|
|
var triangle_index: int = _surface_triangles.size()
|
|
_surface_triangles.append(triangle)
|
|
var center: Vector3 = (
|
|
triangle[0] + triangle[1] + triangle[2]
|
|
) / 3.0
|
|
var cell: Vector2i = _surface_cell_for(center)
|
|
var triangle_indices: Array = (
|
|
_surface_triangle_indices_by_cell.get(cell, []) as Array
|
|
)
|
|
triangle_indices.append(triangle_index)
|
|
_surface_triangle_indices_by_cell[cell] = triangle_indices
|
|
|
|
|
|
func _spawn_puddle_near(player_position: Vector3) -> bool:
|
|
if _active_puddle_count() >= MAX_PUDDLES:
|
|
return false
|
|
var puddle: PuddleState = _first_inactive_puddle()
|
|
if puddle == null:
|
|
return false
|
|
for _attempt: int in 24:
|
|
var triangle_index: int = _random_nearby_triangle_index(
|
|
player_position
|
|
)
|
|
if triangle_index < 0 or triangle_index >= _surface_triangles.size():
|
|
return false
|
|
var triangle: PackedVector3Array = _surface_triangles[triangle_index]
|
|
var position: Vector3 = _random_point_in_triangle(triangle)
|
|
var horizontal_distance: float = Vector2(
|
|
position.x,
|
|
position.z,
|
|
).distance_to(Vector2(player_position.x, player_position.z))
|
|
if (
|
|
horizontal_distance < MINIMUM_PLAYER_DISTANCE
|
|
or horizontal_distance > MAXIMUM_PLAYER_DISTANCE
|
|
or _is_candidate_blocked(position)
|
|
or _is_near_existing_puddle(position)
|
|
):
|
|
continue
|
|
var maximum_radius: float = minf(
|
|
MAXIMUM_PUDDLE_RADIUS,
|
|
_minimum_triangle_edge_distance(position, triangle) * 0.65,
|
|
)
|
|
if maximum_radius < MINIMUM_PUDDLE_RADIUS:
|
|
continue
|
|
var radius_x: float = _random.randf_range(
|
|
MINIMUM_PUDDLE_RADIUS,
|
|
maximum_radius,
|
|
)
|
|
var radius_z: float = radius_x * _random.randf_range(0.58, 0.86)
|
|
_activate_puddle(
|
|
puddle,
|
|
position,
|
|
Vector2(radius_x, radius_z),
|
|
_random.randf_range(0.0, TAU),
|
|
)
|
|
return true
|
|
return false
|
|
|
|
|
|
func _active_puddle_count() -> int:
|
|
var count: int = 0
|
|
for puddle: PuddleState in _puddles:
|
|
if puddle.active:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _first_inactive_puddle() -> PuddleState:
|
|
for puddle: PuddleState in _puddles:
|
|
if not puddle.active:
|
|
return puddle
|
|
return null
|
|
|
|
|
|
func _random_nearby_triangle_index(player_position: Vector3) -> int:
|
|
var base_cell: Vector2i = _surface_cell_for(player_position)
|
|
for _attempt: int in 16:
|
|
var cell := base_cell + Vector2i(
|
|
_random.randi_range(
|
|
-SURFACE_SEARCH_CELL_RADIUS,
|
|
SURFACE_SEARCH_CELL_RADIUS,
|
|
),
|
|
_random.randi_range(
|
|
-SURFACE_SEARCH_CELL_RADIUS,
|
|
SURFACE_SEARCH_CELL_RADIUS,
|
|
),
|
|
)
|
|
var triangle_indices: Array = (
|
|
_surface_triangle_indices_by_cell.get(cell, []) as Array
|
|
)
|
|
if not triangle_indices.is_empty():
|
|
return int(triangle_indices[_random.randi_range(
|
|
0,
|
|
triangle_indices.size() - 1,
|
|
)])
|
|
return -1
|
|
|
|
|
|
func _activate_puddle(
|
|
puddle: PuddleState,
|
|
center: Vector3,
|
|
radii: Vector2,
|
|
rotation_y: float,
|
|
) -> void:
|
|
puddle.center = center
|
|
puddle.radii = radii
|
|
puddle.rotation_y = rotation_y
|
|
puddle.alpha = 0.0
|
|
puddle.active = true
|
|
puddle.player_was_inside = false
|
|
puddle.splash_cooldown = 0.0
|
|
puddle.visual.global_position = center + Vector3.UP * 0.012
|
|
puddle.visual.rotation = Vector3(0.0, rotation_y, 0.0)
|
|
puddle.visual.scale = Vector3(radii.x, 1.0, radii.y)
|
|
_apply_puddle_alpha(puddle)
|
|
puddle.visual.visible = true
|
|
|
|
|
|
func _fade_puddles_in(delta: float) -> void:
|
|
for puddle: PuddleState in _puddles:
|
|
if not puddle.active:
|
|
continue
|
|
puddle.alpha = minf(
|
|
1.0,
|
|
puddle.alpha + FADE_IN_ALPHA_PER_SECOND * delta,
|
|
)
|
|
_apply_puddle_alpha(puddle)
|
|
|
|
|
|
func _fade_puddles_out(delta: float) -> void:
|
|
for puddle: PuddleState in _puddles:
|
|
if not puddle.active:
|
|
continue
|
|
puddle.alpha = maxf(
|
|
0.0,
|
|
puddle.alpha - FADE_OUT_ALPHA_PER_SECOND * delta,
|
|
)
|
|
if puddle.alpha <= 0.0:
|
|
_deactivate_puddle(puddle)
|
|
continue
|
|
_apply_puddle_alpha(puddle)
|
|
|
|
|
|
func _release_distant_puddles(player_position: Vector3) -> void:
|
|
for puddle: PuddleState in _puddles:
|
|
if (
|
|
puddle.active
|
|
and Vector2(puddle.center.x, puddle.center.z).distance_to(
|
|
Vector2(player_position.x, player_position.z)
|
|
) > RELEASE_DISTANCE
|
|
):
|
|
_deactivate_puddle(puddle)
|
|
|
|
|
|
func _deactivate_puddle(puddle: PuddleState) -> void:
|
|
puddle.active = false
|
|
puddle.alpha = 0.0
|
|
puddle.player_was_inside = false
|
|
puddle.splash_cooldown = 0.0
|
|
if puddle.visual != null:
|
|
puddle.visual.visible = false
|
|
|
|
|
|
func _clear_puddles() -> void:
|
|
for puddle: PuddleState in _puddles:
|
|
_deactivate_puddle(puddle)
|
|
if _splash_root == null:
|
|
return
|
|
for splash: Node in _splash_root.get_children():
|
|
splash.queue_free()
|
|
|
|
|
|
func _apply_puddle_alpha(puddle: PuddleState) -> void:
|
|
if puddle.material == null:
|
|
return
|
|
var color := PUDDLE_COLOR
|
|
color.a *= puddle.alpha
|
|
puddle.material.albedo_color = color
|
|
|
|
|
|
func _is_candidate_blocked(position: Vector3) -> bool:
|
|
if not is_inside_tree():
|
|
return false
|
|
var world: World3D = get_world_3d()
|
|
if world == null:
|
|
return false
|
|
var query := PhysicsRayQueryParameters3D.create(
|
|
position + Vector3.UP * 0.05,
|
|
position + Vector3.UP * 1.75,
|
|
TERRAIN_COLLISION_MASK,
|
|
)
|
|
query.collide_with_areas = false
|
|
query.collide_with_bodies = true
|
|
return not world.direct_space_state.intersect_ray(query).is_empty()
|
|
|
|
|
|
func _is_near_existing_puddle(position: Vector3) -> bool:
|
|
var horizontal_position := Vector2(position.x, position.z)
|
|
for puddle: PuddleState in _puddles:
|
|
if not puddle.active:
|
|
continue
|
|
var minimum_distance: float = (
|
|
MINIMUM_PUDDLE_SPACING + maxf(puddle.radii.x, puddle.radii.y)
|
|
)
|
|
if horizontal_position.distance_to(
|
|
Vector2(puddle.center.x, puddle.center.z)
|
|
) < minimum_distance:
|
|
return true
|
|
return false
|
|
|
|
|
|
func _update_player_splashes(
|
|
player_position: Vector3,
|
|
delta: float,
|
|
) -> void:
|
|
var grounded: bool = _local_player.is_on_floor()
|
|
for puddle: PuddleState in _puddles:
|
|
if not puddle.active or puddle.alpha <= 0.08:
|
|
continue
|
|
puddle.splash_cooldown = maxf(
|
|
0.0,
|
|
puddle.splash_cooldown - delta,
|
|
)
|
|
var is_inside: bool = is_point_inside_puddle(
|
|
player_position,
|
|
puddle.center,
|
|
puddle.radii,
|
|
puddle.rotation_y,
|
|
)
|
|
var should_splash: bool = (
|
|
is_inside
|
|
and puddle.splash_cooldown <= 0.0
|
|
and (
|
|
not puddle.player_was_inside
|
|
or (not _player_was_grounded and grounded)
|
|
)
|
|
)
|
|
if should_splash:
|
|
_emit_splash(player_position, puddle.center.y)
|
|
puddle.splash_cooldown = SPLASH_COOLDOWN_SECONDS
|
|
puddle.player_was_inside = is_inside
|
|
_player_was_grounded = grounded
|
|
|
|
|
|
func _emit_splash(player_position: Vector3, surface_height: float) -> void:
|
|
if _splash_root == null or not _splash_root.visible:
|
|
return
|
|
var particles := CPUParticles3D.new()
|
|
particles.name = "RainPuddleSplash"
|
|
particles.global_position = Vector3(
|
|
player_position.x,
|
|
surface_height + 0.04,
|
|
player_position.z,
|
|
)
|
|
particles.amount = 7
|
|
particles.lifetime = 0.38
|
|
particles.one_shot = true
|
|
particles.local_coords = true
|
|
particles.explosiveness = 1.0
|
|
particles.direction = Vector3.UP
|
|
particles.spread = 48.0
|
|
particles.gravity = Vector3(0.0, -3.2, 0.0)
|
|
particles.initial_velocity_min = 0.65
|
|
particles.initial_velocity_max = 1.15
|
|
particles.scale_amount_min = 0.7
|
|
particles.scale_amount_max = 1.0
|
|
var material := StandardMaterial3D.new()
|
|
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
material.albedo_color = SPLASH_COLOR
|
|
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
material.roughness = 1.0
|
|
var droplet_mesh := BoxMesh.new()
|
|
droplet_mesh.size = Vector3(0.028, 0.04, 0.028)
|
|
droplet_mesh.material = material
|
|
particles.mesh = droplet_mesh
|
|
_splash_root.add_child(particles)
|
|
particles.emitting = true
|
|
var cleanup := create_tween()
|
|
cleanup.tween_interval(0.5)
|
|
cleanup.tween_callback(particles.queue_free)
|
|
|
|
|
|
func _seed_for_world() -> int:
|
|
if _test_world == null:
|
|
return 1
|
|
return max(_test_world.get_generation_seed(), 1) ^ 0x5261696e
|
|
|
|
|
|
func _surface_cell_for(position: Vector3) -> Vector2i:
|
|
return Vector2i(
|
|
floori(position.x / SURFACE_CELL_SIZE),
|
|
floori(position.z / SURFACE_CELL_SIZE),
|
|
)
|
|
|
|
|
|
func _random_point_in_triangle(triangle: PackedVector3Array) -> Vector3:
|
|
var first: float = _random.randf()
|
|
var second: float = _random.randf()
|
|
if first + second > 1.0:
|
|
first = 1.0 - first
|
|
second = 1.0 - second
|
|
return triangle[0] + (triangle[1] - triangle[0]) * first + (
|
|
triangle[2] - triangle[0]
|
|
) * second
|
|
|
|
|
|
func _minimum_triangle_edge_distance(
|
|
position: Vector3,
|
|
triangle: PackedVector3Array,
|
|
) -> float:
|
|
var horizontal_position := Vector2(position.x, position.z)
|
|
return minf(
|
|
_horizontal_distance_to_segment(
|
|
horizontal_position,
|
|
Vector2(triangle[0].x, triangle[0].z),
|
|
Vector2(triangle[1].x, triangle[1].z),
|
|
),
|
|
minf(
|
|
_horizontal_distance_to_segment(
|
|
horizontal_position,
|
|
Vector2(triangle[1].x, triangle[1].z),
|
|
Vector2(triangle[2].x, triangle[2].z),
|
|
),
|
|
_horizontal_distance_to_segment(
|
|
horizontal_position,
|
|
Vector2(triangle[2].x, triangle[2].z),
|
|
Vector2(triangle[0].x, triangle[0].z),
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
static func is_puddle_surface_eligible(
|
|
triangle: PackedVector3Array,
|
|
saltwater_height: float,
|
|
) -> bool:
|
|
if triangle.size() != 3 or is_nan(saltwater_height):
|
|
return false
|
|
for vertex: Vector3 in triangle:
|
|
if vertex.y <= saltwater_height + SURFACE_CLEARANCE_ABOVE_WATER:
|
|
return false
|
|
var cross: Vector3 = (triangle[1] - triangle[0]).cross(
|
|
triangle[2] - triangle[0]
|
|
)
|
|
return (
|
|
cross.length_squared() > 0.0000001
|
|
and absf(cross.normalized().dot(Vector3.UP)) >= MINIMUM_SURFACE_UP_DOT
|
|
)
|
|
|
|
|
|
static func is_point_inside_puddle(
|
|
world_position: Vector3,
|
|
center: Vector3,
|
|
radii: Vector2,
|
|
rotation_y: float = 0.0,
|
|
) -> bool:
|
|
if radii.x <= 0.0 or radii.y <= 0.0:
|
|
return false
|
|
if absf(world_position.y - center.y) > MAXIMUM_PLAYER_HEIGHT_OFFSET:
|
|
return false
|
|
var offset := Vector2(
|
|
world_position.x - center.x,
|
|
world_position.z - center.z,
|
|
).rotated(-rotation_y)
|
|
offset = Vector2(offset.x / radii.x, offset.y / radii.y)
|
|
return offset.length_squared() <= 1.0
|
|
|
|
|
|
static func _horizontal_distance_to_segment(
|
|
point: Vector2,
|
|
start: Vector2,
|
|
end: Vector2,
|
|
) -> float:
|
|
var segment: Vector2 = end - start
|
|
var segment_length_squared: float = segment.length_squared()
|
|
if segment_length_squared <= 0.0000001:
|
|
return point.distance_to(start)
|
|
var ratio: float = clampf(
|
|
(point - start).dot(segment) / segment_length_squared,
|
|
0.0,
|
|
1.0,
|
|
)
|
|
return point.distance_to(start + segment * ratio)
|