Refactor fishing surfaces around authored water
This commit is contained in:
parent
9f52620a73
commit
9afa60f6a5
11 changed files with 198 additions and 172 deletions
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue