Refactor fishing surfaces around authored water

This commit is contained in:
Alexander Sellite 2026-08-15 09:52:50 -04:00
parent 9f52620a73
commit 9afa60f6a5
11 changed files with 198 additions and 172 deletions

View file

@ -181,8 +181,11 @@ node names, pool filenames, coordinates, or water height.
Select `WaterBodies/Pond` to move or resize the pond. Its transform is the
authoritative surface position; surface size and fishing/recovery coverage are
owned together. Select `WaterBodies/Ocean` to move the surrounding water as one
feature. Its explicit visual, fishing, and recovery lobes remain local to that
root.
feature. Its fishing coverage is derived from the visible water mesh and the
terrain collision determines the shoreline. New or revised land meshes do not
require hand-authored fishing exclusions. Recovery volumes remain explicit so
they can follow the playable world bounds rather than the horizon-sized visual
ocean.
Placed-feature ownership:

View file

@ -1,10 +1,6 @@
class_name FishingPresentation
extends Node3D
const WaterSurfaceMotionType = preload(
"res://world/water_surface_motion.gd"
)
signal cast_completed
signal outcome_completed(outcome: StringName)
signal presentation_interrupted
@ -58,7 +54,6 @@ var _rod_tip: Marker3D
var _rod_neutral_rotation: Vector3
var _cast_arrival_position: Vector3
var _active_cast_arc_height: float = 0.0
var _cast_tracks_water_surface: bool = false
var _bobber_surface_position: Vector3
var _bobber_idle_elapsed: float = 0.0
var _bobber_base_scale: Vector3 = Vector3.ONE
@ -76,9 +71,6 @@ func _process(delta: float) -> void:
if _mode == VisualMode.FISHING and _bobber.visible:
_bobber_idle_elapsed += delta
_apply_bobber_idle_motion()
var bobber_position := _bobber.global_position
bobber_position.y += WaterSurfaceMotionType.get_default_height_offset()
_bobber.global_position = bobber_position
if _line_mode != LineMode.HIDDEN:
_update_line(delta)
@ -173,8 +165,6 @@ func begin_cast(
if resolved_arc_height >= 0.0
else cast_arc_height
)
_cast_tracks_water_surface = not cast_is_blocked
var cast_start: Vector3 = _rod_tip.global_position
_bobber.global_position = cast_start
_begin_rod_release()
@ -311,7 +301,6 @@ func cleanup() -> void:
_bobber.rotation = Vector3.ZERO
_cast_arrival_position = Vector3.ZERO
_active_cast_arc_height = 0.0
_cast_tracks_water_surface = false
_bobber_surface_position = Vector3.ZERO
_bobber_idle_elapsed = 0.0
set_line_mode(LineMode.HIDDEN)
@ -329,11 +318,6 @@ func _set_cast_sample(
) -> void:
var sample: Vector3 = start.lerp(target, progress)
sample.y += sin(progress * PI) * _active_cast_arc_height
if _cast_tracks_water_surface:
sample.y += (
WaterSurfaceMotionType.get_default_height_offset()
* smoothstep(0.72, 1.0, progress)
)
_bobber.global_position = sample

View file

