Add fishing and shoreline sound effects
This commit is contained in:
parent
de1df5ea21
commit
930acdb1a6
21 changed files with 497 additions and 1 deletions
BIN
audio/ambience/waves.wav
Normal file
BIN
audio/ambience/waves.wav
Normal file
Binary file not shown.
24
audio/ambience/waves.wav.import
Normal file
24
audio/ambience/waves.wav.import
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://dck3tf8qkadea"
|
||||
path="res://.godot/imported/waves.wav-90ffb886ce064ba838eebb25ec72a17e.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://audio/ambience/waves.wav"
|
||||
dest_files=["res://.godot/imported/waves.wav-90ffb886ce064ba838eebb25ec72a17e.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=1
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=1044956
|
||||
compress/mode=0
|
||||
BIN
audio/sfx/fishing/bobber.wav
Normal file
BIN
audio/sfx/fishing/bobber.wav
Normal file
Binary file not shown.
24
audio/sfx/fishing/bobber.wav.import
Normal file
24
audio/sfx/fishing/bobber.wav.import
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://sxydt4wnmrj3"
|
||||
path="res://.godot/imported/bobber.wav-b435bb251a112445f1ddebcacc74804a.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://audio/sfx/fishing/bobber.wav"
|
||||
dest_files=["res://.godot/imported/bobber.wav-b435bb251a112445f1ddebcacc74804a.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=2
|
||||
BIN
audio/sfx/fishing/fighting.wav
Normal file
BIN
audio/sfx/fishing/fighting.wav
Normal file
Binary file not shown.
24
audio/sfx/fishing/fighting.wav.import
Normal file
24
audio/sfx/fishing/fighting.wav.import
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://df6yg85cldetw"
|
||||
path="res://.godot/imported/fighting.wav-1427843e18acf5134cc66d4a813033b6.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://audio/sfx/fishing/fighting.wav"
|
||||
dest_files=["res://.godot/imported/fighting.wav-1427843e18acf5134cc66d4a813033b6.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=1
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=124487
|
||||
compress/mode=0
|
||||
BIN
audio/sfx/fishing/reeling.wav
Normal file
BIN
audio/sfx/fishing/reeling.wav
Normal file
Binary file not shown.
24
audio/sfx/fishing/reeling.wav.import
Normal file
24
audio/sfx/fishing/reeling.wav.import
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://v4yjxtslbrd7"
|
||||
path="res://.godot/imported/reeling.wav-97a9415ae2d9e7ca4c976bb15b10e6fd.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://audio/sfx/fishing/reeling.wav"
|
||||
dest_files=["res://.godot/imported/reeling.wav-97a9415ae2d9e7ca4c976bb15b10e6fd.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=1
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=114628
|
||||
compress/mode=0
|
||||
|
|
@ -13,3 +13,9 @@ bus/1/mute = false
|
|||
bus/1/bypass_fx = false
|
||||
bus/1/volume_db = 0.0
|
||||
bus/1/send = &"Master"
|
||||
bus/2/name = &"SFX"
|
||||
bus/2/solo = false
|
||||
bus/2/mute = false
|
||||
bus/2/bypass_fx = false
|
||||
bus/2/volume_db = 0.0
|
||||
bus/2/send = &"Master"
|
||||
|
|
|
|||
|
|
@ -122,6 +122,9 @@ const NETWORK_INPUT_RESEND_INTERVAL_SECONDS: float = 0.1
|
|||
|
||||
@onready var _catch_controller: CatchControllerType = %CatchController
|
||||
@onready var _presentation: FishingPresentationType = %FishingPresentation
|
||||
@onready var _bobber_audio: AudioStreamPlayer = %BobberAudio
|
||||
@onready var _fight_audio: AudioStreamPlayer = %FightAudio
|
||||
@onready var _reeling_audio: AudioStreamPlayer = %ReelingAudio
|
||||
|
||||
var state: FishingState = FishingState.READY
|
||||
var _external_input_blocked: bool = false
|
||||
|
|
@ -177,6 +180,12 @@ var _pending_cleanup_message: String = ""
|
|||
func _ready() -> void:
|
||||
_bite_rng.randomize()
|
||||
_configure_surface_resolver()
|
||||
var fight_stream := _fight_audio.stream as AudioStreamWAV
|
||||
if fight_stream != null:
|
||||
_configure_audio_loop(fight_stream)
|
||||
var reeling_stream := _reeling_audio.stream as AudioStreamWAV
|
||||
if reeling_stream != null:
|
||||
_configure_audio_loop(reeling_stream)
|
||||
_catch_controller.encounter_updated.connect(_on_catch_encounter_updated)
|
||||
_catch_controller.caught.connect(_on_catch_completed)
|
||||
_catch_controller.escaped.connect(_on_catch_escaped)
|
||||
|
|
@ -223,6 +232,15 @@ func setup(
|
|||
_network_fishing = network_fishing
|
||||
_world_time = world_time
|
||||
_world_weather = world_weather
|
||||
if (
|
||||
_network_session != null
|
||||
and not _network_session.state_changed.is_connected(
|
||||
_on_network_session_state_changed
|
||||
)
|
||||
):
|
||||
_network_session.state_changed.connect(
|
||||
_on_network_session_state_changed
|
||||
)
|
||||
if _network_fishing != null:
|
||||
_network_fishing.local_cast_accepted.connect(
|
||||
_on_network_cast_accepted
|
||||
|
|
@ -348,6 +366,7 @@ func set_local_menu_input_suppressed(
|
|||
_local_menu_input_owners.erase(owner)
|
||||
if suppressed and state == FishingState.WAITING_FOR_BITE:
|
||||
_withdrawal_input_held = false
|
||||
_stop_reeling_audio()
|
||||
_presentation.set_line_mode(
|
||||
FishingPresentationType.LineMode.SLACK
|
||||
)
|
||||
|
|
@ -357,6 +376,7 @@ func set_gameplay_input_enabled(enabled: bool) -> void:
|
|||
_gameplay_input_enabled = enabled
|
||||
if not enabled:
|
||||
_local_menu_input_owners.clear()
|
||||
_stop_reeling_audio()
|
||||
|
||||
|
||||
func configure_accessibility_auto_click(
|
||||
|
|
@ -400,6 +420,8 @@ func _secure_showcase_catch_for_recovery() -> void:
|
|||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_stop_fight_audio()
|
||||
_stop_reeling_audio()
|
||||
_showcase_restore_generation += 1
|
||||
if (
|
||||
state == FishingState.SHOWING_CATCH
|
||||
|
|
@ -531,6 +553,7 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
return
|
||||
FishingState.WAITING_FOR_BITE:
|
||||
_withdrawal_input_held = true
|
||||
_start_reeling_audio()
|
||||
if _network_fishing != null:
|
||||
_network_primary_input_held = true
|
||||
_network_input_resend_elapsed = 0.0
|
||||
|
|
@ -546,6 +569,7 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
get_viewport().set_input_as_handled()
|
||||
elif state == FishingState.WAITING_FOR_BITE:
|
||||
_withdrawal_input_held = false
|
||||
_stop_reeling_audio()
|
||||
if _network_fishing != null:
|
||||
_network_primary_input_held = false
|
||||
_network_input_resend_elapsed = 0.0
|
||||
|
|
@ -731,6 +755,7 @@ func _on_cast_completed() -> void:
|
|||
if not _is_cast_target_valid(_cast_target):
|
||||
_cleanup_attempt("", &"invalid")
|
||||
return
|
||||
_play_bobber_audio()
|
||||
if _network_fishing != null:
|
||||
_network_fishing.request_local_cast(
|
||||
_cast_origin_position,
|
||||
|
|
@ -816,6 +841,7 @@ func _update_waiting_for_bite(delta: float) -> void:
|
|||
and not Input.is_action_pressed("fish_primary")
|
||||
):
|
||||
_withdrawal_input_held = false
|
||||
_stop_reeling_audio()
|
||||
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
||||
|
||||
if not _withdrawal_input_held:
|
||||
|
|
@ -908,6 +934,7 @@ func _activate_bite() -> void:
|
|||
if state != FishingState.WAITING_FOR_BITE:
|
||||
return
|
||||
|
||||
_stop_reeling_audio()
|
||||
if (
|
||||
_active_player == null
|
||||
or _selected_fish == null
|
||||
|
|
@ -923,6 +950,7 @@ func _activate_bite() -> void:
|
|||
if _pending_catch == null or not _pending_catch.is_valid():
|
||||
_cancel_attempt()
|
||||
return
|
||||
_start_fight_audio()
|
||||
bite_activated.emit()
|
||||
status_changed.emit("fish on!")
|
||||
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
||||
|
|
@ -1001,6 +1029,7 @@ func _on_catch_completed() -> void:
|
|||
if _pending_catch == null or not _pending_catch.is_valid():
|
||||
_cancel_attempt()
|
||||
return
|
||||
_stop_fight_audio()
|
||||
state = FishingState.SHOWING_CATCH
|
||||
_showcase_ready = false
|
||||
_showcase_outcome_completed = false
|
||||
|
|
@ -1014,6 +1043,7 @@ func _on_catch_escaped() -> void:
|
|||
if state != FishingState.FIGHTING:
|
||||
return
|
||||
|
||||
_stop_fight_audio()
|
||||
var fish_name: String = "the fish"
|
||||
if _selected_fish != null and not _selected_fish.display_name.is_empty():
|
||||
fish_name = "the %s" % _selected_fish.display_name
|
||||
|
|
@ -1120,6 +1150,8 @@ func _cleanup_attempt(
|
|||
cooldown_message: String = "",
|
||||
visual_outcome: StringName = &"",
|
||||
) -> void:
|
||||
_stop_fight_audio()
|
||||
_stop_reeling_audio()
|
||||
if not visual_outcome.is_empty():
|
||||
if state == FishingState.RETURNING:
|
||||
return
|
||||
|
|
@ -1422,12 +1454,14 @@ func _on_network_cast_rejected(message: String) -> void:
|
|||
func _on_network_bite_started(_attempt_id: String) -> void:
|
||||
if state != FishingState.WAITING_FOR_BITE:
|
||||
return
|
||||
_stop_reeling_audio()
|
||||
state = FishingState.FIGHTING
|
||||
_withdrawal_input_held = false
|
||||
_network_primary_input_held = Input.is_action_pressed("fish_primary")
|
||||
_network_input_resend_elapsed = 0.0
|
||||
_network_auto_click_accumulator = 0.0
|
||||
_network_active_barrier_index = -1
|
||||
_start_fight_audio()
|
||||
bite_activated.emit()
|
||||
status_changed.emit("fish on!")
|
||||
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
||||
|
|
@ -1510,6 +1544,7 @@ func _update_network_auto_click(delta: float) -> void:
|
|||
func _on_network_catch_received(fish_catch: FishCatchType) -> void:
|
||||
if state != FishingState.FIGHTING or fish_catch == null:
|
||||
return
|
||||
_stop_fight_audio()
|
||||
_pending_catch = fish_catch
|
||||
state = FishingState.SHOWING_CATCH
|
||||
_showcase_ready = false
|
||||
|
|
@ -1541,6 +1576,57 @@ func _on_network_attempt_ended(
|
|||
_cleanup_attempt(message, visual_outcome)
|
||||
|
||||
|
||||
func _on_network_session_state_changed(state_value: NetworkSession.State) -> void:
|
||||
if state_value in [
|
||||
NetworkSession.State.INACTIVE,
|
||||
NetworkSession.State.DISCONNECTING,
|
||||
NetworkSession.State.CONNECTION_FAILED,
|
||||
NetworkSession.State.SERVER_LOST,
|
||||
]:
|
||||
_stop_fight_audio()
|
||||
_stop_reeling_audio()
|
||||
|
||||
|
||||
func _start_fight_audio() -> void:
|
||||
if _fight_audio == null or _fight_audio.stream == null:
|
||||
return
|
||||
if not _fight_audio.playing:
|
||||
_fight_audio.play()
|
||||
|
||||
|
||||
func _play_bobber_audio() -> void:
|
||||
if _bobber_audio == null or _bobber_audio.stream == null:
|
||||
return
|
||||
_bobber_audio.play()
|
||||
|
||||
|
||||
func _stop_fight_audio() -> void:
|
||||
if _fight_audio != null and _fight_audio.playing:
|
||||
_fight_audio.stop()
|
||||
|
||||
|
||||
func _configure_audio_loop(stream: AudioStreamWAV) -> void:
|
||||
stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||
if stream.loop_end <= stream.loop_begin:
|
||||
stream.loop_begin = 0
|
||||
stream.loop_end = maxi(
|
||||
1,
|
||||
roundi(stream.get_length() * float(stream.mix_rate)),
|
||||
)
|
||||
|
||||
|
||||
func _start_reeling_audio() -> void:
|
||||
if _reeling_audio == null or _reeling_audio.stream == null:
|
||||
return
|
||||
if not _reeling_audio.playing:
|
||||
_reeling_audio.play()
|
||||
|
||||
|
||||
func _stop_reeling_audio() -> void:
|
||||
if _reeling_audio != null and _reeling_audio.playing:
|
||||
_reeling_audio.stop()
|
||||
|
||||
|
||||
func resolve_safe_withdrawal_surface(
|
||||
from: Vector3,
|
||||
to: Vector3,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
[gd_scene load_steps=12 format=3]
|
||||
[gd_scene load_steps=15 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://fishing/fishing_spot.gd" id="1_spot"]
|
||||
[ext_resource type="AudioStream" path="res://audio/sfx/fishing/fighting.wav" id="2_fighting_audio"]
|
||||
[ext_resource type="AudioStream" path="res://audio/sfx/fishing/reeling.wav" id="2_reeling_audio"]
|
||||
[ext_resource type="AudioStream" path="res://audio/sfx/fishing/bobber.wav" id="2_bobber_audio"]
|
||||
[ext_resource type="Script" path="res://fishing/fishing_presentation.gd" id="3_presentation"]
|
||||
[ext_resource type="Script" path="res://fishing/catch_controller.gd" id="4_catch"]
|
||||
|
||||
|
|
@ -41,6 +44,24 @@ size = Vector3(0.62, 0.04, 0.09)
|
|||
[node name="FishingSpot" type="Node3D"]
|
||||
script = ExtResource("1_spot")
|
||||
|
||||
[node name="BobberAudio" type="AudioStreamPlayer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
stream = ExtResource("2_bobber_audio")
|
||||
volume_db = -14.0
|
||||
bus = &"SFX"
|
||||
|
||||
[node name="FightAudio" type="AudioStreamPlayer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
stream = ExtResource("2_fighting_audio")
|
||||
volume_db = -10.0
|
||||
bus = &"SFX"
|
||||
|
||||
[node name="ReelingAudio" type="AudioStreamPlayer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
stream = ExtResource("2_reeling_audio")
|
||||
volume_db = -10.0
|
||||
bus = &"SFX"
|
||||
|
||||
[node name="CatchController" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("4_catch")
|
||||
|
|
|
|||
|
|
@ -201,9 +201,15 @@ var _application_initialized := false
|
|||
var _pending_existing_root_path := ""
|
||||
var _local_recovery_attempt_id: String = ""
|
||||
|
||||
@onready var _shoreline_ambience: ShorelineAmbience = %ShorelineAmbience
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
DisplayServer.window_set_title("NETfishing")
|
||||
_shoreline_ambience.configure(
|
||||
_player,
|
||||
_test_world.get_saltwater_shoreline_mesh(),
|
||||
)
|
||||
var menu_pattern_material := (
|
||||
_player_menu_backdrop.material as ShaderMaterial
|
||||
)
|
||||
|
|
@ -1026,6 +1032,7 @@ func _reset_pixelation() -> void:
|
|||
|
||||
func _set_gameplay_active(active: bool) -> void:
|
||||
_gameplay_started = active
|
||||
_shoreline_ambience.set_active(active)
|
||||
_title_background.visible = not active
|
||||
_world_pixelation.set_gameplay_active(active)
|
||||
_ui_pixelation.set_gameplay_active(active)
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@
|
|||
[ext_resource type="Script" path="res://network/network_world_weather_service.gd" id="51_network_world_weather"]
|
||||
[ext_resource type="Script" path="res://jobs/player_job_service.gd" id="52_player_jobs"]
|
||||
[ext_resource type="Script" path="res://network/network_job_service.gd" id="53_network_jobs"]
|
||||
[ext_resource type="Script" path="res://world/shoreline_ambience.gd" id="54_shoreline_ambience"]
|
||||
[ext_resource type="AudioStream" path="res://audio/ambience/waves.wav" id="55_waves"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"]
|
||||
shader = ExtResource("40_title_water")
|
||||
|
|
@ -277,6 +279,15 @@ stream = ExtResource("13_title_music")
|
|||
volume_db = -80.0
|
||||
bus = &"Music"
|
||||
|
||||
[node name="ShorelineAmbience" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("54_shoreline_ambience")
|
||||
|
||||
[node name="WavesAudio" type="AudioStreamPlayer" parent="ShorelineAmbience"]
|
||||
unique_name_in_owner = true
|
||||
stream = ExtResource("55_waves")
|
||||
bus = &"SFX"
|
||||
|
||||
[node name="WorldPixelationPostprocess" parent="." unique_id=344378000 instance=ExtResource("16_world_pixelation")]
|
||||
unique_name_in_owner = true
|
||||
|
||||
|
|
|
|||
54
tests/fishing_audio_validation.gd
Normal file
54
tests/fishing_audio_validation.gd
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
extends SceneTree
|
||||
|
||||
const FishingSpotScene := preload("res://fishing/fishing_spot.tscn")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var fishing_spot := FishingSpotScene.instantiate() as FishingSpot
|
||||
root.add_child(fishing_spot)
|
||||
await process_frame
|
||||
|
||||
var fight_audio := fishing_spot.get_node("%FightAudio") as AudioStreamPlayer
|
||||
var bobber_audio := fishing_spot.get_node(
|
||||
"%BobberAudio"
|
||||
) as AudioStreamPlayer
|
||||
assert(bobber_audio != null)
|
||||
assert(bobber_audio.stream != null)
|
||||
assert(bobber_audio.bus == &"SFX")
|
||||
assert(is_equal_approx(bobber_audio.volume_db, -14.0))
|
||||
var bobber_stream := bobber_audio.stream as AudioStreamWAV
|
||||
assert(bobber_stream != null)
|
||||
assert(bobber_stream.loop_mode == AudioStreamWAV.LOOP_DISABLED)
|
||||
assert(fight_audio != null)
|
||||
assert(fight_audio.stream != null)
|
||||
assert(fight_audio.bus == &"SFX")
|
||||
assert(is_equal_approx(fight_audio.volume_db, -10.0))
|
||||
var fight_stream := fight_audio.stream as AudioStreamWAV
|
||||
assert(fight_stream != null)
|
||||
assert(fight_stream.loop_mode == AudioStreamWAV.LOOP_FORWARD)
|
||||
assert(fight_stream.loop_end > fight_stream.loop_begin)
|
||||
var reeling_audio := fishing_spot.get_node(
|
||||
"%ReelingAudio"
|
||||
) as AudioStreamPlayer
|
||||
assert(reeling_audio != null)
|
||||
assert(reeling_audio.stream != null)
|
||||
assert(reeling_audio.bus == &"SFX")
|
||||
assert(is_equal_approx(reeling_audio.volume_db, -10.0))
|
||||
var reeling_stream := reeling_audio.stream as AudioStreamWAV
|
||||
assert(reeling_stream != null)
|
||||
assert(reeling_stream.loop_mode == AudioStreamWAV.LOOP_FORWARD)
|
||||
assert(reeling_stream.loop_end > reeling_stream.loop_begin)
|
||||
|
||||
assert(fishing_spot.has_method("_start_fight_audio"))
|
||||
assert(fishing_spot.has_method("_play_bobber_audio"))
|
||||
assert(fishing_spot.has_method("_stop_fight_audio"))
|
||||
assert(fishing_spot.has_method("_start_reeling_audio"))
|
||||
assert(fishing_spot.has_method("_stop_reeling_audio"))
|
||||
fishing_spot.free()
|
||||
await process_frame
|
||||
print("Fishing audio validation: PASS")
|
||||
quit()
|
||||
1
tests/fishing_audio_validation.gd.uid
Normal file
1
tests/fishing_audio_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bby8mv7bsjd3w
|
||||
48
tests/shoreline_ambience_validation.gd
Normal file
48
tests/shoreline_ambience_validation.gd
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
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 == &"SFX")
|
||||
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()
|
||||
1
tests/shoreline_ambience_validation.gd.uid
Normal file
1
tests/shoreline_ambience_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bwvhgpbkb00l1
|
||||
|
|
@ -58,6 +58,10 @@ func get_pelican_landmark() -> Node3D:
|
|||
return get_node_or_null(pelican_landmark_path) as Node3D
|
||||
|
||||
|
||||
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
||||
return get_node_or_null(^"ShorelineRibbons/Ocean") as MeshInstance3D
|
||||
|
||||
|
||||
func has_terrain_collision() -> bool:
|
||||
var collision_shape: CollisionShape3D = (
|
||||
get_node_or_null(terrain_collision_shape_path) as CollisionShape3D
|
||||
|
|
|
|||
156
world/shoreline_ambience.gd
Normal file
156
world/shoreline_ambience.gd
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
class_name ShorelineAmbience
|
||||
extends Node
|
||||
|
||||
@export_range(0.0, 20.0, 0.1) var near_distance: float = 2.0
|
||||
@export_range(1.0, 100.0, 0.5) var far_distance: float = 24.0
|
||||
@export_range(-40.0, 12.0, 0.5) var near_volume_db: float = 5.0
|
||||
@export_range(-80.0, 0.0, 0.5) var far_volume_db: float = -18.0
|
||||
@export_range(0.05, 1.0, 0.05) var distance_update_interval: float = 0.2
|
||||
@export_range(0.1, 20.0, 0.1) var volume_smoothing_speed: float = 4.0
|
||||
|
||||
@onready var _waves_audio: AudioStreamPlayer = %WavesAudio
|
||||
|
||||
var _listener: Node3D
|
||||
var _shoreline_segments: Array[PackedVector2Array] = []
|
||||
var _distance_update_remaining: float = 0.0
|
||||
var _target_volume_db: float = -80.0
|
||||
var _is_active := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_configure_audio_loop(_waves_audio.stream)
|
||||
_waves_audio.volume_db = -80.0
|
||||
set_process(false)
|
||||
|
||||
|
||||
func configure(listener: Node3D, shoreline_mesh: MeshInstance3D) -> void:
|
||||
_listener = listener
|
||||
_shoreline_segments = _extract_shoreline_segments(shoreline_mesh)
|
||||
_distance_update_remaining = 0.0
|
||||
if _shoreline_segments.is_empty():
|
||||
push_warning("Saltwater shoreline ambience has no coastline geometry.")
|
||||
|
||||
|
||||
func set_active(active: bool) -> void:
|
||||
_is_active = active
|
||||
set_process(active)
|
||||
if active:
|
||||
_distance_update_remaining = 0.0
|
||||
_update_target_volume()
|
||||
_waves_audio.volume_db = _target_volume_db
|
||||
if not _waves_audio.playing and _waves_audio.stream != null:
|
||||
_waves_audio.play()
|
||||
else:
|
||||
_waves_audio.stop()
|
||||
_waves_audio.volume_db = -80.0
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not _is_active:
|
||||
return
|
||||
_distance_update_remaining -= delta
|
||||
if _distance_update_remaining <= 0.0:
|
||||
_distance_update_remaining = distance_update_interval
|
||||
_update_target_volume()
|
||||
_waves_audio.volume_db = lerpf(
|
||||
_waves_audio.volume_db,
|
||||
_target_volume_db,
|
||||
1.0 - exp(-volume_smoothing_speed * delta),
|
||||
)
|
||||
|
||||
|
||||
func _update_target_volume() -> void:
|
||||
if _listener == null or _shoreline_segments.is_empty():
|
||||
_target_volume_db = -80.0
|
||||
return
|
||||
var listener_position := Vector2(
|
||||
_listener.global_position.x,
|
||||
_listener.global_position.z,
|
||||
)
|
||||
var distance := distance_to_segments(listener_position, _shoreline_segments)
|
||||
var blend := clampf(
|
||||
inverse_lerp(near_distance, maxf(near_distance + 0.01, far_distance), distance),
|
||||
0.0,
|
||||
1.0,
|
||||
)
|
||||
_target_volume_db = lerpf(near_volume_db, far_volume_db, smoothstep(0.0, 1.0, blend))
|
||||
|
||||
|
||||
func _extract_shoreline_segments(
|
||||
shoreline_mesh: MeshInstance3D,
|
||||
) -> Array[PackedVector2Array]:
|
||||
var segments: Array[PackedVector2Array] = []
|
||||
var seen_segments: Dictionary = {}
|
||||
if shoreline_mesh == null or shoreline_mesh.mesh == null:
|
||||
return segments
|
||||
for surface_index in shoreline_mesh.mesh.get_surface_count():
|
||||
var arrays := shoreline_mesh.mesh.surface_get_arrays(surface_index)
|
||||
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
var indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
|
||||
if indices.is_empty():
|
||||
continue
|
||||
for index_offset in range(0, indices.size() - 2, 3):
|
||||
var triangle := PackedVector2Array()
|
||||
for corner in 3:
|
||||
var vertex := shoreline_mesh.to_global(
|
||||
vertices[indices[index_offset + corner]]
|
||||
)
|
||||
triangle.append(Vector2(vertex.x, vertex.z))
|
||||
_append_unique_segment(segments, seen_segments, triangle[0], triangle[1])
|
||||
_append_unique_segment(segments, seen_segments, triangle[1], triangle[2])
|
||||
_append_unique_segment(segments, seen_segments, triangle[2], triangle[0])
|
||||
return segments
|
||||
|
||||
|
||||
func _append_unique_segment(
|
||||
segments: Array[PackedVector2Array],
|
||||
seen_segments: Dictionary,
|
||||
start: Vector2,
|
||||
end: Vector2,
|
||||
) -> void:
|
||||
if start.distance_squared_to(end) <= 0.000001:
|
||||
return
|
||||
# The baked ribbon repeats shared triangle edges. Quantized canonical keys
|
||||
# keep only distinct segments without an O(n²) startup pass.
|
||||
var first := start
|
||||
var second := end
|
||||
if first.x > second.x or (is_equal_approx(first.x, second.x) and first.y > second.y):
|
||||
first = end
|
||||
second = start
|
||||
var key := "%d,%d:%d,%d" % [
|
||||
roundi(first.x * 10000.0),
|
||||
roundi(first.y * 10000.0),
|
||||
roundi(second.x * 10000.0),
|
||||
roundi(second.y * 10000.0),
|
||||
]
|
||||
if seen_segments.has(key):
|
||||
return
|
||||
seen_segments[key] = true
|
||||
segments.append(PackedVector2Array([start, end]))
|
||||
|
||||
|
||||
static func distance_to_segments(
|
||||
point: Vector2,
|
||||
segments: Array[PackedVector2Array],
|
||||
) -> float:
|
||||
var closest_squared := INF
|
||||
for segment in segments:
|
||||
if segment.size() < 2:
|
||||
continue
|
||||
var closest := Geometry2D.get_closest_point_to_segment(
|
||||
point,
|
||||
segment[0],
|
||||
segment[1],
|
||||
)
|
||||
closest_squared = minf(closest_squared, point.distance_squared_to(closest))
|
||||
return sqrt(closest_squared) if closest_squared < INF else INF
|
||||
|
||||
|
||||
func _configure_audio_loop(stream: AudioStream) -> void:
|
||||
var wav := stream as AudioStreamWAV
|
||||
if wav == null:
|
||||
return
|
||||
wav.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||
if wav.loop_end <= wav.loop_begin:
|
||||
wav.loop_begin = 0
|
||||
wav.loop_end = int(round(wav.get_length() * wav.mix_rate))
|
||||
1
world/shoreline_ambience.gd.uid
Normal file
1
world/shoreline_ambience.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b0c3o2vy76fg3
|
||||
|
|
@ -61,6 +61,10 @@ func get_pelican_convenience_landmark() -> Node3D:
|
|||
return _starter_island.get_pelican_landmark()
|
||||
|
||||
|
||||
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
||||
return _starter_island.get_saltwater_shoreline_mesh()
|
||||
|
||||
|
||||
func _get_regions() -> Array[WorldRegion]:
|
||||
var regions: Array[WorldRegion] = []
|
||||
for child: Node in _regions_root.get_children():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue