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 Select `WaterBodies/Pond` to move or resize the pond. Its transform is the
authoritative surface position; surface size and fishing/recovery coverage are authoritative surface position; surface size and fishing/recovery coverage are
owned together. Select `WaterBodies/Ocean` to move the surrounding water as one owned together. Select `WaterBodies/Ocean` to move the surrounding water as one
feature. Its explicit visual, fishing, and recovery lobes remain local to that feature. Its fishing coverage is derived from the visible water mesh and the
root. 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: Placed-feature ownership:

View file

@ -1,10 +1,6 @@
class_name FishingPresentation class_name FishingPresentation
extends Node3D extends Node3D
const WaterSurfaceMotionType = preload(
"res://world/water_surface_motion.gd"
)
signal cast_completed signal cast_completed
signal outcome_completed(outcome: StringName) signal outcome_completed(outcome: StringName)
signal presentation_interrupted signal presentation_interrupted
@ -58,7 +54,6 @@ var _rod_tip: Marker3D
var _rod_neutral_rotation: Vector3 var _rod_neutral_rotation: Vector3
var _cast_arrival_position: Vector3 var _cast_arrival_position: Vector3
var _active_cast_arc_height: float = 0.0 var _active_cast_arc_height: float = 0.0
var _cast_tracks_water_surface: bool = false
var _bobber_surface_position: Vector3 var _bobber_surface_position: Vector3
var _bobber_idle_elapsed: float = 0.0 var _bobber_idle_elapsed: float = 0.0
var _bobber_base_scale: Vector3 = Vector3.ONE var _bobber_base_scale: Vector3 = Vector3.ONE
@ -76,9 +71,6 @@ func _process(delta: float) -> void:
if _mode == VisualMode.FISHING and _bobber.visible: if _mode == VisualMode.FISHING and _bobber.visible:
_bobber_idle_elapsed += delta _bobber_idle_elapsed += delta
_apply_bobber_idle_motion() _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: if _line_mode != LineMode.HIDDEN:
_update_line(delta) _update_line(delta)
@ -173,8 +165,6 @@ func begin_cast(
if resolved_arc_height >= 0.0 if resolved_arc_height >= 0.0
else cast_arc_height else cast_arc_height
) )
_cast_tracks_water_surface = not cast_is_blocked
var cast_start: Vector3 = _rod_tip.global_position var cast_start: Vector3 = _rod_tip.global_position
_bobber.global_position = cast_start _bobber.global_position = cast_start
_begin_rod_release() _begin_rod_release()
@ -311,7 +301,6 @@ func cleanup() -> void:
_bobber.rotation = Vector3.ZERO _bobber.rotation = Vector3.ZERO
_cast_arrival_position = Vector3.ZERO _cast_arrival_position = Vector3.ZERO
_active_cast_arc_height = 0.0 _active_cast_arc_height = 0.0
_cast_tracks_water_surface = false
_bobber_surface_position = Vector3.ZERO _bobber_surface_position = Vector3.ZERO
_bobber_idle_elapsed = 0.0 _bobber_idle_elapsed = 0.0
set_line_mode(LineMode.HIDDEN) set_line_mode(LineMode.HIDDEN)
@ -329,11 +318,6 @@ func _set_cast_sample(
) -> void: ) -> void:
var sample: Vector3 = start.lerp(target, progress) var sample: Vector3 = start.lerp(target, progress)
sample.y += sin(progress * PI) * _active_cast_arc_height 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 _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, 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(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.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_range(0.01, 0.5, 0.01) var solid_bobber_clearance: float = 0.13
@export_category("Withdrawal") @export_category("Withdrawal")
@ -1457,7 +1457,6 @@ func _configure_surface_resolver() -> void:
_surface_resolver.query_radius = fishable_query_radius _surface_resolver.query_radius = fishable_query_radius
_surface_resolver.ray_start_height = preview_ray_start_height _surface_resolver.ray_start_height = preview_ray_start_height
_surface_resolver.ray_length = preview_surface_ray_length _surface_resolver.ray_length = preview_surface_ray_length
_surface_resolver.water_occlusion_tolerance = water_occlusion_tolerance
func resolve_fishing_surface( func resolve_fishing_surface(
@ -1479,7 +1478,7 @@ func resolve_fishing_surface(
get_world_3d().direct_space_state, get_world_3d().direct_space_state,
target, target,
resolved_reference_y, 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 query_radius: float = 0.08
var ray_start_height: float = 100.0 var ray_start_height: float = 100.0
var ray_length: float = 240.0 var ray_length: float = 240.0
var water_occlusion_tolerance: float = 0.04
var arc_sample_spacing: float = 0.3 var arc_sample_spacing: float = 0.3
var _water_query_shape: CylinderShape3D = CylinderShape3D.new() var _water_query_shape: CylinderShape3D = CylinderShape3D.new()
@ -52,9 +51,9 @@ func resolve_surface(
var solid_is_above_water: bool = false var solid_is_above_water: bool = false
if not solid_hit.is_empty(): if not solid_hit.is_empty():
var solid_position: Vector3 = solid_hit["position"] var solid_position: Vector3 = solid_hit["position"]
solid_is_above_water = ( # The water body's authored height is the exact shoreline authority:
solid_position.y >= water_height - water_occlusion_tolerance # 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: if not solid_is_above_water:
sample.has_surface = true sample.has_surface = true
sample.position = Vector3( sample.position = Vector3(

View file

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

View file

@ -1,9 +1,6 @@
class_name RemoteFishingPresentation class_name RemoteFishingPresentation
extends Node3D extends Node3D
const WaterSurfaceMotionType = preload(
"res://world/water_surface_motion.gd"
)
const LINE_THICKNESS: float = 0.016 const LINE_THICKNESS: float = 0.016
signal return_completed signal return_completed
@ -157,10 +154,6 @@ func _set_cast_sample(
) -> void: ) -> void:
_target = start.lerp(target, progress) _target = start.lerp(target, progress)
_target.y += sin(progress * PI) * 2.0 _target.y += sin(progress * PI) * 2.0
_target.y += (
WaterSurfaceMotionType.get_default_height_offset()
* smoothstep(0.72, 1.0, progress)
)
_bobber.global_position = _target _bobber.global_position = _target
@ -175,10 +168,7 @@ func _on_cast_finished() -> void:
func _apply_bobber_idle_motion() -> void: func _apply_bobber_idle_motion() -> void:
var phase: float = _bobber_idle_elapsed * 0.7 * TAU var phase: float = _bobber_idle_elapsed * 0.7 * TAU
_target = _pending_target + Vector3.UP * ( _target = _pending_target + Vector3.UP * sin(phase) * 0.035
WaterSurfaceMotionType.get_default_height_offset()
+ sin(phase) * 0.035
)
_bobber.global_position = _target _bobber.global_position = _target
_bobber.rotation.z = deg_to_rad(4.0) * sin(phase * 0.73) _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"] [sub_resource type="BoxShape3D" id="BoxShape3D_umo15"]
resource_local_to_scene = true resource_local_to_scene = true
size = Vector3(17, 2, 80) size = Vector3(10000, 2, 10000)
[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)
[sub_resource type="BoxShape3D" id="BoxShape3D_1fdkm"] [sub_resource type="BoxShape3D" id="BoxShape3D_1fdkm"]
resource_local_to_scene = true 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] [node name="VisualWater" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean" index="0" unique_id=1176408937]
mesh = SubResource("PlaneMesh_6upbg") 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") 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] [node name="WestShape" parent="TestWorld/Regions/StarterIslandRegion/WaterBodies/Ocean/RecoveryRegions/OceanRecoveryRegion" index="0" unique_id=1183368645]
shape = SubResource("BoxShape3D_1fdkm") 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_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, 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(-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(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) _add_water_region(world, Vector3(20.0, 3.0, 0.0), Vector2(6.0, 6.0), null)
await physics_frame await physics_frame
@ -73,6 +75,21 @@ func _validate_resolver_layers() -> void:
) )
assert(water_under_overhang.is_fishable()) assert(water_under_overhang.is_fishable())
assert(is_equal_approx(water_under_overhang.position.y, 2.0)) 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( var covered_water: FishingSurfaceSampleType = resolver.resolve_surface(
space_state, space_state,
@ -158,38 +175,44 @@ func _validate_portable_water_body() -> void:
water_body.queue_free() water_body.queue_free()
await process_frame 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: func _validate_starter_region_surfaces() -> void:
var region := StarterRegionScene.instantiate() as Node3D var region := StarterRegionScene.instantiate() as Node3D
root.add_child(region) 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( var ocean_region := region.get_node(
"WaterBodies/Ocean/FishingRegions/OceanFishingRegion" "WaterBodies/Ocean/FishingRegions/OceanFishingRegion"
) as FishableWaterRegionType ) as FishableWaterRegionType
var minimum_x: float = INF assert(ocean_region.get_child_count() == 1)
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,
))
var fishing_spot := FishingSpotScene.instantiate() as FishingSpotType var fishing_spot := FishingSpotScene.instantiate() as FishingSpotType
root.add_child(fishing_spot) root.add_child(fishing_spot)
await physics_frame await physics_frame
@ -207,6 +230,27 @@ func _validate_starter_region_surfaces() -> void:
) )
assert(ocean.is_fishable()) assert(ocean.is_fishable())
assert(is_equal_approx(ocean.position.y, -0.45)) 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 = ( var expanded_ocean: FishingSurfaceSampleType = (
fishing_spot.resolve_fishing_surface( fishing_spot.resolve_fishing_surface(
Vector3(72.0, -0.45, 0.0), Vector3(72.0, -0.45, 0.0),

View file

@ -515,9 +515,10 @@ void fragment() {
water_alpha + shoreline_foam * shoreline_opacity_boost, water_alpha + shoreline_foam * shoreline_opacity_boost,
0.98 0.98
); );
// Preserve most of the water layer at exact terrain contact. Fully fading // The authored water-body height is also the casting authority. Fade the
// it exposed the sand texture as a broken tan seam along the water plane. // visual layer completely at exact terrain contact so dry land cannot look
water_alpha *= mix(0.82, 1.0, contact_stability); // fishable while the stable sub-surface water remains visible.
water_alpha *= contact_stability;
float surface_mottle = value_noise( float surface_mottle = value_noise(
world_position.xz * surface_mottle_scale 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="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="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="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_baker.gd" id="21_shoreline_baker"]
[ext_resource type="Script" path="res://world/water/shoreline_ribbon_config.gd" id="22_shoreline_config"] [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 resource_local_to_scene = true
size = Vector2(10000, 10000) size = Vector2(10000, 10000)
[sub_resource type="BoxShape3D" id="OceanWestShape"] [sub_resource type="BoxShape3D" id="OceanFishingShape"]
resource_local_to_scene = true resource_local_to_scene = true
size = Vector3(20.820662814, 2, 97.979589711) size = Vector3(10000, 2, 10000)
[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)
[sub_resource type="BoxShape3D" id="OceanWestRecoveryShape"] [sub_resource type="BoxShape3D" id="OceanWestRecoveryShape"]
resource_local_to_scene = true resource_local_to_scene = true
@ -161,11 +144,20 @@ shape = SubResource("PondRecoveryShape")
[node name="Ocean" type="Node3D" parent="WaterBodies" unique_id=552655799] [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) 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] [node name="VisualWater" type="MeshInstance3D" parent="WaterBodies/Ocean" unique_id=1176408937]
material_override = ExtResource("13_water") material_override = ExtResource("13_water")
mesh = SubResource("OceanWaterMesh") mesh = SubResource("OceanWaterMesh")
script = ExtResource("20_surface_motion")
[node name="FishingRegions" type="Node3D" parent="WaterBodies/Ocean" unique_id=205598473] [node name="FishingRegions" type="Node3D" parent="WaterBodies/Ocean" unique_id=205598473]
@ -179,25 +171,9 @@ water_type = 1
fish_pool = ExtResource("4_pool") fish_pool = ExtResource("4_pool")
surface_height_mode = 1 surface_height_mode = 1
[node name="WestShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=357406062] [node name="Shape" 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) position = Vector3(0, -1, 0)
shape = SubResource("OceanWestShape") shape = SubResource("OceanFishingShape")
[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="RecoveryRegions" type="Node3D" parent="WaterBodies/Ocean" unique_id=463706703] [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): set(value):
surface_size = Vector2(maxf(value.x, 0.1), maxf(value.y, 0.1)) surface_size = Vector2(maxf(value.x, 0.1), maxf(value.y, 0.1))
_sync_owned_nodes() _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. ## Material applied to the visible water surface.
@export var water_material: Material: @export var water_material: Material:
set(value): set(value):
@ -44,6 +50,11 @@ var fishing_depth: float = 4.0:
_sync_owned_nodes() _sync_owned_nodes()
@export_group("Recovery Coverage") @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") @export_range(0.1, 20.0, 0.1, "or_greater", "suffix:m")
var recovery_depth: float = 4.8: var recovery_depth: float = 4.8:
set(value): set(value):
@ -75,9 +86,10 @@ func _sync_owned_nodes() -> void:
var visual_water := get_node_or_null(visual_water_path) as MeshInstance3D var visual_water := get_node_or_null(visual_water_path) as MeshInstance3D
if visual_water != null: if visual_water != null:
var plane_mesh := visual_water.mesh as PlaneMesh 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 plane_mesh.size = surface_size
visual_water.material_override = water_material visual_water.material_override = water_material
var surface_footprint: Rect2 = _resolve_surface_footprint(visual_water)
var fishing_shape_node := ( var fishing_shape_node := (
get_node_or_null(fishing_shape_path) as CollisionShape3D 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 # The authored root is the one water-surface height. Keep the fishable
# volume entirely below that plane so visible water and interaction # volume entirely below that plane so visible water and interaction
# can be moved together without a second height to tune. # 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( fishing_shape.size = Vector3(
surface_size.x, surface_footprint.size.x,
fishing_depth, fishing_depth,
surface_size.y surface_footprint.size.y,
) )
var recovery_region := ( if manage_recovery_coverage:
get_node_or_null(recovery_region_path) as Area3D var recovery_region := (
) get_node_or_null(recovery_region_path) as Area3D
if recovery_region != null: )
recovery_region.position.y = -recovery_depth * 0.5 if recovery_region != null:
var recovery_shape_node := ( recovery_region.position = Vector3(
get_node_or_null(recovery_shape_path) as CollisionShape3D surface_footprint.get_center().x,
) -recovery_depth * 0.5,
if recovery_shape_node != null: surface_footprint.get_center().y,
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
) )
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(): if Engine.is_editor_hint():
update_configuration_warnings() 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: func _get_configuration_warnings() -> PackedStringArray:
var warnings := PackedStringArray() var warnings := PackedStringArray()
if not scale.is_equal_approx(Vector3.ONE): 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." "Keep root scale at 1; resize water with Surface Size."
) )
var visual_water := get_node_or_null(visual_water_path) as MeshInstance3D var visual_water := get_node_or_null(visual_water_path) as MeshInstance3D
if visual_water == null or not visual_water.mesh is PlaneMesh: if visual_water == null or visual_water.mesh == null:
warnings.append("VisualWater must provide a PlaneMesh.") 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 var fishing_shape := get_node_or_null(fishing_shape_path) as CollisionShape3D
if fishing_shape == null or not fishing_shape.shape is BoxShape3D: if fishing_shape == null or not fishing_shape.shape is BoxShape3D:
warnings.append("FishingRegion must provide a BoxShape3D.") warnings.append("FishingRegion must provide a BoxShape3D.")
var recovery_region := ( if manage_recovery_coverage:
get_node_or_null(recovery_region_path) as PlayerWaterTrigger 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."
) )
var recovery_shape := ( if recovery_region == null:
get_node_or_null(recovery_shape_path) as CollisionShape3D warnings.append("RecoveryRegion must use PlayerWaterTrigger.")
) elif (
if recovery_shape == null or not recovery_shape.shape is BoxShape3D: recovery_region.surface_height_mode
warnings.append("RecoveryRegion must provide a BoxShape3D.") != 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( var fishing_region := get_node_or_null(
fishing_region_path fishing_region_path
) as FishableWaterRegion ) as FishableWaterRegion