feat: add rain puddles and stabilize shoreline water

This commit is contained in:
Alexander Sellite 2026-09-02 01:27:20 -04:00
parent eeef99793d
commit 3a61056ce5
49 changed files with 1033 additions and 1852 deletions

View file

@ -505,7 +505,7 @@ func _run() -> void:
"trailblazer",
))
var remote_transform: Transform3D = player.global_transform
remote_transform.origin += Vector3(0.8, 0.0, 0.0)
remote_transform.origin += Vector3(20.0, 0.0, 0.0)
var remote_avatar := spawn_service.spawn_remote_player(
NAMEPLATE_TEST_PEER_ID,
remote_transform,
@ -534,6 +534,29 @@ func _run() -> void:
assert(remote_plate.visible)
assert((remote_nameplate.get("name") as Label).text == "remote stray")
assert((remote_nameplate.get("title") as Label).text == "trailblazer")
chat_ui.call(
"_show_speech_bubble",
NAMEPLATE_TEST_PEER_ID,
"distant hello",
"",
)
await process_frame
chat_ui.call("_update_speech")
var remote_speech_entries: Dictionary = chat_ui.get("_speech")
var remote_speech: Dictionary = remote_speech_entries.get(
NAMEPLATE_TEST_PEER_ID,
{},
)
var remote_speech_bubble := remote_speech.get("bubble") as PanelContainer
assert(remote_speech_bubble != null and remote_speech_bubble.visible)
assert(is_equal_approx(
remote_speech_bubble.scale.x,
ChatUI.speech_scale_for_distance(
remote_avatar.global_position.distance_to(player.global_position)
),
))
assert(remote_speech_bubble.scale.x < 1.0)
chat_ui.call("_on_peer_removed", NAMEPLATE_TEST_PEER_ID)
spawn_service.remove_peer(NAMEPLATE_TEST_PEER_ID)
registry.remove_peer(NAMEPLATE_TEST_PEER_ID)
chat_ui.call("_update_nameplates")

View file

@ -524,21 +524,16 @@ func _validate_starter_water_bodies() -> void:
assert(ocean.water_type == WaterType.Type.SALT_WATER)
assert(pond.fish_pool == PondPool)
assert(ocean.fish_pool == OceanPool)
assert(region.get_node_or_null("ShorelineRibbons/Pond") == null)
var shoreline_geometry := (
region.get_node_or_null("ShorelineRibbons/Ocean") as MeshInstance3D
)
assert(shoreline_geometry != null)
assert(not shoreline_geometry.visible)
assert(shoreline_geometry.material_override == null)
var pond_material := (
region.get_node("WaterBodies/Pond/VisualWater") as MeshInstance3D
).material_override as ShaderMaterial
var ocean_material := (
region.get_node("WaterBodies/Ocean/VisualWater") as MeshInstance3D
).material_override as ShaderMaterial
assert(not bool(pond_material.get_shader_parameter("tide_effect_enabled")))
assert(bool(ocean_material.get_shader_parameter("tide_effect_enabled")))
assert(pond_material != null)
assert(ocean_material != null)
assert(float(ocean_material.get_shader_parameter("shoreline_foam_depth")) > 0.0)
assert(float(ocean_material.get_shader_parameter("shoreline_foam_strength")) > 0.0)
region.free()

View file

@ -131,9 +131,6 @@ func _validate_normal_profile(main: Node) -> void:
assert((main.get_node("%TitleMusic") as AudioStreamPlayer).stream != null)
assert((main.get_node("%NewGameMusic") as AudioStreamPlayer).stream != null)
assert((main.get_node("%DuskMusic") as AudioStreamPlayer).stream != null)
assert(
(main.get_node("%WavesAudio") as AudioStreamPlayer).stream != null
)
assert(
(main.get_node("RainAmbience") as AudioStreamPlayer).stream != null
)

View file

@ -40,6 +40,15 @@ func _validate_voice_distance_attenuation() -> void:
ChatUI.animalese_volume_offset_db_for_distance(24.0),
-80.0,
))
assert(is_equal_approx(ChatUI.speech_scale_for_distance(0.0), 1.0))
assert(is_equal_approx(ChatUI.speech_scale_for_distance(4.0), 1.0))
var middle_distance_scale := ChatUI.speech_scale_for_distance(14.0)
assert(middle_distance_scale < 1.0)
assert(middle_distance_scale > ChatUI.SPEECH_MINIMUM_SCALE)
assert(is_equal_approx(
ChatUI.speech_scale_for_distance(24.0),
ChatUI.SPEECH_MINIMUM_SCALE,
))
func _run_host() -> void:

View file