@ -93,7 +93,7 @@ enum FishingState {
@export_range(1.0, 200.0, 1.0) var preview_ray_start_height: float = 100.0
@export_range(1.0, 400.0, 1.0) var preview_surface_ray_length: float = 240.0
@export_range(0.01, 1.0, 0.01) var preview_marker_vertical_offset: float = 0.06
@export_range(0.0, 0.5, 0.01) var water_occlusion_tolerance: float = 0.04
@export_range(0.0, 0.5, 0.01) var solid_probe_clearance: float = 0.04
@export_range(0.01, 0.5, 0.01) var solid_bobber_clearance: float = 0.13
@export_category("Withdrawal")
@ -1457,7 +1457,6 @@ func _configure_surface_resolver() -> void:
_surface_resolver.query_radius = fishable_query_radius
_surface_resolver.ray_start_height = preview_ray_start_height
_surface_resolver.ray_length = preview_surface_ray_length
_surface_resolver.water_occlusion_tolerance = water_occlusion_tolerance
func resolve_fishing_surface(
@ -1479,7 +1478,7 @@ func resolve_fishing_surface(
get_world_3d().direct_space_state,
target,
resolved_reference_y,
water_occlusion_tolerance,
solid_probe_clearance,
)

View file

@ -13,7 +13,6 @@ 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()
@ -52,9 +51,9 @@ func resolve_surface(
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
)
# The water body's authored height is the exact shoreline authority:
# submerged terrain is fishable and terrain at or above it is dry.
solid_is_above_water = solid_position.y >= water_height
if not solid_is_above_water:
sample.has_surface = true
sample.position = Vector3(

View file

@ -4,9 +4,6 @@ extends RefCounted
const FishableWaterRegionType = preload(
"res://world/fishable_water_region.gd"
)
const WaterSurfaceMotionType = preload(
"res://world/water_surface_motion.gd"
)
var has_surface: bool = false
var position: Vector3 = Vector3.ZERO
@ -22,12 +19,7 @@ func is_fishable() -> bool:
func get_marker_position(vertical_offset: float) -> Vector3:
if not has_surface:
return position
var presentation_offset := vertical_offset
if is_water_surface:
presentation_offset += (
WaterSurfaceMotionType.get_default_height_offset()
)
return position + normal.normalized() * presentation_offset
return position + normal.normalized() * vertical_offset
func get_bobber_position(solid_clearance: float) -> Vector3:

View file

@ -1,9 +1,6 @@
class_name RemoteFishingPresentation
extends Node3D
const WaterSurfaceMotionType = preload(
"res://world/water_surface_motion.gd"
)
const LINE_THICKNESS: float = 0.016
signal return_completed
@ -157,10 +154,6 @@ func _set_cast_sample(
) -> void:
_target = start.lerp(target, progress)
_target.y += sin(progress * PI) * 2.0
_target.y += (
WaterSurfaceMotionType.get_default_height_offset()
* smoothstep(0.72, 1.0, progress)
)
_bobber.global_position = _target
@ -175,10 +168,7 @@ func _on_cast_finished() -> void:
func _apply_bobber_idle_motion() -> void:
var phase: float = _bobber_idle_elapsed * 0.7 * TAU
_target = _pending_target + Vector3.UP * (
WaterSurfaceMotionType.get_default_height_offset()
+ sin(phase) * 0.035
)
_target = _pending_target + Vector3.UP * sin(phase) * 0.035
_bobber.global_position = _target
_bobber.rotation.z = deg_to_rad(4.0) * sin(phase * 0.73)

View file

@ -109,19 +109,7 @@ size = Vector2(10000, 10000)
[sub_resource type="BoxShape3D" id="BoxShape3D_umo15"]
resource_local_to_scene = true
size = Vector3(17, 2, 80)
[sub_resource type="BoxShape3D" id="BoxShape3D_e4lmg"]
resource_local_to_scene = true
size = Vector3(42, 2, 80)
[sub_resource type="BoxShape3D" id="BoxShape3D_eb3g5"]
resource_local_to_scene = true
size = Vector3(48, 2, 18)
[sub_resource type="BoxShape3D" id="BoxShape3D_wsqap"]
resource_local_to_scene = true
size = Vector3(48, 2, 18)
size = Vector3(10000, 2, 10000)
[sub_resource type="BoxShape3D" id="BoxShape3D_1fdkm"]
resource_local_to_scene = true
@ -336,18 +324,9 @@ shape = SubResource("BoxShape3D_l6210")
[node name="VisualWater" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean" index="0" unique_id=1176408937]
mesh = SubResource("PlaneMesh_6upbg")
[node name="WestShape" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/FishingRegions/OceanFishingRegion" index="0" unique_id=357406062]
[node name="Shape" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/FishingRegions/OceanFishingRegion" index="0" unique_id=357406062]
shape = SubResource("BoxShape3D_umo15")
[node name="EastShape" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/FishingRegions/OceanFishingRegion" index="1" unique_id=772224738]
shape = SubResource("BoxShape3D_e4lmg")
[node name="NorthShape" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/FishingRegions/OceanFishingRegion" index="2" unique_id=646147646]
shape = SubResource("BoxShape3D_eb3g5")
[node name="SouthShape" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/FishingRegions/OceanFishingRegion" index="3" unique_id=894083854]
shape = SubResource("BoxShape3D_wsqap")
[node name="WestShape" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/RecoveryRegions/OceanRecoveryRegion" index="0" unique_id=1183368645]
shape = SubResource("BoxShape3D_1fdkm")

View file

@ -51,6 +51,8 @@ func _validate_resolver_layers() -> void:
_add_water_region(world, Vector3(0.0, 2.0, 0.0), Vector2(8.0, 8.0), PondPool)
_add_solid_box(world, Vector3(1.0, 1.0, 1.0), Vector3(2.0, 2.25, 0.0))
_add_solid_box(world, Vector3(1.0, 1.0, 1.0), Vector3(-2.0, 6.0, 0.0))
_add_solid_box(world, Vector3(1.0, 1.0, 1.0), Vector3(0.0, 1.49, 2.0))
_add_solid_box(world, Vector3(1.0, 1.0, 1.0), Vector3(0.0, 1.5, -2.0))
_add_water_region(world, Vector3(10.0, 5.0, 0.0), Vector2(6.0, 6.0), PondPool)
_add_water_region(world, Vector3(20.0, 3.0, 0.0), Vector2(6.0, 6.0), null)
await physics_frame
@ -73,6 +75,21 @@ func _validate_resolver_layers() -> void:
)
assert(water_under_overhang.is_fishable())
assert(is_equal_approx(water_under_overhang.position.y, 2.0))
var shallow_water: FishingSurfaceSampleType = resolver.resolve_surface(
space_state,
Vector3(0.0, 2.0, 2.0),
4.0,
0.04,
)
assert(shallow_water.is_fishable())
var exact_shore_contact: FishingSurfaceSampleType = resolver.resolve_surface(
space_state,
Vector3(0.0, 2.0, -2.0),
4.0,
0.04,
)
assert(exact_shore_contact.has_surface)
assert(not exact_shore_contact.is_fishable())
var covered_water: FishingSurfaceSampleType = resolver.resolve_surface(
space_state,
@ -158,38 +175,44 @@ func _validate_portable_water_body() -> void:
water_body.queue_free()
await process_frame
var mesh_derived_body := WaterBodyScene.instantiate() as WaterBodyAuthoring
mesh_derived_body.position = Vector3(-4.0, 3.0, 6.0)
mesh_derived_body.derive_coverage_from_visual_mesh = true
mesh_derived_body.fishing_depth = 3.0
mesh_derived_body.fish_pool = PondPool
var derived_visual := mesh_derived_body.get_node(
"VisualWater"
) as MeshInstance3D
var derived_mesh := derived_visual.mesh as PlaneMesh
derived_mesh.size = Vector2(18.0, 11.0)
root.add_child(mesh_derived_body)
await process_frame
var derived_shape_node := mesh_derived_body.get_node(
"FishingRegion/Shape"
) as CollisionShape3D
var derived_shape := derived_shape_node.shape as BoxShape3D
assert(derived_shape.size.is_equal_approx(Vector3(18.0, 3.0, 11.0)))
assert(derived_shape_node.position.is_equal_approx(Vector3(0.0, -1.5, 0.0)))
assert(derived_mesh.size.is_equal_approx(Vector2(18.0, 11.0)))
mesh_derived_body.queue_free()
await process_frame
func _validate_starter_region_surfaces() -> void:
var region := StarterRegionScene.instantiate() as Node3D
root.add_child(region)
var ocean_body := region.get_node(
"WaterBodies/Ocean"
) as WaterBodyAuthoring
assert(ocean_body != null)
assert(ocean_body.derive_coverage_from_visual_mesh)
assert(not ocean_body.manage_recovery_coverage)
var ocean_region := region.get_node(
"WaterBodies/Ocean/FishingRegions/OceanFishingRegion"
) as FishableWaterRegionType
var minimum_x: float = INF
var maximum_x: float = -INF
var minimum_z: float = INF
var maximum_z: float = -INF
for child: Node in ocean_region.get_children():
var shape_node := child as CollisionShape3D
assert(shape_node != null and shape_node.shape is BoxShape3D)
var box := shape_node.shape as BoxShape3D
assert(is_equal_approx(box.size.y, 2.0))
minimum_x = minf(minimum_x, shape_node.position.x - box.size.x * 0.5)
maximum_x = maxf(maximum_x, shape_node.position.x + box.size.x * 0.5)
minimum_z = minf(minimum_z, shape_node.position.z - box.size.z * 0.5)
maximum_z = maxf(maximum_z, shape_node.position.z + box.size.z * 0.5)
var ocean_footprint := Vector2(
maximum_x - minimum_x,
maximum_z - minimum_z,
)
assert(is_equal_approx(
ocean_footprint.x * ocean_footprint.y,
12840.0,
))
assert(is_equal_approx(
ocean_footprint.x / 107.0,
ocean_footprint.y / 80.0,
))
assert(ocean_region.get_child_count() == 1)
var fishing_spot := FishingSpotScene.instantiate() as FishingSpotType
root.add_child(fishing_spot)
await physics_frame
@ -207,6 +230,27 @@ func _validate_starter_region_surfaces() -> void:
)
assert(ocean.is_fishable())
assert(is_equal_approx(ocean.position.y, -0.45))
var ocean_visual := region.get_node(
"WaterBodies/Ocean/VisualWater"
) as MeshInstance3D
assert(ocean_visual != null)
assert(is_equal_approx(ocean_visual.global_position.y, ocean.position.y))
assert(not ocean_visual.is_processing())
var ocean_shapes := region.get_node(
"WaterBodies/Ocean/FishingRegions/OceanFishingRegion"
).get_children()
assert(ocean_shapes.size() == 1)
var ocean_shape := ocean_shapes[0] as CollisionShape3D
assert(ocean_shape != null)
var ocean_box := ocean_shape.shape as BoxShape3D
assert(ocean_box != null)
var ocean_mesh := ocean_visual.mesh as PlaneMesh
assert(ocean_mesh != null)
assert(ocean_box.size.is_equal_approx(Vector3(
ocean_mesh.size.x,
2.0,
ocean_mesh.size.y,
)))
var expanded_ocean: FishingSurfaceSampleType = (
fishing_spot.resolve_fishing_surface(
Vector3(72.0, -0.45, 0.0),

View file

@ -515,9 +515,10 @@ void fragment() {
water_alpha + shoreline_foam * shoreline_opacity_boost,
0.98
);
// Preserve most of the water layer at exact terrain contact. Fully fading
// it exposed the sand texture as a broken tan seam along the water plane.
water_alpha *= mix(0.82, 1.0, contact_stability);
// The authored water-body height is also the casting authority. Fade the
// visual layer completely at exact terrain contact so dry land cannot look
// fishable while the stable sub-surface water remains visible.
water_alpha *= contact_stability;
float surface_mottle = value_noise(
world_position.xz * surface_mottle_scale

View file

@ -12,7 +12,6 @@
[ext_resource type="Material" path="res://world/materials/stylized_water.tres" id="13_water"]
[ext_resource type="ArrayMesh" path="res://world/generated/shorelines/starter_ocean_shoreline.tres" id="17_ocean_shoreline"]
[ext_resource type="Material" path="res://world/materials/stylized_water_fresh.tres" id="19_fresh_water"]
[ext_resource type="Script" path="res://world/water_surface_motion.gd" id="20_surface_motion"]
[ext_resource type="Script" path="res://world/water/shoreline_ribbon_baker.gd" id="21_shoreline_baker"]
[ext_resource type="Script" path="res://world/water/shoreline_ribbon_config.gd" id="22_shoreline_config"]
@ -35,25 +34,9 @@ size = Vector3(16.1, 4.8, 12.3675)
resource_local_to_scene = true
size = Vector2(10000, 10000)
[sub_resource type="BoxShape3D" id="OceanWestShape"]
[sub_resource type="BoxShape3D" id="OceanFishingShape"]
resource_local_to_scene = true
size = Vector3(20.820662814, 2, 97.979589711)
[sub_resource type="BoxShape3D" id="OceanEastShape"]
resource_local_to_scene = true
size = Vector3(51.439284598, 2, 97.979589711)
[sub_resource type="BoxShape3D" id="OceanNorthShape"]
resource_local_to_scene = true
size = Vector3(58.787753827, 2, 22.045407685)
[sub_resource type="BoxShape3D" id="OceanSouthShape"]
resource_local_to_scene = true
size = Vector3(58.787753827, 2, 22.045407685)
[sub_resource type="BoxShape3D" id="OceanCenterShape"]
resource_local_to_scene = true
size = Vector3(58.787753827, 2, 53.888774341)
size = Vector3(10000, 2, 10000)
[sub_resource type="BoxShape3D" id="OceanWestRecoveryShape"]
resource_local_to_scene = true
@ -161,11 +144,20 @@ shape = SubResource("PondRecoveryShape")
[node name="Ocean" type="Node3D" parent="WaterBodies" unique_id=552655799]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12, -0.45, 0)
script = ExtResource("10_water_body")
derive_coverage_from_visual_mesh = true
water_material = ExtResource("13_water")
fishing_depth = 2.0
fish_pool = ExtResource("4_pool")
water_type = 1
location_tags = Array[StringName]([&"coast", &"ocean"])
manage_recovery_coverage = false
fishing_shape_path = NodePath("FishingRegions/OceanFishingRegion/Shape")
fishing_region_path = NodePath("FishingRegions/OceanFishingRegion")
[node name="VisualWater" type="MeshInstance3D" parent="WaterBodies/Ocean" unique_id=1176408937]
material_override = ExtResource("13_water")
mesh = SubResource("OceanWaterMesh")
script = ExtResource("20_surface_motion")
[node name="FishingRegions" type="Node3D" parent="WaterBodies/Ocean" unique_id=205598473]
@ -179,25 +171,9 @@ water_type = 1
fish_pool = ExtResource("4_pool")
surface_height_mode = 1
[node name="WestShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=357406062]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -55.613519213, -1, 0)
shape = SubResource("OceanWestShape")
[node name="EastShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=772224738]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 39.30420832, -1, 0)
shape = SubResource("OceanEastShape")
[node name="NorthShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=646147646]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15.809310892, -1, 37.967091013)
shape = SubResource("OceanNorthShape")
[node name="SouthShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=894083854]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15.809310892, -1, -37.967091013)
shape = SubResource("OceanSouthShape")
[node name="CenterShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion"]
position = Vector3(-15.809310892, -1, 0)
shape = SubResource("OceanCenterShape")
[node name="Shape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=357406062]
position = Vector3(0, -1, 0)
shape = SubResource("OceanFishingShape")
[node name="RecoveryRegions" type="Node3D" parent="WaterBodies/Ocean" unique_id=463706703]

View file

@ -14,6 +14,12 @@ var surface_size: Vector2 = Vector2(10.0, 10.0):
set(value):
surface_size = Vector2(maxf(value.x, 0.1), maxf(value.y, 0.1))
_sync_owned_nodes()
## Derive fishing coverage from VisualWater's mesh bounds instead of resizing
## the mesh from Surface Size. Use this for imported or shared water surfaces.
@export var derive_coverage_from_visual_mesh: bool = false:
set(value):
derive_coverage_from_visual_mesh = value
_sync_owned_nodes()
## Material applied to the visible water surface.
@export var water_material: Material:
set(value):
@ -44,6 +50,11 @@ var fishing_depth: float = 4.0:
_sync_owned_nodes()
@export_group("Recovery Coverage")
## Keep recovery coverage independent when a water body uses custom volumes.
@export var manage_recovery_coverage: bool = true:
set(value):
manage_recovery_coverage = value
_sync_owned_nodes()
@export_range(0.1, 20.0, 0.1, "or_greater", "suffix:m")
var recovery_depth: float = 4.8:
set(value):
@ -75,9 +86,10 @@ func _sync_owned_nodes() -> void:
var visual_water := get_node_or_null(visual_water_path) as MeshInstance3D
if visual_water != null:
var plane_mesh := visual_water.mesh as PlaneMesh
if plane_mesh != null:
if plane_mesh != null and not derive_coverage_from_visual_mesh:
plane_mesh.size = surface_size
visual_water.material_override = water_material
var surface_footprint: Rect2 = _resolve_surface_footprint(visual_water)
var fishing_shape_node := (
get_node_or_null(fishing_shape_path) as CollisionShape3D
@ -99,34 +111,73 @@ func _sync_owned_nodes() -> void:
# The authored root is the one water-surface height. Keep the fishable
# volume entirely below that plane so visible water and interaction
# can be moved together without a second height to tune.
fishing_shape_node.position.y = -fishing_depth * 0.5
fishing_shape_node.position = Vector3(
surface_footprint.get_center().x,
-fishing_depth * 0.5,
surface_footprint.get_center().y,
)
fishing_shape.size = Vector3(
surface_size.x,
surface_footprint.size.x,
fishing_depth,
surface_size.y
surface_footprint.size.y,
)
var recovery_region := (
get_node_or_null(recovery_region_path) as Area3D
)
if recovery_region != null:
recovery_region.position.y = -recovery_depth * 0.5
var recovery_shape_node := (
get_node_or_null(recovery_shape_path) as CollisionShape3D
)
if recovery_shape_node != null:
var recovery_shape := recovery_shape_node.shape as BoxShape3D
if recovery_shape != null:
recovery_shape.size = Vector3(
surface_size.x,
recovery_depth,
surface_size.y
if manage_recovery_coverage:
var recovery_region := (
get_node_or_null(recovery_region_path) as Area3D
)
if recovery_region != null:
recovery_region.position = Vector3(
surface_footprint.get_center().x,
-recovery_depth * 0.5,
surface_footprint.get_center().y,
)
var recovery_shape_node := (
get_node_or_null(recovery_shape_path) as CollisionShape3D
)
if recovery_shape_node != null:
var recovery_shape := recovery_shape_node.shape as BoxShape3D
if recovery_shape != null:
recovery_shape.size = Vector3(
surface_footprint.size.x,
recovery_depth,
surface_footprint.size.y,
)
if Engine.is_editor_hint():
update_configuration_warnings()
func _resolve_surface_footprint(visual_water: MeshInstance3D) -> Rect2:
var authored_footprint := Rect2(-surface_size * 0.5, surface_size)
if (
not derive_coverage_from_visual_mesh
or visual_water == null
or visual_water.mesh == null
):
return authored_footprint
var mesh_bounds: AABB = visual_water.mesh.get_aabb()
if mesh_bounds.size.x <= 0.0 or mesh_bounds.size.z <= 0.0:
return authored_footprint
var visual_to_body: Transform3D = (
global_transform.affine_inverse() * visual_water.global_transform
)
var minimum := Vector2(INF, INF)
var maximum := Vector2(-INF, -INF)
for corner_index: int in 8:
var corner := mesh_bounds.position + Vector3(
mesh_bounds.size.x if (corner_index & 1) != 0 else 0.0,
mesh_bounds.size.y if (corner_index & 2) != 0 else 0.0,
mesh_bounds.size.z if (corner_index & 4) != 0 else 0.0,
)
var body_corner: Vector3 = visual_to_body * corner
minimum.x = minf(minimum.x, body_corner.x)
minimum.y = minf(minimum.y, body_corner.z)
maximum.x = maxf(maximum.x, body_corner.x)
maximum.y = maxf(maximum.y, body_corner.z)
return Rect2(minimum, maximum - minimum)
func _get_configuration_warnings() -> PackedStringArray:
var warnings := PackedStringArray()
if not scale.is_equal_approx(Vector3.ONE):
@ -134,28 +185,36 @@ func _get_configuration_warnings() -> PackedStringArray:
"Keep root scale at 1; resize water with Surface Size."
)
var visual_water := get_node_or_null(visual_water_path) as MeshInstance3D
if visual_water == null or not visual_water.mesh is PlaneMesh:
warnings.append("VisualWater must provide a PlaneMesh.")
if visual_water == null or visual_water.mesh == null:
warnings.append("VisualWater must provide a mesh.")
elif (
not derive_coverage_from_visual_mesh
and not visual_water.mesh is PlaneMesh
):
warnings.append(
"VisualWater must provide a PlaneMesh when coverage is authored by size."
)
var fishing_shape := get_node_or_null(fishing_shape_path) as CollisionShape3D
if fishing_shape == null or not fishing_shape.shape is BoxShape3D:
warnings.append("FishingRegion must provide a BoxShape3D.")
var recovery_region := (
get_node_or_null(recovery_region_path) as PlayerWaterTrigger
)
if recovery_region == null:
warnings.append("RecoveryRegion must use PlayerWaterTrigger.")
elif (
recovery_region.surface_height_mode
!= PlayerWaterTrigger.SurfaceHeightMode.PARENT_GLOBAL_Y
):
warnings.append(
"RecoveryRegion must derive surface height from its parent."
if manage_recovery_coverage:
var recovery_region := (
get_node_or_null(recovery_region_path) as PlayerWaterTrigger
)
var recovery_shape := (
get_node_or_null(recovery_shape_path) as CollisionShape3D
)
if recovery_shape == null or not recovery_shape.shape is BoxShape3D:
warnings.append("RecoveryRegion must provide a BoxShape3D.")
if recovery_region == null:
warnings.append("RecoveryRegion must use PlayerWaterTrigger.")
elif (
recovery_region.surface_height_mode
!= PlayerWaterTrigger.SurfaceHeightMode.PARENT_GLOBAL_Y
):
warnings.append(
"RecoveryRegion must derive surface height from its parent."
)
var recovery_shape := (
get_node_or_null(recovery_shape_path) as CollisionShape3D
)
if recovery_shape == null or not recovery_shape.shape is BoxShape3D:
warnings.append("RecoveryRegion must provide a BoxShape3D.")
var fishing_region := get_node_or_null(
fishing_region_path
) as FishableWaterRegion