@ -0,0 +1,84 @@
extends SceneTree
const MainScene: PackedScene = preload("res://main/main.tscn")
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
root.size = Vector2i(1280, 720)
var main: Node = MainScene.instantiate()
root.add_child(main)
for _frame: int in 4:
await process_frame
if not bool(main.get("_application_initialized")):
main.call("_activate_selected_data_path", "", true)
for _frame: int in 8:
await process_frame
assert(bool(main.get("_application_initialized")))
assert(bool(main.call(
"_apply_world",
WorldLayout.GENERATED,
PlayerSaveManager.DEFAULT_WORLD_SEED,
true,
)))
var weather := main.get_node(
"%WorldWeatherService"
) as WorldWeatherService
assert(weather != null)
weather.apply_authoritative_snapshot(
WorldWeatherService.Weather.RAINY,
60.0,
)
main.call("_set_gameplay_active", true)
await physics_frame
await physics_frame
var puddles := main.get_node(
"RainPuddlePresentation"
) as RainPuddlePresentation
assert(puddles != null)
assert(not puddles.is_light_performance_profile())
assert(puddles.is_processing())
puddles.set_suppressed(true)
assert(not puddles.is_processing())
puddles.set_suppressed(false)
assert(puddles.is_processing())
puddles.set_active(false)
assert(not puddles.is_processing())
puddles.set_active(true)
assert(puddles.is_processing())
for _tick: int in 18:
puddles.call("_process", 0.25)
assert(puddles.get_active_puddle_count() > 0)
var visual_root := puddles.get_node("PuddleVisuals") as Node3D
assert(visual_root != null)
for puddle_visual: Node in visual_root.get_children():
assert(puddle_visual is MeshInstance3D)
assert(puddle_visual.get_child_count() == 0)
weather.apply_authoritative_snapshot(
WorldWeatherService.Weather.SUNNY,
60.0,
)
puddles.call("_process", 30.0)
assert(puddles.get_active_puddle_count() == 0)
_stop_audio_players(main)
main.queue_free()
for _frame: int in 8:
await process_frame
print("Rain puddle runtime validation: PASS")
quit()
func _stop_audio_players(root_node: Node) -> void:
if root_node is AudioStreamPlayer:
(root_node as AudioStreamPlayer).stop()
elif root_node is AudioStreamPlayer2D:
(root_node as AudioStreamPlayer2D).stop()
elif root_node is AudioStreamPlayer3D:
(root_node as AudioStreamPlayer3D).stop()
for child: Node in root_node.get_children():
_stop_audio_players(child)

View file

@ -0,0 +1 @@
uid://jo5lqk2n8bik

View file

@ -1,50 +0,0 @@
extends SceneTree
const ShorelineAmbienceType := preload("res://world/shoreline_ambience.gd")
const WavesStream := preload("res://audio/ambience/waves.wav")
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var waves := WavesStream as AudioStreamWAV
assert(waves != null)
var segments: Array[PackedVector2Array] = [
PackedVector2Array([Vector2.ZERO, Vector2(10.0, 0.0)]),
]
assert(is_equal_approx(
ShorelineAmbienceType.distance_to_segments(Vector2(5.0, 3.0), segments),
3.0,
))
assert(is_equal_approx(
ShorelineAmbienceType.distance_to_segments(Vector2(-4.0, 0.0), segments),
4.0,
))
var controller := ShorelineAmbienceType.new() as ShorelineAmbience
var runtime_audio := AudioStreamPlayer.new()
runtime_audio.name = "WavesAudio"
runtime_audio.unique_name_in_owner = true
runtime_audio.stream = WavesStream
runtime_audio.bus = &"SFX"
controller.add_child(runtime_audio)
runtime_audio.owner = controller
root.add_child(controller)
await process_frame
assert(runtime_audio.bus == &"Environment")
assert(runtime_audio.stream == null)
controller.set_audio_enabled(true)
assert(controller.near_distance < controller.far_distance)
assert(controller.near_volume_db > controller.far_volume_db)
var runtime_waves := runtime_audio.stream as AudioStreamWAV
assert(runtime_waves.loop_mode == AudioStreamWAV.LOOP_FORWARD)
assert(runtime_waves.loop_begin == 0)
assert(runtime_waves.loop_end == 1044956)
controller.free()
await process_frame
print("Shoreline ambience validation: PASS")
quit()

View file

@ -1 +0,0 @@
uid://bwvhgpbkb00l1

View file

@ -1,97 +0,0 @@
extends SceneTree
const ShorelineMesh: ArrayMesh = preload(
"res://world/generated/shorelines/starter_ocean_shoreline.tres"
)
const GEOMETRY_EPSILON := 0.0001
const CROSSING_NEIGHBORHOOD := 32
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
assert(ShorelineMesh.get_surface_count() == 1)
var arrays := ShorelineMesh.surface_get_arrays(0)
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
assert(not vertices.is_empty())
assert(vertices.size() % 2 == 0)
assert(indices.size() == vertices.size() * 3)
var point_count: int = int(float(vertices.size()) / 2.0)
var land_edge := PackedVector2Array()
var water_edge := PackedVector2Array()
for index: int in point_count:
var land := vertices[index * 2]
var water := vertices[index * 2 + 1]
land_edge.append(Vector2(land.x, land.z))
water_edge.append(Vector2(water.x, water.z))
var ribbon_width := land.distance_to(water)
assert(ribbon_width >= 0.34)
assert(ribbon_width <= 1.5)
var crossing_count := (
_count_local_crossings(water_edge)
+ _count_local_crossings(land_edge)
)
if crossing_count > 0:
push_error(
"Shoreline ribbon contains %d local edge crossings."
% crossing_count
)
quit(1)
return
print("Shoreline ribbon validation: PASS")
quit()
func _count_local_crossings(points: PackedVector2Array) -> int:
var crossing_count := 0
for first: int in points.size():
var first_next := (first + 1) % points.size()
var checked_neighbors := mini(
CROSSING_NEIGHBORHOOD,
points.size() - 2,
)
for offset: int in range(2, checked_neighbors + 1):
var second := (first + offset) % points.size()
var second_next := (second + 1) % points.size()
if second_next == first:
continue
if _segments_cross(
points[first],
points[first_next],
points[second],
points[second_next],
):
crossing_count += 1
return crossing_count
func _segments_cross(
a: Vector2,
b: Vector2,
c: Vector2,
d: Vector2,
) -> bool:
if (
maxf(a.x, b.x) < minf(c.x, d.x) - GEOMETRY_EPSILON
or maxf(c.x, d.x) < minf(a.x, b.x) - GEOMETRY_EPSILON
or maxf(a.y, b.y) < minf(c.y, d.y) - GEOMETRY_EPSILON
or maxf(c.y, d.y) < minf(a.y, b.y) - GEOMETRY_EPSILON
):
return false
var first_direction := b - a
var second_direction := d - c
var denominator := first_direction.cross(second_direction)
if absf(denominator) <= GEOMETRY_EPSILON:
return false
var offset := c - a
var first_amount := offset.cross(second_direction) / denominator
var second_amount := offset.cross(first_direction) / denominator
return (
first_amount > GEOMETRY_EPSILON
and first_amount < 1.0 - GEOMETRY_EPSILON
and second_amount > GEOMETRY_EPSILON
and second_amount < 1.0 - GEOMETRY_EPSILON
)

View file

@ -1 +0,0 @@
uid://c73frygpfhred

View file

@ -19,6 +19,9 @@ const FishAvailabilityType = preload("res://fish/fish_availability.gd")
const FishableWaterRegionType = preload(
"res://world/fishable_water_region.gd"
)
const RainPuddlePresentationType = preload(
"res://world/rain_puddle_presentation.gd"
)
func _initialize() -> void:
@ -34,6 +37,7 @@ func _run() -> void:
_validate_fishing_weather_seams()
_validate_fishing_spot_context()
_validate_weather_presentation()
_validate_rain_puddle_surface_rules()
print("World weather validation: PASS")
quit()
@ -285,6 +289,53 @@ func _validate_fishing_spot_context() -> void:
weather.free()
func _validate_rain_puddle_surface_rules() -> void:
var dry_flat := PackedVector3Array([
Vector3(-1.0, 0.0, -1.0),
Vector3(2.0, 0.0, -1.0),
Vector3(-1.0, 0.0, 2.0),
])
assert(RainPuddlePresentationType.is_puddle_surface_eligible(
dry_flat,
-0.25,
))
var water_edge := PackedVector3Array([
Vector3(-1.0, 0.0, -1.0),
Vector3(2.0, 0.0, -1.0),
Vector3(-1.0, -0.2, 2.0),
])
assert(not RainPuddlePresentationType.is_puddle_surface_eligible(
water_edge,
-0.25,
))
var steep_ground := PackedVector3Array([
Vector3(-1.0, 0.0, -1.0),
Vector3(2.0, 0.0, -1.0),
Vector3(-1.0, 2.0, -0.75),
])
assert(not RainPuddlePresentationType.is_puddle_surface_eligible(
steep_ground,
-0.25,
))
var center := Vector3(4.0, 1.0, -3.0)
var radii := Vector2(1.0, 0.5)
assert(RainPuddlePresentationType.is_point_inside_puddle(
Vector3(4.75, 1.0, -3.0),
center,
radii,
))
assert(not RainPuddlePresentationType.is_point_inside_puddle(
Vector3(5.1, 1.0, -3.0),
center,
radii,
))
assert(not RainPuddlePresentationType.is_point_inside_puddle(
Vector3(4.0, 2.1, -3.0),
center,
radii,
))
func _validate_weather_presentation() -> void:
var world_root := Node3D.new()
root.add_child(world_root)