diff --git a/CREDITS.md b/CREDITS.md index 4f8b351..ea0a6f5 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -39,6 +39,10 @@ written with or without assistance. https://freesound.org/s/852826/ - Rain Loop Ontario by Ayton -- https://freesound.org/s/212799/ -- License: Attribution 3.0 +- Dog_Bark.wav by ivolipa -- https://freesound.org/s/328729/ -- License: + Creative Commons 0 +- Cat meow.m4a by Christyboy100 -- https://freesound.org/s/495694/ -- License: + Attribution 3.0 - “Quick Water Droplet” by qubodup, used for the bobber water-impact sound. Freesound sound 792931, Creative Commons Zero (CC0): https://freesound.org/s/792931/ diff --git a/default_bus_layout.tres b/default_bus_layout.tres index 665b0e9..d780a19 100644 --- a/default_bus_layout.tres +++ b/default_bus_layout.tres @@ -19,3 +19,9 @@ bus/2/mute = false bus/2/bypass_fx = false bus/2/volume_db = 0.0 bus/2/send = &"Master" +bus/3/name = &"Environment" +bus/3/solo = false +bus/3/mute = false +bus/3/bypass_fx = false +bus/3/volume_db = 0.0 +bus/3/send = &"Master" diff --git a/docs/ASSET-PROVENANCE.md b/docs/ASSET-PROVENANCE.md index 9c3bc76..3d3907a 100644 --- a/docs/ASSET-PROVENANCE.md +++ b/docs/ASSET-PROVENANCE.md @@ -36,6 +36,8 @@ locations only and must never appear in scenes or resources. | Manual-reeling loop | `audio/sfx/fishing/reeling.wav` | Edited from the same CC0 “Spinning reel.wav” source. | | Saltwater wave ambience | `audio/ambience/waves.wav` | “Gentle Ocean Waves Loop” by Freesound user kkenny101, sound 852826, CC0. | | Bobber water impact | `audio/sfx/fishing/bobber.wav` | “Quick Water Droplet” by Freesound user qubodup, sound 792931, CC0. | +| Character bark call | `sound/dialogue/calls/bark.wav` | Edited from `Dog_Bark.wav` by Freesound user ivolipa, sound 328729, CC0. | +| Character meow call | `sound/dialogue/calls/meow.wav` | Edited from `Cat meow.m4a` by Freesound user Christyboy100, sound 495694, Attribution 3.0. | ### Fishing fight loop source record @@ -91,6 +93,40 @@ locations only and must never appear in scenes or resources. - The downloaded source remains in the project owner's source-work archive; only the finished runtime sound ships in the game. +### Character-call source records + +#### Bark + +- Source page: https://freesound.org/s/328729/ +- Creator: ivolipa +- Source title: `Dog_Bark.wav` +- License: Creative Commons Zero (CC0) +- Downloaded source filename: `328729__ivolipa__dog_bark.wav` +- Downloaded source SHA-256: + `c3d542dee7bfcc1901428e0c1408c0bd522d778d8e5e1dbca156cab008f3d712` +- Runtime edit source/destination SHA-256: + `5ac2f32f33724a2fc232d725c5c37d4cfe3e93b3c1f19aa4177d2c8e3009ec78` +- Runtime format: 44.1 kHz, 16-bit, stereo PCM WAV; 0.446780 seconds; + one-shot. + +#### Meow + +- Source page: https://freesound.org/s/495694/ +- Creator: Christyboy100 +- Source title: `Cat meow.m4a` +- License: Attribution 3.0 +- Downloaded source filename: `495694__christyboy100__cat-meow.m4a` +- Downloaded source SHA-256: + `afa3f6ef7afa2f7fee1e634d5505d2101cbba4421f8d8ff9adf562939c7a18f8` +- Converted working WAV SHA-256: + `a505e6e230e7d9d76b65c46270d97a35a1acc510b43aa5e7343d7b04f424cade` +- Runtime edit source/destination SHA-256: + `cce22ce1fbbc9325126a153aedc4526fde1aacf78780574fb432197dd44eb6b3` +- Runtime format: 44.1 kHz, 16-bit, stereo PCM WAV; 0.605420 seconds; + one-shot. +- The downloaded sources and working edits remain in the project owner's + source-work archive; only the finished runtime sounds ship in the game. + ## Generated resources Godot `.import` sidecars and deterministic shoreline `.tres` meshes are derived diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index cadd214..988c88c 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -153,6 +153,7 @@ var _cast_charge: float = 0.0 var _cast_direction: Vector3 = Vector3.FORWARD var _cast_origin_position: Vector3 var _cast_target: Vector3 +var _preferred_cast_arc_height: float = -1.0 var _withdrawal_endpoint: Vector3 var _withdrawal_progress: float = 0.0 var _withdrawal_input_held: bool = false @@ -402,6 +403,11 @@ func begin_water_recovery() -> void: FishingState.FIGHTING, ]: _cancel_attempt() + # Recovery replaces the normal rod-return presentation. Finish the local + # cleanup now so its temporary movement lock is not captured and restored + # after the player respawns. + if state != FishingState.READY: + _finalize_attempt_cleanup("") elif state == FishingState.SHOWING_CATCH: _secure_showcase_catch_for_recovery() @@ -622,7 +628,10 @@ func _begin_aiming(player: PlayerType) -> void: _cast_target = _calculate_cast_target(minimum_cast_distance) state = FishingState.AIMING_CAST status_changed.emit("") - _cast_path_is_clear = is_cast_path_clear(_cast_origin_position, _cast_target) + _cast_path_is_clear = is_cast_path_clear( + _get_cast_launch_position(), + _cast_target, + ) var target_is_fishable: bool = ( _aim_surface_sample.is_fishable() and _cast_path_is_clear ) @@ -750,7 +759,10 @@ func _update_cast_charge(delta: float) -> void: var maximum_distance: float = maxf(minimum_cast_distance, maximum_cast_distance) var distance: float = lerpf(minimum_cast_distance, maximum_distance, _cast_charge) _cast_target = _calculate_cast_target(distance) - _cast_path_is_clear = is_cast_path_clear(_cast_origin_position, _cast_target) + _cast_path_is_clear = is_cast_path_clear( + _get_cast_launch_position(), + _cast_target, + ) var target_is_fishable: bool = ( _aim_surface_sample.is_fishable() and _cast_path_is_clear ) @@ -770,8 +782,9 @@ func _confirm_cast() -> void: if state != FishingState.AIMING_CAST: return + var cast_launch_position := _get_cast_launch_position() _cast_path_is_clear = is_cast_path_clear( - _cast_origin_position, + cast_launch_position, _cast_target, ) var cast_is_invalid: bool = ( @@ -780,7 +793,7 @@ func _confirm_cast() -> void: var arrival_position: Vector3 = _cast_target if not _cast_path_is_clear: arrival_position = _resolve_cast_impact_position( - _cast_origin_position, + cast_launch_position, _cast_target, ) if _active_player != null: @@ -1344,6 +1357,14 @@ func _capture_cast_direction(player: PlayerType) -> Vector3: return direction.normalized() +func _get_cast_launch_position() -> Vector3: + if _active_player != null: + var rod_tip: Marker3D = _active_player.get_fishing_rod_tip() + if rod_tip != null and is_instance_valid(rod_tip): + return rod_tip.global_position + return _cast_origin_position + + func _calculate_cast_target(distance: float) -> Vector3: var query_position := Vector3( _cast_origin_position.x + _cast_direction.x * distance, @@ -1399,17 +1420,25 @@ func is_target_fishable(target: Vector3) -> bool: func _is_cast_target_valid(target: Vector3) -> bool: return ( is_target_fishable(target) - and is_cast_path_clear(_cast_origin_position, target) + and is_cast_path_clear(_get_cast_launch_position(), target) ) func is_cast_path_clear(origin: Vector3, target: Vector3) -> bool: - return _surface_resolver.find_first_cast_collision( + if _preferred_cast_arc_height < 0.0: + _preferred_cast_arc_height = maxf( + _presentation.cast_arc_height, + 0.0, + ) + var resolved_arc: Dictionary = _surface_resolver.resolve_cast_arc( get_world_3d().direct_space_state, origin, target, - _presentation.cast_arc_height, - ).is_empty() + _preferred_cast_arc_height, + ) + _presentation.cast_arc_height = float(resolved_arc["arc_height"]) + var collision: Dictionary = resolved_arc["collision"] + return collision.is_empty() func _resolve_cast_impact_position(origin: Vector3, target: Vector3) -> Vector3: @@ -1540,6 +1569,11 @@ func _on_network_cast_accepted( target: Vector3, ) -> void: if state != FishingState.CASTING: + # A cast response can arrive after water recovery has already cancelled + # its local presentation. Do not leave that late authoritative attempt + # alive with its movement lock still applied. + if _network_fishing != null and _network_fishing.has_local_attempt(): + _network_fishing.cancel_local_attempt("") return _cast_target = target _bobber_water_position = target diff --git a/fishing/fishing_surface_resolver.gd b/fishing/fishing_surface_resolver.gd index 08818ce..a764c58 100644 --- a/fishing/fishing_surface_resolver.gd +++ b/fishing/fishing_surface_resolver.gd @@ -101,6 +101,40 @@ func find_first_cast_collision( return {} +func resolve_cast_arc( + space_state: PhysicsDirectSpaceState3D, + origin: Vector3, + target: Vector3, + preferred_arc_height: float, +) -> Dictionary: + var raised_arc_height: float = maxf(preferred_arc_height, 0.0) + var raised_collision := find_first_cast_collision( + space_state, + origin, + target, + raised_arc_height, + ) + if raised_collision.is_empty() or is_zero_approx(raised_arc_height): + return { + "arc_height": raised_arc_height, + "collision": raised_collision, + } + + # A canopy or pier roof can intersect the decorative raised arc even when + # the direct path out toward the water is unobstructed. Prefer that low cast + # instead of visibly sending the bobber and line upward into the ceiling. + var direct_collision := find_first_cast_collision( + space_state, + origin, + target, + 0.0, + ) + return { + "arc_height": 0.0, + "collision": direct_collision, + } + + func resolve_withdrawal_surface( space_state: PhysicsDirectSpaceState3D, current_position: Vector3, diff --git a/main/main.gd b/main/main.gd index e62acfb..c4a6089 100644 --- a/main/main.gd +++ b/main/main.gd @@ -1123,6 +1123,13 @@ func _process(_delta: float) -> void: func _apply_runtime_settings(settings: PlayerSettingsType) -> void: if settings == null: return + var requested_window_mode: DisplayServer.WindowMode = ( + DisplayServer.WINDOW_MODE_FULLSCREEN + if settings.fullscreen_enabled + else DisplayServer.WINDOW_MODE_WINDOWED + ) + if DisplayServer.window_get_mode() != requested_window_mode: + DisplayServer.window_set_mode(requested_window_mode) _apply_world_pixelation(settings.world_pixel_size) _ui_pixelation.set_pixel_size(settings.ui_pixel_size) _ui_pixelation.set_on_screen_keyboard_enabled( diff --git a/main/main.tscn b/main/main.tscn index af6f663..6f52273 100644 --- a/main/main.tscn +++ b/main/main.tscn @@ -405,7 +405,7 @@ script = ExtResource("54_shoreline_ambience") [node name="WavesAudio" type="AudioStreamPlayer" parent="ShorelineAmbience" unique_id=193530658] unique_name_in_owner = true stream = ExtResource("55_waves") -bus = &"SFX" +bus = &"Environment" [node name="WorldPixelationPostprocess" parent="." unique_id=344378000 instance=ExtResource("16_world_pixelation")] unique_name_in_owner = true diff --git a/network/network_chat_service.gd b/network/network_chat_service.gd index bed0d01..368c529 100644 --- a/network/network_chat_service.gd +++ b/network/network_chat_service.gd @@ -4,17 +4,37 @@ extends Node const BURST_COUNT: int = 3 const WINDOW_COUNT: int = 5 const WINDOW_SECONDS: float = 10.0 +const CALL_COOLDOWN_MILLISECONDS: int = 180 +const CALL_PITCH_VARIANTS: Array[float] = [ + 0.96, + 1.03, + 0.985, + 1.055, + 1.0, + 0.975, + 1.04, +] +const VoiceProfilesType = preload( + "res://player/animalese_voice_profiles.gd" +) signal message_received(message: Dictionary) signal local_message_confirmed(message: Dictionary) signal send_rejected(message: String) signal history_replaced(messages: Array[Dictionary]) +signal character_call_received( + peer_id: int, + call_id: String, + pitch_scale: float, +) var _session: NetworkSession var _history: Array[Dictionary] = [] var _seen_messages: Dictionary[String, bool] = {} var _request_ledgers: Dictionary[int, Dictionary] = {} var _rate_times: Dictionary[int, Array] = {} +var _last_call_msec: Dictionary[int, int] = {} +var _call_variant_indices: Dictionary[int, int] = {} var _sequence: int = 0 var _peer_names: Dictionary[int, String] = {} var _relationships: PlayerRelationshipStore @@ -79,6 +99,134 @@ func send_local_message(body: String) -> bool: return true +func send_local_character_call(call_id: String) -> bool: + if ( + _session == null + or not _session.is_gameplay_session_active() + or not VoiceProfilesType.is_valid_call(call_id) + or ( + not _session.is_host() + and not _session.supports_server_capability( + NetworkChatProtocol.CAPABILITY + ) + ) + ): + return false + var request := { + "request_id": _new_id("character_call"), + "session_id": _session.get_session_id(), + "call_id": call_id, + "sender_fingerprint": _session.get_local_identity_fingerprint(), + } + request["sender_signature"] = _session.sign_local_action( + "character_call", _character_call_signature_fields(request) + ) + if _session.is_host(): + _handle_character_call(_session.get_local_peer_id(), request) + else: + submit_character_call.rpc_id(1, request) + return true + + +@rpc("any_peer", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL) +func submit_character_call(data: Dictionary) -> void: + var sender_id := multiplayer.get_remote_sender_id() + if _session.is_host() and _session.is_authenticated_peer(sender_id): + _handle_character_call(sender_id, data) + + +func _handle_character_call(peer_id: int, data: Dictionary) -> void: + if ( + typeof(data.get("request_id")) != TYPE_STRING + or str(data["request_id"]).is_empty() + or str(data["request_id"]).length() > 64 + or typeof(data.get("session_id")) != TYPE_STRING + or str(data["session_id"]) != _session.get_session_id() + or typeof(data.get("call_id")) != TYPE_STRING + or not VoiceProfilesType.is_valid_call(str(data["call_id"])) + or typeof(data.get("sender_fingerprint")) != TYPE_STRING + or typeof(data.get("sender_signature")) != TYPE_PACKED_BYTE_ARRAY + ): + return + var record := _session.get_peer_record(peer_id) + if ( + record == null + or record.identity_fingerprint != str(data["sender_fingerprint"]) + or not _session.verify_peer_action( + peer_id, + "character_call", + _character_call_signature_fields(data), + data["sender_signature"], + ) + or not _consume_character_call_rate(peer_id) + ): + return + var pitch_scale: float = _next_character_call_pitch(peer_id) + _apply_character_call(peer_id, str(data["call_id"]), pitch_scale) + receive_character_call.rpc( + peer_id, + str(data["call_id"]), + pitch_scale, + ) + + +@rpc("authority", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL) +func receive_character_call( + peer_id: int, + call_id: String, + pitch_scale: float, +) -> void: + _apply_character_call(peer_id, call_id, pitch_scale) + + +func _apply_character_call( + peer_id: int, + call_id: String, + pitch_scale: float, +) -> void: + if ( + not VoiceProfilesType.is_valid_call(call_id) + or pitch_scale < 0.9 + or pitch_scale > 1.1 + or _session == null + or not _session.is_gameplay_session_active() + ): + return + var record := _session.get_peer_record(peer_id) + if record == null or is_sender_filtered(record.identity_fingerprint): + return + character_call_received.emit(peer_id, call_id, pitch_scale) + + +func _next_character_call_pitch(peer_id: int) -> float: + var variant_index: int = _call_variant_indices.get(peer_id, 0) + var pitch_scale: float = CALL_PITCH_VARIANTS[ + variant_index % CALL_PITCH_VARIANTS.size() + ] + _call_variant_indices[peer_id] = variant_index + 1 + return pitch_scale + + +func _consume_character_call_rate(peer_id: int) -> bool: + var now_msec: int = Time.get_ticks_msec() + var last_msec: int = _last_call_msec.get( + peer_id, now_msec - CALL_COOLDOWN_MILLISECONDS + ) + if now_msec - last_msec < CALL_COOLDOWN_MILLISECONDS: + return false + _last_call_msec[peer_id] = now_msec + return true + + +func _character_call_signature_fields(data: Dictionary) -> Array: + return [ + str(data.get("session_id", "")), + str(data.get("request_id", "")), + str(data.get("sender_fingerprint", "")), + str(data.get("call_id", "")), + ] + + func broadcast_system_message(body: String) -> bool: if _session == null or not _session.is_host(): return false @@ -272,6 +420,8 @@ func _on_peer_authenticated(peer_id: int, display_name: String) -> void: func _on_peer_removed(peer_id: int) -> void: _request_ledgers.erase(peer_id) _rate_times.erase(peer_id) + _last_call_msec.erase(peer_id) + _call_variant_indices.erase(peer_id) if not _session.is_host(): return var display_name: String = _peer_names.get(peer_id, "Player") @@ -329,6 +479,8 @@ func _on_session_state_changed(state: NetworkSession.State) -> void: _seen_messages.clear() _request_ledgers.clear() _rate_times.clear() + _last_call_msec.clear() + _call_variant_indices.clear() _peer_names.clear() _sequence = 0 history_replaced.emit([]) diff --git a/network/network_profile_preferences.gd b/network/network_profile_preferences.gd index 2c3924e..5ecb6fd 100644 --- a/network/network_profile_preferences.gd +++ b/network/network_profile_preferences.gd @@ -9,6 +9,7 @@ var profile_id: String = "" var display_name: String = "Player" var voice_id: String = VoiceProfilesType.DEFAULT_ID var speech_speed_id: String = VoiceProfilesType.DEFAULT_SPEED_ID +var call_id: String = VoiceProfilesType.DEFAULT_CALL_ID var created_at_unix: int = 0 var _profile_path := "" var _expected_hash := "" @@ -55,37 +56,43 @@ func load_or_create() -> bool: display_name = "Player" voice_id = VoiceProfilesType.DEFAULT_ID speech_speed_id = VoiceProfilesType.DEFAULT_SPEED_ID + call_id = VoiceProfilesType.DEFAULT_CALL_ID created_at_unix = int(Time.get_unix_time_from_system()) return _save_atomic() func set_display_name(value: String) -> bool: - return set_profile_identity(value, voice_id, speech_speed_id) + return set_profile_identity(value, voice_id, speech_speed_id, call_id) func set_profile_identity( value: String, selected_voice_id: String, selected_speech_speed_id: String, + selected_call_id: String, ) -> bool: var clean_name: String = value.strip_edges() if ( not is_valid_display_name(clean_name) or not VoiceProfilesType.is_valid(selected_voice_id) or not VoiceProfilesType.is_valid_speed(selected_speech_speed_id) + or not VoiceProfilesType.is_valid_call(selected_call_id) ): return false var previous_name: String = display_name var previous_voice_id: String = voice_id var previous_speech_speed_id: String = speech_speed_id + var previous_call_id: String = call_id display_name = clean_name voice_id = selected_voice_id speech_speed_id = selected_speech_speed_id + call_id = selected_call_id if _save_atomic(): return true display_name = previous_name voice_id = previous_voice_id speech_speed_id = previous_speech_speed_id + call_id = previous_call_id return false @@ -140,6 +147,9 @@ func _load_existing() -> bool: speech_speed_id = VoiceProfilesType.sanitized_speed_id( str(data.get("speech_speed_id", VoiceProfilesType.DEFAULT_SPEED_ID)) ) + call_id = VoiceProfilesType.sanitized_call_id( + str(data.get("call_id", VoiceProfilesType.DEFAULT_CALL_ID)) + ) created_at_unix = int(data["created_at_unix"]) return true @@ -151,6 +161,7 @@ func _save_atomic() -> bool: "display_name": display_name, "voice_id": voice_id, "speech_speed_id": speech_speed_id, + "call_id": call_id, "created_at_unix": created_at_unix, } var result := PortableFileGuard.write_guarded( diff --git a/network/network_profile_service.gd b/network/network_profile_service.gd index 4a0a5bd..86b1be1 100644 --- a/network/network_profile_service.gd +++ b/network/network_profile_service.gd @@ -24,6 +24,7 @@ var _spawn_service: PlayerSpawnService var _pending_apply: Dictionary[String, Dictionary] = {} var _pending_voice_ids: Dictionary[String, String] = {} var _pending_speech_speed_ids: Dictionary[String, String] = {} +var _pending_call_ids: Dictionary[String, String] = {} var _host_pending_apply: Dictionary[String, Dictionary] = {} var _latest_check_id: String = "" var _latest_check_name: String = "" @@ -65,6 +66,10 @@ func get_persisted_speech_speed_id() -> String: return _preferences.speech_speed_id +func get_persisted_call_id() -> String: + return _preferences.call_id + + func get_identity_fingerprint() -> String: return ( _session.get_local_identity_fingerprint() @@ -104,6 +109,7 @@ func apply_profile( use_anyway: bool, voice_id: String = VoiceProfilesType.DEFAULT_ID, speech_speed_id: String = VoiceProfilesType.DEFAULT_SPEED_ID, + call_id: String = VoiceProfilesType.DEFAULT_CALL_ID, ) -> bool: var clean_name := display_name.strip_edges() if ( @@ -111,6 +117,7 @@ func apply_profile( or not CharacterCustomizationCatalog.validate_snapshot(appearance) or not VoiceProfilesType.is_valid(voice_id) or not VoiceProfilesType.is_valid_speed(speech_speed_id) + or not VoiceProfilesType.is_valid_call(call_id) ): apply_finished.emit(false, "Check the player name and appearance choices.") return false @@ -129,6 +136,7 @@ func apply_profile( _pending_apply[request_id] = request _pending_voice_ids[request_id] = voice_id _pending_speech_speed_ids[request_id] = speech_speed_id + _pending_call_ids[request_id] = call_id if _session == null or not _session.is_gameplay_session_active(): _apply_local_result(request_id, true, "", false, PackedStringArray()) elif _session.is_host(): @@ -287,12 +295,14 @@ func _apply_local_result( _pending_apply.erase(request_id) _pending_voice_ids.erase(request_id) _pending_speech_speed_ids.erase(request_id) + _pending_call_ids.erase(request_id) conflict_result.emit(request_id, conflict, suggestions) apply_finished.emit(false, message) return var previous_name := _preferences.display_name var previous_voice_id := _preferences.voice_id var previous_speech_speed_id := _preferences.speech_speed_id + var previous_call_id := _preferences.call_id var requested_voice_id: String = _pending_voice_ids.get( request_id, VoiceProfilesType.DEFAULT_ID, @@ -301,14 +311,20 @@ func _apply_local_result( request_id, VoiceProfilesType.DEFAULT_SPEED_ID, ) + var requested_call_id: String = _pending_call_ids.get( + request_id, + VoiceProfilesType.DEFAULT_CALL_ID, + ) if not _preferences.set_profile_identity( str(request["display_name"]), requested_voice_id, requested_speech_speed_id, + requested_call_id, ): _pending_apply.erase(request_id) _pending_voice_ids.erase(request_id) _pending_speech_speed_ids.erase(request_id) + _pending_call_ids.erase(request_id) apply_finished.emit(false, "Profile could not be saved.") return if not _appearance_store.save_snapshot(request["appearance"]): @@ -316,15 +332,18 @@ func _apply_local_result( previous_name, previous_voice_id, previous_speech_speed_id, + previous_call_id, ) _pending_apply.erase(request_id) _pending_voice_ids.erase(request_id) _pending_speech_speed_ids.erase(request_id) + _pending_call_ids.erase(request_id) apply_finished.emit(false, "Profile could not be saved.") return _pending_apply.erase(request_id) _pending_voice_ids.erase(request_id) _pending_speech_speed_ids.erase(request_id) + _pending_call_ids.erase(request_id) if _session != null and _session.is_gameplay_session_active(): if _session.is_host(): _session.apply_canonical_profile( @@ -451,6 +470,7 @@ func _on_session_state_changed(state: NetworkSession.State) -> void: _pending_apply.clear() _pending_voice_ids.clear() _pending_speech_speed_ids.clear() + _pending_call_ids.clear() _host_pending_apply.clear() _latest_check_id = "" _latest_check_name = "" diff --git a/network/network_session.gd b/network/network_session.gd index 27b599f..b747a32 100644 --- a/network/network_session.gd +++ b/network/network_session.gd @@ -8,7 +8,7 @@ const DEFAULT_TRANSPORT_MAX_CLIENTS: int = 31 const CONNECTION_TIMEOUT_SECONDS: float = 10.0 const AUTHENTICATION_TIMEOUT_SECONDS: float = 60.0 const INPUT_INTERVAL: float = 1.0 / 30.0 -const SNAPSHOT_INTERVAL: float = 1.0 / 20.0 +const SNAPSHOT_INTERVAL: float = 1.0 / 30.0 signal state_changed(state: State) signal status_message_changed(message: String) @@ -574,9 +574,13 @@ func apply_canonical_profile( var appearance_changed := _registry.update_appearance(peer_id, appearance) if name_changed: peer_display_name_changed.emit(peer_id, display_name) - if name_changed and appearance_changed: + if appearance_changed: + var avatar: Player = _spawn_service.get_avatar(peer_id) + if avatar != null: + avatar.apply_appearance_snapshot(appearance) + if name_changed or appearance_changed: peer_profile_changed.emit(peer_id, display_name, appearance.duplicate(true)) - return name_changed and appearance_changed + return name_changed or appearance_changed @rpc("any_peer", "call_remote", "reliable", 0) @@ -1018,6 +1022,14 @@ func submit_client_hello(data: Dictionary) -> void: _spawn_service.get_spawn_transform_for_index(spawn_index) ) _spawn_service.spawn_remote_player(sender_id, spawn_transform, true) + var spawned_avatar: Player = _spawn_service.get_avatar(sender_id) + if spawned_avatar != null: + spawned_avatar.apply_appearance_snapshot(submitted_appearance) + peer_profile_changed.emit( + sender_id, + display_name, + submitted_appearance.duplicate(true), + ) receive_server_hello.rpc_id( sender_id, NetworkProtocol.make_server_hello( diff --git a/player/animalese_voice_profiles.gd b/player/animalese_voice_profiles.gd index aa135c1..2ea6cab 100644 --- a/player/animalese_voice_profiles.gd +++ b/player/animalese_voice_profiles.gd @@ -3,6 +3,8 @@ extends RefCounted const DEFAULT_ID: String = "natural" const DEFAULT_SPEED_ID: String = "normal" +const DEFAULT_CALL_ID: String = "meow" +const CALL_AUDIO_DIRECTORY: String = "res://sound/dialogue/calls" const OPTIONS: Array[Dictionary] = [ {"id": "tiny", "label": "tiny", "pitch": 1.24}, {"id": "bright", "label": "bright", "pitch": 1.10}, @@ -17,6 +19,10 @@ const SPEED_OPTIONS: Array[Dictionary] = [ {"id": "quick", "label": "quick", "characters_per_second": 34.0}, {"id": "rapid", "label": "rapid", "characters_per_second": 40.0}, ] +const CALL_OPTIONS: Array[Dictionary] = [ + {"id": "meow", "label": "meow"}, + {"id": "bark", "label": "bark"}, +] static func is_valid(voice_id: String) -> bool: @@ -55,3 +61,21 @@ static func speed_for(speed_id: String) -> float: if str(option.get("id", "")) == resolved_id: return float(option.get("characters_per_second", 28.0)) return 28.0 + + +static func is_valid_call(call_id: String) -> bool: + for option: Dictionary in CALL_OPTIONS: + if str(option.get("id", "")) == call_id: + return true + return false + + +static func sanitized_call_id(call_id: String) -> String: + return call_id if is_valid_call(call_id) else DEFAULT_CALL_ID + + +static func call_audio_path(call_id: String) -> String: + return "%s/%s.wav" % [ + CALL_AUDIO_DIRECTORY, + sanitized_call_id(call_id), + ] diff --git a/player/player.gd b/player/player.gd index 8413be3..301d82c 100644 --- a/player/player.gd +++ b/player/player.gd @@ -237,6 +237,7 @@ var _catch_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04) var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity")) var _camera_dragging: bool = false var _camera_input_enabled: bool = true +var _camera_drag_prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE var _free_camera_active: bool = false var _free_camera_body: CharacterBody3D var _free_camera: Camera3D @@ -272,8 +273,10 @@ var _network_target_position: Vector3 var _network_target_velocity: Vector3 var _network_target_visual_yaw: float = 0.0 var _network_snapshot_ready: bool = false +var _network_snapshot_age: float = 0.0 var _character_animation_name: StringName = &"" var _sitting: bool = false +var _sit_after_landing: bool = false var _sitting_intent_pending: bool = false var _sitting_intent_sequence: int = -1 var _held_fish_visible: bool = false @@ -380,6 +383,9 @@ func _physics_process(delta: float) -> void: or (_network_authoritative_simulation and _network_jump_pending) ) ) + if _sit_after_landing and is_on_floor(): + _sit_after_landing = false + _set_sitting(true, local_control_enabled) if _sitting: if jump_requested: _set_sitting(false, local_control_enabled) @@ -479,12 +485,8 @@ func _process(delta: float) -> void: if _free_camera_active: return - if ( - _camera_input_enabled - and _camera_dragging - and not Input.is_action_pressed("camera_drag") - ): - _camera_dragging = false + if _camera_dragging and not Input.is_action_pressed("camera_drag"): + _set_camera_dragging(false) if _camera_input_enabled: var stick: Vector2 = _get_controller_camera_stick() @@ -639,6 +641,9 @@ func _update_character_animation() -> void: func toggle_sitting() -> void: + if _sit_after_landing: + _sit_after_landing = false + return var should_sit: bool = not _sitting if should_sit: if ( @@ -651,6 +656,9 @@ func toggle_sitting() -> void: ]) ): return + if not is_on_floor(): + _sit_after_landing = true + return _set_sitting(should_sit, local_control_enabled) @@ -692,7 +700,7 @@ func _unhandled_input(event: InputEvent) -> void: return if event.is_action("camera_drag"): - _camera_dragging = event.is_pressed() + _set_camera_dragging(event.is_pressed()) get_viewport().set_input_as_handled() return @@ -742,6 +750,17 @@ func _get_current_speed() -> float: return walk_speed +func _set_camera_dragging(active: bool) -> void: + if _camera_dragging == active: + return + _camera_dragging = active + if active: + _camera_drag_prior_mouse_mode = Input.mouse_mode + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + elif Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: + Input.mouse_mode = _camera_drag_prior_mouse_mode + + func _get_network_aware_speed() -> float: if local_control_enabled: return _get_current_speed() @@ -801,7 +820,7 @@ func _set_free_camera_active(active: bool) -> void: _free_camera_active = true return _free_camera_active = false - _camera_dragging = false + _set_camera_dragging(false) if _free_camera != null: _free_camera.current = false _free_camera = null @@ -878,7 +897,7 @@ func set_local_control(enabled: bool) -> void: if is_node_ready(): _camera.current = enabled if not enabled: - _camera_dragging = false + _set_camera_dragging(false) func set_network_peer_id(peer_id: int) -> void: @@ -894,6 +913,7 @@ func configure_network_remote(authoritative_simulation: bool) -> void: _network_authoritative_simulation = authoritative_simulation _network_interpolation_enabled = not authoritative_simulation _network_snapshot_ready = false + _network_snapshot_age = 0.0 _camera.current = false @@ -943,7 +963,13 @@ func apply_authoritative_network_input(data: Dictionary) -> void: _network_sprint = bool(data.get("sprint", false)) _network_sneak = bool(data.get("sneak", false)) _network_slow_walk = bool(data.get("slow_walk", false)) - _set_sitting(bool(data.get("sitting", false))) + var sitting_requested: bool = bool(data.get("sitting", false)) + if sitting_requested and not is_on_floor(): + _sit_after_landing = true + _set_sitting(false) + else: + _sit_after_landing = false + _set_sitting(sitting_requested) func make_network_snapshot(peer_id: int) -> Dictionary: @@ -965,6 +991,7 @@ func push_network_snapshot(snapshot: Dictionary) -> void: _network_target_position = parsed["position"] _network_target_velocity = parsed["velocity"] _network_target_visual_yaw = parsed["visual_yaw"] + _network_snapshot_age = 0.0 _set_sitting(bool(parsed["sitting"])) if not _network_snapshot_ready: global_position = _network_target_position @@ -1009,6 +1036,7 @@ func apply_network_teleport(snapshot: Dictionary) -> void: _network_target_position = global_position _network_target_velocity = velocity _network_target_visual_yaw = _visuals.rotation.y + _network_snapshot_age = 0.0 _network_snapshot_ready = true @@ -1065,11 +1093,21 @@ func _parse_network_snapshot(snapshot: Dictionary) -> Dictionary: func _update_network_interpolation(delta: float) -> void: if not _network_snapshot_ready: return - var position_weight: float = 1.0 - exp(-12.0 * delta) - global_position = global_position.lerp( - _network_target_position, - position_weight + _network_snapshot_age = minf(_network_snapshot_age + delta, 0.1) + var predicted_position: Vector3 = ( + _network_target_position + + _network_target_velocity * _network_snapshot_age ) + var projected_position: Vector3 = ( + global_position + _network_target_velocity * delta + ) + if projected_position.distance_to(predicted_position) > 2.0: + global_position = predicted_position + else: + global_position = projected_position.lerp( + predicted_position, + 1.0 - exp(-10.0 * delta) + ) velocity = _network_target_velocity _visuals.rotation.y = lerp_angle( _visuals.rotation.y, @@ -1096,7 +1134,7 @@ func is_movement_enabled() -> bool: func set_camera_input_enabled(enabled: bool) -> void: _camera_input_enabled = enabled if not enabled: - _camera_dragging = false + _set_camera_dragging(false) func set_camera_active(active: bool) -> void: @@ -1469,7 +1507,10 @@ func _complete_showcase_restore( _camera_input_enabled = ( _showcase_camera_snapshot.camera_input_enabled ) - _camera_dragging = _showcase_camera_snapshot.camera_dragging + _set_camera_dragging( + _showcase_camera_snapshot.camera_dragging + and Input.is_action_pressed("camera_drag") + ) _showcase_camera_snapshot = null if restored_callback.is_valid(): restored_callback.call() @@ -1487,7 +1528,7 @@ func _capture_showcase_camera_snapshot() -> void: _showcase_camera_snapshot.camera_input_enabled = _camera_input_enabled _showcase_camera_snapshot.camera_dragging = _camera_dragging _camera_input_enabled = false - _camera_dragging = false + _set_camera_dragging(false) func _begin_showcase_camera_transition() -> Vector3: diff --git a/project.godot b/project.godot index 9ddaa7f..3c43ed2 100644 --- a/project.godot +++ b/project.godot @@ -185,6 +185,11 @@ open_emotes={ , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":11,"pressure":0.0,"pressed":false,"script":null) ] } +character_call={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":71,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} open_quick_actions={ "deadzone": 0.2, "events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null) diff --git a/scripts/portmaster/licenses/CREDITS.md b/scripts/portmaster/licenses/CREDITS.md index 8edbd4f..5b78a80 100755 --- a/scripts/portmaster/licenses/CREDITS.md +++ b/scripts/portmaster/licenses/CREDITS.md @@ -37,6 +37,10 @@ written with or without assistance. - “Gentle Ocean Waves Loop” by kkenny101, used for the saltwater shoreline ambience. Freesound sound 852826, Creative Commons Zero (CC0): https://freesound.org/s/852826/ +- Dog_Bark.wav by ivolipa -- https://freesound.org/s/328729/ -- License: + Creative Commons 0 +- Cat meow.m4a by Christyboy100 -- https://freesound.org/s/495694/ -- License: + Attribution 3.0 - “Quick Water Droplet” by qubodup, used for the bobber water-impact sound. Freesound sound 792931, Creative Commons Zero (CC0): https://freesound.org/s/792931/ diff --git a/settings/player_settings.gd b/settings/player_settings.gd index 8979e57..a20a3e1 100644 --- a/settings/player_settings.gd +++ b/settings/player_settings.gd @@ -7,6 +7,8 @@ const MIN_MOUSE_SENSITIVITY: float = 0.001 const MAX_MOUSE_SENSITIVITY: float = 0.012 const MIN_CONTROLLER_SENSITIVITY: float = 0.5 const MAX_CONTROLLER_SENSITIVITY: float = 5.0 +const MIN_AUDIO_VOLUME: float = 0.0 +const MAX_AUDIO_VOLUME: float = 1.0 const MIN_WORLD_PIXEL_SIZE: int = 1 const MAX_WORLD_PIXEL_SIZE: int = 5 const DEFAULT_WORLD_PIXEL_SIZE: int = 3 @@ -33,6 +35,11 @@ const UI_COMPACT_RENDER_HEIGHTS: Array[int] = [0, 408, 336, 264, 192] @export var chat_dock_right: bool = false @export var chat_mobile_mode: bool = false @export var paint_dock_right: bool = true +@export var fullscreen_enabled: bool = false +@export_range(0.0, 1.0, 0.01) var master_volume: float = 1.0 +@export_range(0.0, 1.0, 0.01) var music_volume: float = 1.0 +@export_range(0.0, 1.0, 0.01) var effects_volume: float = 1.0 +@export_range(0.0, 1.0, 0.01) var environment_volume: float = 1.0 @export_range(1, 5, 1) var world_pixel_size: int = DEFAULT_WORLD_PIXEL_SIZE @export_range(1, 5, 1) var ui_pixel_size: int = DEFAULT_UI_PIXEL_SIZE @@ -48,6 +55,10 @@ func is_valid() -> bool: and is_finite(controller_camera_sensitivity) and controller_camera_sensitivity >= MIN_CONTROLLER_SENSITIVITY and controller_camera_sensitivity <= MAX_CONTROLLER_SENSITIVITY + and _is_valid_audio_volume(master_volume) + and _is_valid_audio_volume(music_volume) + and _is_valid_audio_volume(effects_volume) + and _is_valid_audio_volume(environment_volume) and chat_draft.length() <= MAX_CHAT_DRAFT_CHARACTERS and world_pixel_size >= MIN_WORLD_PIXEL_SIZE and world_pixel_size <= MAX_WORLD_PIXEL_SIZE @@ -70,11 +81,24 @@ func copy() -> PlayerSettings: result.chat_dock_right = chat_dock_right result.chat_mobile_mode = chat_mobile_mode result.paint_dock_right = paint_dock_right + result.fullscreen_enabled = fullscreen_enabled + result.master_volume = master_volume + result.music_volume = music_volume + result.effects_volume = effects_volume + result.environment_volume = environment_volume result.world_pixel_size = world_pixel_size result.ui_pixel_size = ui_pixel_size return result +static func _is_valid_audio_volume(value: float) -> bool: + return ( + is_finite(value) + and value >= MIN_AUDIO_VOLUME + and value <= MAX_AUDIO_VOLUME + ) + + static func get_world_grid_height( pixel_size: int, displayed_height: int, diff --git a/settings/player_settings_manager.gd b/settings/player_settings_manager.gd index a983ca3..84b3f44 100644 --- a/settings/player_settings_manager.gd +++ b/settings/player_settings_manager.gd @@ -5,6 +5,7 @@ const SETTINGS_VERSION: int = 1 const SETTINGS_PATH: String = "user://player_settings.json" const TEMP_PATH: String = "user://player_settings.json.tmp" const BACKUP_PATH: String = "user://player_settings.json.backup" +const SILENT_VOLUME_DB: float = -80.0 signal settings_changed(settings: PlayerSettings) @@ -16,12 +17,14 @@ func load_settings() -> bool: _recover_interrupted_write() if not FileAccess.file_exists(SETTINGS_PATH): current_settings = PlayerSettings.new() + _apply_audio_settings(current_settings) emit_signal("settings_changed", current_settings) return true var file := FileAccess.open(SETTINGS_PATH, FileAccess.READ) if file == null: push_warning("Unable to open player settings; using defaults.") current_settings = PlayerSettings.new() + _apply_audio_settings(current_settings) emit_signal("settings_changed", current_settings) return false var json := JSON.new() @@ -42,6 +45,9 @@ func load_settings() -> bool: var presentation: Dictionary = {} if typeof(data.get("presentation")) == TYPE_DICTIONARY: presentation = data["presentation"] + var audio: Dictionary = {} + if typeof(data.get("audio")) == TYPE_DICTIONARY: + audio = data["audio"] if ( typeof(accessibility.get("auto_click_enabled")) != TYPE_BOOL or typeof(camera.get("invert_vertical")) != TYPE_BOOL @@ -73,6 +79,10 @@ func load_settings() -> bool: presentation.has("paint_dock_right") and typeof(presentation["paint_dock_right"]) != TYPE_BOOL ) + or ( + presentation.has("fullscreen_enabled") + and typeof(presentation["fullscreen_enabled"]) != TYPE_BOOL + ) ): return _use_defaults_after_corruption("Player settings values are invalid.") var loaded := PlayerSettings.new() @@ -122,10 +132,20 @@ func load_settings() -> bool: presentation.get("chat_mobile_mode", false) ) loaded.paint_dock_right = bool(presentation.get("paint_dock_right", true)) + loaded.fullscreen_enabled = bool( + presentation.get("fullscreen_enabled", false) + ) + loaded.master_volume = _read_float(audio.get("master", 1.0), 1.0) + loaded.music_volume = _read_float(audio.get("music", 1.0), 1.0) + loaded.effects_volume = _read_float(audio.get("effects", 1.0), 1.0) + loaded.environment_volume = _read_float( + audio.get("environment", 1.0), 1.0 + ) if not loaded.is_valid(): return _use_defaults_after_corruption("Player settings values are invalid.") current_settings = loaded _is_dirty = false + _apply_audio_settings(current_settings) emit_signal("settings_changed", current_settings) return true @@ -138,7 +158,9 @@ func apply_settings(settings: PlayerSettings) -> bool: _is_dirty = true if not save_now(): current_settings = previous + _apply_audio_settings(current_settings) return false + _apply_audio_settings(current_settings) emit_signal("settings_changed", current_settings) return true @@ -192,6 +214,12 @@ func save_now() -> bool: "controller_sensitivity": current_settings.controller_camera_sensitivity, "invert_vertical": current_settings.invert_camera_y, }, + "audio": { + "master": current_settings.master_volume, + "music": current_settings.music_volume, + "effects": current_settings.effects_volume, + "environment": current_settings.environment_volume, + }, "presentation": { "world_pixel_size": current_settings.world_pixel_size, "ui_pixel_size": current_settings.ui_pixel_size, @@ -200,6 +228,7 @@ func save_now() -> bool: "chat_dock_right": current_settings.chat_dock_right, "chat_mobile_mode": current_settings.chat_mobile_mode, "paint_dock_right": current_settings.paint_dock_right, + "fullscreen_enabled": current_settings.fullscreen_enabled, }, } if current_settings.chat_draft.is_empty(): @@ -232,6 +261,22 @@ func save_if_dirty() -> bool: return not _is_dirty or save_now() +func preview_audio_levels( + master: float, + music: float, + effects: float, + environment: float, +) -> void: + _set_bus_volume(&"Master", master) + _set_bus_volume(&"Music", music) + _set_bus_volume(&"SFX", effects) + _set_bus_volume(&"Environment", environment) + + +func restore_audio_levels() -> void: + _apply_audio_settings(current_settings) + + func _exit_tree() -> void: save_if_dirty() @@ -240,10 +285,38 @@ func _use_defaults_after_corruption(message: String) -> bool: push_warning(message) current_settings = PlayerSettings.new() _is_dirty = false + _apply_audio_settings(current_settings) emit_signal("settings_changed", current_settings) return false +func _apply_audio_settings(settings: PlayerSettings) -> void: + preview_audio_levels( + settings.master_volume, + settings.music_volume, + settings.effects_volume, + settings.environment_volume, + ) + + +func _set_bus_volume(bus_name: StringName, linear_volume: float) -> void: + var bus_index: int = AudioServer.get_bus_index(bus_name) + if bus_index < 0: + return + var resolved_volume: float = clampf( + linear_volume, + PlayerSettings.MIN_AUDIO_VOLUME, + PlayerSettings.MAX_AUDIO_VOLUME, + ) + AudioServer.set_bus_mute(bus_index, resolved_volume <= 0.0) + AudioServer.set_bus_volume_db( + bus_index, + SILENT_VOLUME_DB + if resolved_volume <= 0.0 + else linear_to_db(resolved_volume), + ) + + func _recover_interrupted_write() -> void: if FileAccess.file_exists(SETTINGS_PATH): _remove_if_present(TEMP_PATH) diff --git a/sound/dialogue/calls/bark.wav b/sound/dialogue/calls/bark.wav new file mode 100644 index 0000000..2629fe7 Binary files /dev/null and b/sound/dialogue/calls/bark.wav differ diff --git a/sound/dialogue/calls/bark.wav.import b/sound/dialogue/calls/bark.wav.import new file mode 100644 index 0000000..702904d --- /dev/null +++ b/sound/dialogue/calls/bark.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://c8x46srpghnil" +path="res://.godot/imported/bark.wav-9da6fd5d720375f4a64b26e364fcd74c.sample" + +[deps] + +source_file="res://sound/dialogue/calls/bark.wav" +dest_files=["res://.godot/imported/bark.wav-9da6fd5d720375f4a64b26e364fcd74c.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 diff --git a/sound/dialogue/calls/meow.wav b/sound/dialogue/calls/meow.wav new file mode 100644 index 0000000..662b416 Binary files /dev/null and b/sound/dialogue/calls/meow.wav differ diff --git a/sound/dialogue/calls/meow.wav.import b/sound/dialogue/calls/meow.wav.import new file mode 100644 index 0000000..058b09a --- /dev/null +++ b/sound/dialogue/calls/meow.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://y86nskf7ewrg" +path="res://.godot/imported/meow.wav-eb7bd4377946745ae1566c72ca026fae.sample" + +[deps] + +source_file="res://sound/dialogue/calls/meow.wav" +dest_files=["res://.godot/imported/meow.wav-eb7bd4377946745ae1566c72ca026fae.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 diff --git a/tests/shoreline_ambience_validation.gd b/tests/shoreline_ambience_validation.gd index 505452a..4297d72 100644 --- a/tests/shoreline_ambience_validation.gd +++ b/tests/shoreline_ambience_validation.gd @@ -34,7 +34,7 @@ func _run() -> void: runtime_audio.owner = controller root.add_child(controller) await process_frame - assert(runtime_audio.bus == &"SFX") + assert(runtime_audio.bus == &"Environment") assert(controller.near_distance < controller.far_distance) assert(controller.near_volume_db > controller.far_volume_db) var runtime_waves := runtime_audio.stream as AudioStreamWAV diff --git a/ui/animalese_voice.gd b/ui/animalese_voice.gd index b963a47..c6372ee 100644 --- a/ui/animalese_voice.gd +++ b/ui/animalese_voice.gd @@ -24,6 +24,7 @@ func _ready() -> void: polyphonic_stream.polyphony = POLYPHONY _player = AudioStreamPlayer.new() _player.name = "AnimaleseAudio" + _player.bus = &"SFX" _player.stream = polyphonic_stream add_child(_player) _player.play() diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index 69d35e9..3a18f25 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -215,9 +215,9 @@ func open_chat() -> void: _session.submit_neutral_local_movement() _entry.show() _entry.virtual_keyboard_enabled = false - _entry.grab_focus() _hint.hide() _refresh_input_ownership() + call_deferred("_focus_entry_after_open") _update_panel_opacity(true) Input.mouse_mode = Input.MOUSE_MODE_VISIBLE @@ -304,6 +304,13 @@ func request_virtual_keyboard() -> bool: return true +func _focus_entry_after_open() -> void: + if not _opened or not _entry.visible: + return + _entry.grab_focus() + _refresh_input_ownership() + + func is_open() -> bool: return _opened @@ -314,7 +321,13 @@ func is_collapsed() -> bool: func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("open_chat"): - toggle_chat() + if event is InputEventKey: + if _opened: + _focus_entry_after_open() + else: + open_chat() + else: + toggle_chat() get_viewport().set_input_as_handled() return if event is InputEventKey and event.pressed and not event.echo: @@ -796,7 +809,7 @@ func _handle_time_command(parts: PackedStringArray) -> void: "World time set to %s (%s)." % [phase_name, _world_time.get_clock_text()] ) - close_chat() + call_deferred("close_chat") func _on_local_message_confirmed(message: Dictionary) -> void: @@ -811,7 +824,7 @@ func _on_local_message_confirmed(message: Dictionary) -> void: _entry.clear() _set_status("") _flush_draft() - close_chat() + call_deferred("close_chat") func _on_message(message: Dictionary) -> void: diff --git a/ui/fishing_shop.gd b/ui/fishing_shop.gd index 03fdf16..12234c6 100644 --- a/ui/fishing_shop.gd +++ b/ui/fishing_shop.gd @@ -50,7 +50,7 @@ enum ShopSection { const SHOP_SECTION_LABELS: Array[String] = [ "Upgrades", - "Bait", + "Bait and Lures", "Snacks", "Equipment", "Art Supplies", @@ -143,13 +143,19 @@ func _focus_buy_page() -> void: func _build_shop_tabs() -> void: + _shop_tab_bar.custom_minimum_size.y = 44.0 + _shop_tab_bar.show() for section_index: int in range(ShopSection.size()): var tab: OrganizerTab = OrganizerTabType.new() tab.text = SHOP_SECTION_LABELS[section_index] tab.palette_index = section_index + tab.custom_minimum_size = Vector2(132.0, 42.0) tab.size_flags_horizontal = Control.SIZE_EXPAND_FILL + tab.focus_mode = Control.FOCUS_ALL + tab.mouse_filter = Control.MOUSE_FILTER_STOP tab.pressed.connect(_select_shop_section.bind(section_index, true)) _shop_tab_bar.add_child(tab) + tab.show() _shop_tabs.append(tab) _select_shop_section(ShopSection.UPGRADES, false) @@ -287,6 +293,7 @@ func open_shop() -> bool: _feedback.text = "" deactivate_shop_cooler_page() show() + _shop_tab_bar.show() _select_shop_section(ShopSection.UPGRADES, false) _refresh_all() _buy_mode_button.grab_focus() @@ -312,6 +319,7 @@ func get_shop_cooler_mount() -> Control: func activate_shop_cooler_page() -> void: + _shop_tab_bar.hide() _cooler_page_active = true _shop_panel.hide() _shop_cooler_page.show() @@ -319,6 +327,8 @@ func activate_shop_cooler_page() -> void: func deactivate_shop_cooler_page() -> void: + if visible: + _shop_tab_bar.show() _cooler_page_active = false _cooler_modal_open = false _back_to_shop_button.disabled = false @@ -468,10 +478,32 @@ func _refresh_supplies() -> void: if _shop_section == ShopSection.UPGRADES: return _stock_title.text = SHOP_SECTION_LABELS[int(_shop_section)] - for item_id: StringName in FishingShopStockType.get_stock_item_ids(): + var stock_item_ids: Array[StringName] = ( + FishingShopStockType.get_stock_item_ids() + ) + if _shop_section == ShopSection.BAIT: + var grouped_item_ids: Array[StringName] = [] + for item_id: StringName in stock_item_ids: + var bait_item: ItemDataType = _item_catalog.get_item_by_id(item_id) + if bait_item != null and bait_item.is_bait(): + grouped_item_ids.append(item_id) + for item_id: StringName in stock_item_ids: + var lure_item: ItemDataType = _item_catalog.get_item_by_id(item_id) + if lure_item != null and lure_item.is_lure(): + grouped_item_ids.append(item_id) + stock_item_ids = grouped_item_ids + var current_stock_group: StringName = StringName() + for item_id: StringName in stock_item_ids: var item: ItemDataType = _item_catalog.get_item_by_id(item_id) if item == null or not _item_belongs_in_current_section(item): continue + if _shop_section == ShopSection.BAIT: + var stock_group: StringName = ( + &"bait" if item.is_bait() else &"lures" + ) + if stock_group != current_stock_group: + _add_stock_section(str(stock_group)) + current_stock_group = stock_group var button := Button.new() button.custom_minimum_size = Vector2(195, 54) button.icon = item.icon @@ -563,14 +595,11 @@ func _refresh_supplies() -> void: func _item_belongs_in_current_section(item: ItemDataType) -> bool: match _shop_section: ShopSection.BAIT: - return item.is_bait() + return item.is_bait() or item.is_lure() ShopSection.SNACKS: return item.category == ItemDataType.Category.CONSUMABLE ShopSection.EQUIPMENT: - return item.category in [ - ItemDataType.Category.TOOL, - ItemDataType.Category.LURE, - ] + return item.category == ItemDataType.Category.TOOL return false diff --git a/ui/fishing_shop.tscn b/ui/fishing_shop.tscn index 5732baf..ef10a3c 100644 --- a/ui/fishing_shop.tscn +++ b/ui/fishing_shop.tscn @@ -64,6 +64,7 @@ offset_bottom = 104.0 text = "back to shop" [node name="ShopPanel" type="PanelContainer" parent="."] +z_index = 2 unique_name_in_owner = true layout_mode = 1 anchors_preset = 8 @@ -143,9 +144,13 @@ text = "" horizontal_alignment = 1 theme_override_colors/font_color = Color(1, 0.82, 0.4, 1) -[node name="ShopTabBar" type="HBoxContainer" parent="ShopPanel/Margin/Layout"] +[node name="ShopTabBar" type="HBoxContainer" parent="."] unique_name_in_owner = true -layout_mode = 2 +z_index = 1 +offset_left = 298.0 +offset_top = 26.0 +offset_right = 982.0 +offset_bottom = 70.0 alignment = 1 theme_override_constants/separation = 6 diff --git a/ui/game_ui.gd b/ui/game_ui.gd index c2dfca2..5e1c19a 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -93,6 +93,7 @@ const FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET: float = -104.0 @onready var _status_label: Label = %StatusLabel @onready var _bite_prompt_button: Button = %BitePromptButton @onready var _gameplay_transient_hud: Control = %GameplayTransientHUD +@onready var _active_bait_indicator: Control = %ActiveBaitIndicator @onready var _active_bait_button: Button = %ActiveBaitButton @onready var _active_bait_quantity_badge: Panel = %ActiveBaitQuantityBadge @onready var _active_bait_quantity: Label = %ActiveBaitQuantity @@ -114,9 +115,13 @@ const FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET: float = -104.0 @onready var _experience_bubble_label: Label = %ExperienceBubbleLabel const TypewriterRevealType = preload("res://ui/typewriter_reveal.gd") const AnimaleseVoiceType = preload("res://ui/animalese_voice.gd") +const VoiceProfilesType = preload( + "res://player/animalese_voice_profiles.gd" +) const SHOP_ANIMALESE_VOICE_ID: String = "natural" const SHOP_ANIMALESE_BASE_PITCH: float = 1.08 const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0 +const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000 @onready var _canonical_stage: Control = %CanonicalStage @@ -136,6 +141,7 @@ const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0 var _shop_animalese_voice: AnimaleseVoiceType var _shop_npc_player_in_range: bool = false var _shop_npc_spoken_for_current_visit: bool = false +var _shop_npc_next_speech_msec: int = 0 @onready var _effect_status: Label = %EffectStatus @onready var _chat_ui: ChatUIType = %ChatUI @onready var _emote_radial_menu: EmoteRadialMenuType = %EmoteRadialMenu @@ -155,6 +161,9 @@ var _shop_npc_spoken_for_current_visit: bool = false var _showcase_active: bool = false var _player: PlayerType +var _network_chat_service: NetworkChatService +var _network_profile: NetworkProfilePreferences +var _spawn_service: PlayerSpawnService var _bag: PlayerBagType var _item_catalog: ItemCatalogType var _player_menu_open: bool = false @@ -279,6 +288,9 @@ func setup( world_sun: DirectionalLight3D, ) -> void: _player = player + _network_chat_service = network_chat_service + _network_profile = network_profile + _spawn_service = spawn_service _bag = bag _item_catalog = item_catalog _settings_manager = settings_manager @@ -300,6 +312,15 @@ func setup( ): _bag.contents_changed.connect(_on_hud_bait_inventory_changed) _refresh_active_bait_indicator() + if ( + _network_chat_service != null + and not _network_chat_service.character_call_received.is_connected( + _on_character_call_received + ) + ): + _network_chat_service.character_call_received.connect( + _on_character_call_received + ) if ( _experience != null and not _experience.experience_awarded.is_connected( @@ -411,6 +432,12 @@ func _input(event: InputEvent) -> void: and _surface_drawing_toolbar.owns_pointer_event(event) ): return + if event.is_action_pressed("character_call") and _can_use_character_call(): + _network_chat_service.send_local_character_call( + _network_profile.call_id + ) + get_viewport().set_input_as_handled() + return var drawing_can_open: bool = ( _gameplay_ui_enabled and not _system_menu_open @@ -479,6 +506,49 @@ func _input(event: InputEvent) -> void: get_viewport().set_input_as_handled() +func _can_use_character_call() -> bool: + return ( + _gameplay_ui_enabled + and not _system_menu_open + and not _player_menu_open + and not _shop_open + and not _chat_input_open + and not _showcase_active + and not _virtual_mouse_active + and _network_chat_service != null + and _network_profile != null + ) + + +func _on_character_call_received( + peer_id: int, + call_id: String, + pitch_scale: float, +) -> void: + if _spawn_service == null: + return + var avatar: PlayerType = _spawn_service.get_avatar(peer_id) + var audio_path: String = VoiceProfilesType.call_audio_path(call_id) + if avatar == null or not ResourceLoader.exists(audio_path, "AudioStream"): + return + var stream: AudioStream = load(audio_path) as AudioStream + if stream == null: + return + var call_player := AudioStreamPlayer3D.new() + call_player.name = "CharacterCall" + call_player.bus = &"SFX" + call_player.stream = stream + call_player.pitch_scale = pitch_scale + call_player.max_distance = 28.0 + call_player.unit_size = 4.0 + call_player.position = ( + Vector3.UP * 1.25 * avatar.get_character_visual_scale() + ) + avatar.add_child(call_player) + call_player.finished.connect(call_player.queue_free) + call_player.play() + + func _handle_controller_chat_controls(event: InputEvent) -> bool: var button_event: InputEventJoypadButton = event as InputEventJoypadButton if ( @@ -1274,6 +1344,7 @@ func _apply_active_bait_indicator_style() -> void: func _refresh_active_bait_indicator() -> void: + _refresh_active_bait_indicator_visibility() if not is_node_ready(): return var item: ItemDataType @@ -1313,12 +1384,22 @@ func _on_hud_active_bait_changed(_item_id: StringName) -> void: _refresh_active_bait_indicator() +func _refresh_active_bait_indicator_visibility() -> void: + _active_bait_indicator.visible = ( + _gameplay_ui_enabled + and not _system_menu_open + and not _player_menu_open + and not _shop_open + ) + + func _on_hud_bait_inventory_changed() -> void: _refresh_active_bait_indicator() func set_gameplay_ui_enabled(enabled: bool) -> void: _gameplay_ui_enabled = enabled + _refresh_active_bait_indicator_visibility() if enabled: _try_start_shop_npc_speech() _gameplay_transient_hud.visible = enabled and not _player_menu_open @@ -1411,6 +1492,12 @@ func _try_start_shop_npc_speech() -> void: ): return _shop_npc_spoken_for_current_visit = true + var now_msec: int = Time.get_ticks_msec() + if now_msec < _shop_npc_next_speech_msec: + return + _shop_npc_next_speech_msec = ( + now_msec + SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS + ) _ensure_shop_animalese_voice() TypewriterRevealType.start( _shop_prompt_message, @@ -2132,6 +2219,7 @@ func _refresh_hotbar_visibility() -> void: func _emit_interactive_pointer_ui_changed() -> void: + _refresh_active_bait_indicator_visibility() interactive_pointer_ui_changed.emit( _system_menu_open or _player_menu_open or _shop_open or _chat_input_open ) diff --git a/ui/profile_page.gd b/ui/profile_page.gd index 4a9f074..fe3e719 100644 --- a/ui/profile_page.gd +++ b/ui/profile_page.gd @@ -24,6 +24,8 @@ var _draft_voice_id: String = VoiceProfilesType.DEFAULT_ID var _persisted_voice_id: String = VoiceProfilesType.DEFAULT_ID var _draft_speech_speed_id: String = VoiceProfilesType.DEFAULT_SPEED_ID var _persisted_speech_speed_id: String = VoiceProfilesType.DEFAULT_SPEED_ID +var _draft_call_id: String = VoiceProfilesType.DEFAULT_CALL_ID +var _persisted_call_id: String = VoiceProfilesType.DEFAULT_CALL_ID var _category_id: String = "species" var _dirty: bool = false var _allow_duplicate: bool = false @@ -478,45 +480,54 @@ func _category_label(category_id: String) -> String: func _build_voice_options() -> void: var description := Label.new() description.text = "Choose how your character sounds in chat." + description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + description.size_flags_horizontal = Control.SIZE_EXPAND_FILL + description.add_theme_font_size_override("font_size", 12) description.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) _option_list.add_child(description) var grid := GridContainer.new() - grid.columns = 2 - grid.add_theme_constant_override("h_separation", 8) - grid.add_theme_constant_override("v_separation", 8) + grid.columns = 3 + grid.add_theme_constant_override("h_separation", 6) + grid.add_theme_constant_override("v_separation", 4) + grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL _option_list.add_child(grid) for option: Dictionary in VoiceProfilesType.OPTIONS: var option_id := str(option.get("id", "")) var button := Button.new() button.text = str(option.get("label", option_id)) button.toggle_mode = true - button.custom_minimum_size = Vector2(170.0, 42.0) + button.custom_minimum_size = Vector2(108.0, 32.0) + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.button_pressed = _draft_voice_id == option_id button.pressed.connect(_select_voice_option.bind(option_id)) UtilityPageStyle.apply_compact_ocean_button(button) grid.add_child(button) var divider := HSeparator.new() - divider.custom_minimum_size.y = 8.0 + divider.custom_minimum_size.y = 2.0 _option_list.add_child(divider) var speed_title := Label.new() speed_title.text = "speech speed" - speed_title.add_theme_font_size_override("font_size", 16) + speed_title.add_theme_font_size_override("font_size", 13) speed_title.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY ) _option_list.add_child(speed_title) var speed_description := Label.new() - speed_description.text = "Controls player chat speech on this device only." + speed_description.text = "Controls chat speech on this device only." + speed_description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + speed_description.size_flags_horizontal = Control.SIZE_EXPAND_FILL + speed_description.add_theme_font_size_override("font_size", 12) speed_description.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) _option_list.add_child(speed_description) var speed_grid := GridContainer.new() - speed_grid.columns = 2 - speed_grid.add_theme_constant_override("h_separation", 8) - speed_grid.add_theme_constant_override("v_separation", 8) + speed_grid.columns = 3 + speed_grid.add_theme_constant_override("h_separation", 6) + speed_grid.add_theme_constant_override("v_separation", 4) + speed_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL _option_list.add_child(speed_grid) for option: Dictionary in VoiceProfilesType.SPEED_OPTIONS: var speed_id := str(option.get("id", "")) @@ -526,13 +537,49 @@ func _build_voice_options() -> void: float(option.get("characters_per_second", 28.0)) ) speed_button.toggle_mode = true - speed_button.custom_minimum_size = Vector2(170.0, 38.0) + speed_button.custom_minimum_size = Vector2(108.0, 32.0) + speed_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL speed_button.button_pressed = _draft_speech_speed_id == speed_id speed_button.pressed.connect( _select_speech_speed_option.bind(speed_id) ) UtilityPageStyle.apply_compact_ocean_button(speed_button) speed_grid.add_child(speed_button) + var call_divider := HSeparator.new() + call_divider.custom_minimum_size.y = 2.0 + _option_list.add_child(call_divider) + var call_title := Label.new() + call_title.text = "call" + call_title.add_theme_font_size_override("font_size", 13) + call_title.add_theme_color_override( + "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY + ) + _option_list.add_child(call_title) + var call_description := Label.new() + call_description.text = "Press G to make this sound." + call_description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + call_description.size_flags_horizontal = Control.SIZE_EXPAND_FILL + call_description.add_theme_font_size_override("font_size", 12) + call_description.add_theme_color_override( + "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY + ) + _option_list.add_child(call_description) + var call_grid := GridContainer.new() + call_grid.columns = 2 + call_grid.add_theme_constant_override("h_separation", 8) + call_grid.add_theme_constant_override("v_separation", 8) + _option_list.add_child(call_grid) + for option: Dictionary in VoiceProfilesType.CALL_OPTIONS: + var call_id := str(option.get("id", "")) + var call_button := Button.new() + call_button.text = str(option.get("label", call_id)) + call_button.toggle_mode = true + call_button.custom_minimum_size = Vector2(108.0, 32.0) + call_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + call_button.button_pressed = _draft_call_id == call_id + call_button.pressed.connect(_select_call_option.bind(call_id)) + UtilityPageStyle.apply_compact_ocean_button(call_button) + call_grid.add_child(call_button) func _select_voice_option(voice_id: String) -> void: @@ -555,6 +602,15 @@ func _select_speech_speed_option(speed_id: String) -> void: _play_voice_preview() +func _select_call_option(call_id: String) -> void: + if not VoiceProfilesType.is_valid_call(call_id): + return + _draft_call_id = call_id + _dirty = _draft_differs() + _refresh_options() + _refresh_actions() + + func _play_voice_preview() -> void: if _voice_preview_tween != null and _voice_preview_tween.is_valid(): _voice_preview_tween.kill() @@ -982,6 +1038,7 @@ func _apply() -> void: _allow_duplicate, _draft_voice_id, _draft_speech_speed_id, + _draft_call_id, ) @@ -1001,10 +1058,12 @@ func _load_persisted() -> void: _persisted_speech_speed_id = ( _service.get_persisted_speech_speed_id() ) + _persisted_call_id = _service.get_persisted_call_id() _draft_name = _persisted_name _draft_appearance = _persisted_appearance.duplicate(true) _draft_voice_id = _persisted_voice_id _draft_speech_speed_id = _persisted_speech_speed_id + _draft_call_id = _persisted_call_id TypewriterRevealType.set_characters_per_second( VoiceProfilesType.speed_for(_persisted_speech_speed_id) ) @@ -1045,6 +1104,7 @@ func _confirm_pending_action() -> void: _draft_appearance = CharacterCustomizationCatalog.default_snapshot() _draft_voice_id = VoiceProfilesType.DEFAULT_ID _draft_speech_speed_id = VoiceProfilesType.DEFAULT_SPEED_ID + _draft_call_id = VoiceProfilesType.DEFAULT_CALL_ID _preview.apply_appearance_profile(_draft_appearance) _dirty = _draft_differs() _refresh_options() @@ -1060,6 +1120,7 @@ func _draft_differs() -> bool: or _draft_appearance != _persisted_appearance or _draft_voice_id != _persisted_voice_id or _draft_speech_speed_id != _persisted_speech_speed_id + or _draft_call_id != _persisted_call_id ) diff --git a/ui/settings/settings_bubble_page.gd b/ui/settings/settings_bubble_page.gd index 4710be9..dd01309 100644 --- a/ui/settings/settings_bubble_page.gd +++ b/ui/settings/settings_bubble_page.gd @@ -19,7 +19,7 @@ const ControllerFocusNavigationType = preload( var _cluster: BubbleCluster var _bubbles: Array[BubbleButton] = [] -var _focus_bubbles: Array[BubbleButton] = [] +var _focus_controls: Array[Control] = [] var _transition: Tween var _transition_generation: int = 0 var _resting_cluster_position: Vector2 = Vector2.ZERO @@ -33,9 +33,9 @@ func _ready() -> void: if bubble != null: _bubbles.append(bubble) for path: NodePath in focus_paths: - var bubble := get_node(path) as BubbleButton - if bubble != null: - _focus_bubbles.append(bubble) + var focus_control := get_node(path) as Control + if focus_control != null: + _focus_controls.append(focus_control) _cluster.configure(_bubbles) _configure_focus_order() resized.connect(_update_layout) @@ -173,13 +173,13 @@ func _update_layout() -> void: _cluster.position = _resting_cluster_position _cluster.size = field_size _cluster.apply_layout(field_size, compact) - ControllerFocusNavigationType.configure_spatial_neighbors(_focus_bubbles) + ControllerFocusNavigationType.configure_spatial_neighbors(_focus_controls) func _configure_focus_order() -> void: for bubble: BubbleButton in _bubbles: bubble.focus_mode = Control.FOCUS_NONE - ControllerFocusNavigationType.configure_spatial_neighbors(_focus_bubbles) + ControllerFocusNavigationType.configure_spatial_neighbors(_focus_controls) func _set_interactive(interactive: bool) -> void: @@ -188,11 +188,11 @@ func _set_interactive(interactive: bool) -> void: if interactive else Control.MOUSE_FILTER_IGNORE ) - for bubble: BubbleButton in _focus_bubbles: - bubble.focus_mode = ( + for focus_control: Control in _focus_controls: + focus_control.focus_mode = ( Control.FOCUS_ALL if interactive else Control.FOCUS_NONE ) - bubble.mouse_filter = ( + focus_control.mouse_filter = ( Control.MOUSE_FILTER_STOP if interactive else Control.MOUSE_FILTER_IGNORE diff --git a/ui/settings_panel.gd b/ui/settings_panel.gd index 640cc1d..857ce4b 100644 --- a/ui/settings_panel.gd +++ b/ui/settings_panel.gd @@ -5,6 +5,7 @@ const MAX_PANEL_SIZE: Vector2 = Vector2(960.0, 700.0) const PANEL_EDGE_MARGIN: float = 16.0 const PAGE_ROOT: StringName = &"root" const PAGE_DISPLAY: StringName = &"display" +const PAGE_SOUND: StringName = &"sound" const PAGE_CONTROLS: StringName = &"controls" const PAGE_ACCESSIBILITY: StringName = &"accessibility" const PAGE_DATA: StringName = &"data" @@ -40,6 +41,7 @@ enum PresentationMode { @onready var _root_page: SettingsBubblePage = %RootPage @onready var _display_page: SettingsBubblePage = %DisplayPage +@onready var _sound_page: SettingsBubblePage = %SoundPage @onready var _controls_page: SettingsBubblePage = %ControlsPage @onready var _accessibility_page: SettingsBubblePage = %AccessibilityPage @onready var _data_page: SettingsBubblePage = %DataPage @@ -50,12 +52,21 @@ enum PresentationMode { @onready var _chat_dock: BubbleButton = %ChatDock @onready var _chat_mode: BubbleButton = %ChatMode @onready var _paint_dock: BubbleButton = %PaintDock +@onready var _fullscreen_toggle: BubbleButton = %FullscreenToggle @onready var _mouse_value: BubbleButton = %MouseValue @onready var _controller_value: BubbleButton = %ControllerValue @onready var _invert_y_toggle: BubbleButton = %InvertYToggle @onready var _on_screen_keyboard_toggle: BubbleButton = %OnScreenKeyboardToggle @onready var _auto_click_toggle: BubbleButton = %AutoClickToggle @onready var _auto_click_interval: BubbleButton = %AutoClickIntervalValue +@onready var _master_volume_slider: HSlider = %MasterVolumeSlider +@onready var _music_volume_slider: HSlider = %MusicVolumeSlider +@onready var _effects_volume_slider: HSlider = %EffectsVolumeSlider +@onready var _environment_volume_slider: HSlider = %EnvironmentVolumeSlider +@onready var _master_volume_value: Label = %MasterVolumeValue +@onready var _music_volume_value: Label = %MusicVolumeValue +@onready var _effects_volume_value: Label = %EffectsVolumeValue +@onready var _environment_volume_value: Label = %EnvironmentVolumeValue @onready var _world_options: Array[BubbleButton] = [ %WorldLegible, @@ -86,6 +97,10 @@ var _mouse_sensitivity: float = 0.005 var _controller_sensitivity: float = 2.5 var _invert_camera_y: bool = false var _on_screen_keyboard_enabled: bool = false +var _master_volume: float = 1.0 +var _music_volume: float = 1.0 +var _effects_volume: float = 1.0 +var _environment_volume: float = 1.0 var _network_profile: NetworkProfilePreferences var _network_session: NetworkSession var _data_root: PlayerDataRoot @@ -111,11 +126,13 @@ func _ready() -> void: _pages = { PAGE_ROOT: _root_page, PAGE_DISPLAY: _display_page, + PAGE_SOUND: _sound_page, PAGE_CONTROLS: _controls_page, PAGE_ACCESSIBILITY: _accessibility_page, PAGE_DATA: _data_page, } %DisplayCategory.pressed.connect(_push_page.bind(PAGE_DISPLAY)) + %SoundCategory.pressed.connect(_push_page.bind(PAGE_SOUND)) %ControlsCategory.pressed.connect(_push_page.bind(PAGE_CONTROLS)) %AccessibilityCategory.pressed.connect( _push_page.bind(PAGE_ACCESSIBILITY) @@ -124,12 +141,14 @@ func _ready() -> void: %ApplySettingsButton.pressed.connect(_apply_settings) %RootBackButton.pressed.connect(close_panel) %DisplayBackButton.pressed.connect(handle_back) + %SoundBackButton.pressed.connect(handle_back) %ControlsBackButton.pressed.connect(handle_back) %ControllerMapping.pressed.connect(_open_controller_mapping) %AccessibilityBackButton.pressed.connect(handle_back) %DataBackButton.pressed.connect(handle_back) %RootBackButton.gui_input.connect(_on_back_bubble_gui_input) %DisplayBackButton.gui_input.connect(_on_back_bubble_gui_input) + %SoundBackButton.gui_input.connect(_on_back_bubble_gui_input) %ControlsBackButton.gui_input.connect(_on_back_bubble_gui_input) %AccessibilityBackButton.gui_input.connect(_on_back_bubble_gui_input) %DataBackButton.gui_input.connect(_on_back_bubble_gui_input) @@ -150,6 +169,7 @@ func _ready() -> void: _chat_dock.pressed.connect(_toggle_chat_dock) _chat_mode.pressed.connect(_toggle_chat_mode) _paint_dock.pressed.connect(_toggle_paint_dock) + _fullscreen_toggle.pressed.connect(_toggle_fullscreen) %MouseDecrease.pressed.connect(_adjust_mouse_sensitivity.bind(-1)) %MouseIncrease.pressed.connect(_adjust_mouse_sensitivity.bind(1)) _mouse_value.pressed.connect(_adjust_mouse_sensitivity.bind(1)) @@ -177,6 +197,18 @@ func _ready() -> void: _auto_click_interval.pressed.connect( _adjust_auto_click_interval.bind(1) ) + _master_volume_slider.value_changed.connect( + _on_audio_volume_changed.bind(&"master") + ) + _music_volume_slider.value_changed.connect( + _on_audio_volume_changed.bind(&"music") + ) + _effects_volume_slider.value_changed.connect( + _on_audio_volume_changed.bind(&"effects") + ) + _environment_volume_slider.value_changed.connect( + _on_audio_volume_changed.bind(&"environment") + ) _mouse_value.gui_input.connect( _on_continuous_value_input.bind(&"mouse") ) @@ -198,6 +230,7 @@ func _ready() -> void: _on_controller_mapping_panel_closed ) _style_data_page() + _style_sound_page() call_deferred("_refresh_panel_size") @@ -708,6 +741,10 @@ func _apply_settings() -> void: edited.controller_camera_sensitivity = _controller_sensitivity edited.invert_camera_y = _invert_camera_y edited.on_screen_keyboard_enabled = _on_screen_keyboard_enabled + edited.master_volume = _master_volume + edited.music_volume = _music_volume + edited.effects_volume = _effects_volume + edited.environment_volume = _environment_volume if _settings_manager.apply_settings(edited): if ( _presentation_mode == PresentationMode.TITLE_EMBEDDED @@ -730,6 +767,17 @@ func _load_controls() -> void: _controller_sensitivity = settings.controller_camera_sensitivity _invert_camera_y = settings.invert_camera_y _on_screen_keyboard_enabled = settings.on_screen_keyboard_enabled + _master_volume = settings.master_volume + _music_volume = settings.music_volume + _effects_volume = settings.effects_volume + _environment_volume = settings.environment_volume + _master_volume_slider.set_value_no_signal(_master_volume * 100.0) + _music_volume_slider.set_value_no_signal(_music_volume * 100.0) + _effects_volume_slider.set_value_no_signal(_effects_volume * 100.0) + _environment_volume_slider.set_value_no_signal( + _environment_volume * 100.0 + ) + _settings_manager.restore_audio_levels() _refresh_value_labels() @@ -752,6 +800,9 @@ func _refresh_value_labels() -> void: _paint_dock.text = ( "paint dock\n" + ("right" if settings.paint_dock_right else "left") ) + _fullscreen_toggle.text = ( + "fullscreen\n" + ("on" if settings.fullscreen_enabled else "off") + ) _mouse_value.text = "mouse\nsensitivity\n%.4f" % _mouse_sensitivity _controller_value.text = ( "controller\nsensitivity\n%.1f" % _controller_sensitivity @@ -771,6 +822,7 @@ func _refresh_value_labels() -> void: _auto_click_interval.text = ( "auto-click\ninterval\n%.2f s" % _auto_click_interval_value ) + _refresh_audio_value_labels() for index: int in _world_options.size(): _world_options[index].button_pressed = ( index + 1 == settings.world_pixel_size @@ -803,6 +855,49 @@ func _style_data_page() -> void: ) +func _style_sound_page() -> void: + UtilityPageStyle.apply_page(_sound_page) + var paper := _sound_page.get_node("Paper") as PanelContainer + paper.add_theme_stylebox_override( + "panel", UtilityPageStyle.panel_style() + ) + var content := _sound_page.get_node("Paper/Content") as VBoxContainer + for node: Node in content.find_children("*", "Label", true, false): + (node as Label).add_theme_color_override( + "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY + ) + + +func _on_audio_volume_changed(value: float, channel: StringName) -> void: + var linear_volume: float = clampf(value / 100.0, 0.0, 1.0) + match channel: + &"master": + _master_volume = linear_volume + &"music": + _music_volume = linear_volume + &"effects": + _effects_volume = linear_volume + &"environment": + _environment_volume = linear_volume + _refresh_audio_value_labels() + if _settings_manager != null: + _settings_manager.preview_audio_levels( + _master_volume, + _music_volume, + _effects_volume, + _environment_volume, + ) + + +func _refresh_audio_value_labels() -> void: + _master_volume_value.text = "%d%%" % roundi(_master_volume * 100.0) + _music_volume_value.text = "%d%%" % roundi(_music_volume * 100.0) + _effects_volume_value.text = "%d%%" % roundi(_effects_volume * 100.0) + _environment_volume_value.text = ( + "%d%%" % roundi(_environment_volume * 100.0) + ) + + func _set_world_pixelation(pixel_size: int) -> void: if _settings_manager == null: return @@ -849,6 +944,17 @@ func _toggle_chat_mode() -> void: _refresh_value_labels() +func _toggle_fullscreen() -> void: + if _settings_manager == null: + return + var edited: PlayerSettings = _settings_manager.current_settings.copy() + edited.fullscreen_enabled = not edited.fullscreen_enabled + if not _settings_manager.apply_settings(edited): + _feedback.text = "failed to save fullscreen setting." + return + _refresh_value_labels() + + func _toggle_paint_dock() -> void: if _settings_manager == null: return diff --git a/ui/settings_panel.tscn b/ui/settings_panel.tscn index e831674..4cf1681 100644 --- a/ui/settings_panel.tscn +++ b/ui/settings_panel.tscn @@ -41,8 +41,8 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("3_page") page_id = &"root" -bubble_paths = Array[NodePath]([NodePath("BubbleCluster/DisplayCategory"), NodePath("BubbleCluster/ControlsCategory"), NodePath("BubbleCluster/AccessibilityCategory"), NodePath("BubbleCluster/DataCategory"), NodePath("BubbleCluster/ApplySettingsButton"), NodePath("BubbleCluster/RootBackButton")]) -focus_paths = Array[NodePath]([NodePath("BubbleCluster/DisplayCategory"), NodePath("BubbleCluster/ControlsCategory"), NodePath("BubbleCluster/AccessibilityCategory"), NodePath("BubbleCluster/DataCategory"), NodePath("BubbleCluster/ApplySettingsButton"), NodePath("BubbleCluster/RootBackButton")]) +bubble_paths = Array[NodePath]([NodePath("BubbleCluster/DisplayCategory"), NodePath("BubbleCluster/SoundCategory"), NodePath("BubbleCluster/ControlsCategory"), NodePath("BubbleCluster/AccessibilityCategory"), NodePath("BubbleCluster/DataCategory"), NodePath("BubbleCluster/ApplySettingsButton"), NodePath("BubbleCluster/RootBackButton")]) +focus_paths = Array[NodePath]([NodePath("BubbleCluster/DisplayCategory"), NodePath("BubbleCluster/SoundCategory"), NodePath("BubbleCluster/ControlsCategory"), NodePath("BubbleCluster/AccessibilityCategory"), NodePath("BubbleCluster/DataCategory"), NodePath("BubbleCluster/ApplySettingsButton"), NodePath("BubbleCluster/RootBackButton")]) initial_focus_path = NodePath("BubbleCluster/DisplayCategory") back_focus_path = NodePath("BubbleCluster/RootBackButton") compact_maximum_layout_size = Vector2(544, 400) @@ -88,6 +88,18 @@ minimum_font_size = 17 maximum_font_size = 25 motion_phase = 1.45 +[node name="SoundCategory" parent="RootPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "sound" +neutral_size = Vector2(144, 136) +desktop_anchor = Vector2(100, 180) +compact_anchor = Vector2(90, 150) +compact_minimum_size = Vector2(118, 112) +minimum_font_size = 17 +maximum_font_size = 25 +motion_phase = 2.0 + [node name="AccessibilityCategory" parent="RootPage/BubbleCluster" instance=ExtResource("4_bubble")] unique_name_in_owner = true custom_minimum_size = Vector2(0, 0) @@ -163,8 +175,8 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("3_page") page_id = &"display" -bubble_paths = Array[NodePath]([NodePath("BubbleCluster/WorldValue"), NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UIValue"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/ChatDock"), NodePath("BubbleCluster/ChatMode"), NodePath("BubbleCluster/PaintDock"), NodePath("BubbleCluster/DisplayBackButton")]) -focus_paths = Array[NodePath]([NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/ChatDock"), NodePath("BubbleCluster/ChatMode"), NodePath("BubbleCluster/PaintDock"), NodePath("BubbleCluster/DisplayBackButton")]) +bubble_paths = Array[NodePath]([NodePath("BubbleCluster/WorldValue"), NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UIValue"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/ChatDock"), NodePath("BubbleCluster/ChatMode"), NodePath("BubbleCluster/PaintDock"), NodePath("BubbleCluster/FullscreenToggle"), NodePath("BubbleCluster/DisplayBackButton")]) +focus_paths = Array[NodePath]([NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/ChatDock"), NodePath("BubbleCluster/ChatMode"), NodePath("BubbleCluster/PaintDock"), NodePath("BubbleCluster/FullscreenToggle"), NodePath("BubbleCluster/DisplayBackButton")]) initial_focus_path = NodePath("BubbleCluster/WorldRetro") back_focus_path = NodePath("BubbleCluster/DisplayBackButton") compact_maximum_layout_size = Vector2(544, 400) @@ -343,8 +355,8 @@ unique_name_in_owner = true custom_minimum_size = Vector2(0, 0) text = "chat dock\nleft" neutral_size = Vector2(96, 96) -desktop_anchor = Vector2(105, 435) -compact_anchor = Vector2(76, 378) +desktop_anchor = Vector2(70, 435) +compact_anchor = Vector2(55, 378) compact_minimum_size = Vector2(82, 82) minimum_font_size = 12 maximum_font_size = 16 @@ -355,8 +367,8 @@ unique_name_in_owner = true custom_minimum_size = Vector2(0, 0) text = "chat mode\ndesktop" neutral_size = Vector2(96, 96) -desktop_anchor = Vector2(275, 435) -compact_anchor = Vector2(225, 378) +desktop_anchor = Vector2(215, 435) +compact_anchor = Vector2(180, 378) compact_minimum_size = Vector2(82, 82) minimum_font_size = 12 maximum_font_size = 16 @@ -367,24 +379,215 @@ unique_name_in_owner = true custom_minimum_size = Vector2(0, 0) text = "paint dock\nright" neutral_size = Vector2(96, 96) -desktop_anchor = Vector2(445, 435) -compact_anchor = Vector2(380, 378) +desktop_anchor = Vector2(360, 435) +compact_anchor = Vector2(305, 378) compact_minimum_size = Vector2(82, 82) minimum_font_size = 12 maximum_font_size = 16 motion_phase = 1.7 +[node name="FullscreenToggle" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "fullscreen\noff" +neutral_size = Vector2(104, 96) +desktop_anchor = Vector2(505, 435) +compact_anchor = Vector2(430, 378) +compact_minimum_size = Vector2(88, 82) +minimum_font_size = 11 +maximum_font_size = 15 +motion_phase = 1.85 + [node name="DisplayBackButton" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")] unique_name_in_owner = true custom_minimum_size = Vector2(0, 0) text = "back" neutral_size = Vector2(96, 90) -desktop_anchor = Vector2(615, 435) -compact_anchor = Vector2(530, 378) +desktop_anchor = Vector2(650, 435) +compact_anchor = Vector2(555, 378) compact_minimum_size = Vector2(82, 78) minimum_font_size = 13 maximum_font_size = 16 -motion_phase = 2.0 +motion_phase = 2.15 + +[node name="SoundPage" type="Control" parent="."] +unique_name_in_owner = true +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("3_page") +page_id = &"sound" +bubble_paths = Array[NodePath]([NodePath("BubbleCluster/SoundBackButton")]) +focus_paths = Array[NodePath]([NodePath("Paper/Content/VolumeGrid/MasterVolumeSlider"), NodePath("Paper/Content/VolumeGrid/MusicVolumeSlider"), NodePath("Paper/Content/VolumeGrid/EffectsVolumeSlider"), NodePath("Paper/Content/VolumeGrid/EnvironmentVolumeSlider"), NodePath("BubbleCluster/SoundBackButton")]) +initial_focus_path = NodePath("Paper/Content/VolumeGrid/MasterVolumeSlider") +back_focus_path = NodePath("BubbleCluster/SoundBackButton") +compact_maximum_layout_size = Vector2(544, 400) + +[node name="BubbleCluster" type="Control" parent="SoundPage"] +layout_mode = 0 +offset_right = 720.0 +offset_bottom = 520.0 +script = ExtResource("5_cluster") +profile = ExtResource("6_profile") +desktop_reference_size = Vector2(720, 520) +compact_reference_size = Vector2(608, 448) + +[node name="Paper" type="PanelContainer" parent="SoundPage"] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -310.0 +offset_top = -230.0 +offset_right = 310.0 +offset_bottom = 205.0 +grow_horizontal = 2 +grow_vertical = 2 +z_index = 1 + +[node name="Content" type="VBoxContainer" parent="SoundPage/Paper"] +layout_mode = 2 +theme_override_constants/separation = 18 + +[node name="Heading" type="Label" parent="SoundPage/Paper/Content"] +layout_mode = 2 +theme_override_font_sizes/font_size = 28 +text = "sound" +horizontal_alignment = 1 + +[node name="Helper" type="Label" parent="SoundPage/Paper/Content"] +layout_mode = 2 +text = "Adjust the mix. Changes are saved when you apply settings." +horizontal_alignment = 1 + +[node name="VolumeGrid" type="GridContainer" parent="SoundPage/Paper/Content"] +layout_mode = 2 +theme_override_constants/h_separation = 16 +theme_override_constants/v_separation = 18 +columns = 3 + +[node name="MasterLabel" type="Label" parent="SoundPage/Paper/Content/VolumeGrid"] +custom_minimum_size = Vector2(118, 42) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "master" +vertical_alignment = 1 + +[node name="MasterVolumeSlider" type="HSlider" parent="SoundPage/Paper/Content/VolumeGrid"] +unique_name_in_owner = true +custom_minimum_size = Vector2(350, 42) +layout_mode = 2 +focus_mode = 2 +tooltip_text = "master volume" +max_value = 100.0 +step = 1.0 +value = 100.0 + +[node name="MasterVolumeValue" type="Label" parent="SoundPage/Paper/Content/VolumeGrid"] +unique_name_in_owner = true +custom_minimum_size = Vector2(70, 42) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "100%" +horizontal_alignment = 2 +vertical_alignment = 1 + +[node name="MusicLabel" type="Label" parent="SoundPage/Paper/Content/VolumeGrid"] +custom_minimum_size = Vector2(118, 42) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "music" +vertical_alignment = 1 + +[node name="MusicVolumeSlider" type="HSlider" parent="SoundPage/Paper/Content/VolumeGrid"] +unique_name_in_owner = true +custom_minimum_size = Vector2(350, 42) +layout_mode = 2 +focus_mode = 2 +tooltip_text = "music volume" +max_value = 100.0 +step = 1.0 +value = 100.0 + +[node name="MusicVolumeValue" type="Label" parent="SoundPage/Paper/Content/VolumeGrid"] +unique_name_in_owner = true +custom_minimum_size = Vector2(70, 42) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "100%" +horizontal_alignment = 2 +vertical_alignment = 1 + +[node name="EffectsLabel" type="Label" parent="SoundPage/Paper/Content/VolumeGrid"] +custom_minimum_size = Vector2(118, 42) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "effects" +vertical_alignment = 1 + +[node name="EffectsVolumeSlider" type="HSlider" parent="SoundPage/Paper/Content/VolumeGrid"] +unique_name_in_owner = true +custom_minimum_size = Vector2(350, 42) +layout_mode = 2 +focus_mode = 2 +tooltip_text = "effects volume" +max_value = 100.0 +step = 1.0 +value = 100.0 + +[node name="EffectsVolumeValue" type="Label" parent="SoundPage/Paper/Content/VolumeGrid"] +unique_name_in_owner = true +custom_minimum_size = Vector2(70, 42) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "100%" +horizontal_alignment = 2 +vertical_alignment = 1 + +[node name="EnvironmentLabel" type="Label" parent="SoundPage/Paper/Content/VolumeGrid"] +custom_minimum_size = Vector2(118, 42) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "environment" +vertical_alignment = 1 + +[node name="EnvironmentVolumeSlider" type="HSlider" parent="SoundPage/Paper/Content/VolumeGrid"] +unique_name_in_owner = true +custom_minimum_size = Vector2(350, 42) +layout_mode = 2 +focus_mode = 2 +tooltip_text = "environment volume" +max_value = 100.0 +step = 1.0 +value = 100.0 + +[node name="EnvironmentVolumeValue" type="Label" parent="SoundPage/Paper/Content/VolumeGrid"] +unique_name_in_owner = true +custom_minimum_size = Vector2(70, 42) +layout_mode = 2 +theme_override_font_sizes/font_size = 18 +text = "100%" +horizontal_alignment = 2 +vertical_alignment = 1 + +[node name="SoundBackButton" parent="SoundPage/BubbleCluster" instance=ExtResource("4_bubble")] +unique_name_in_owner = true +z_index = 4 +layout_mode = 0 +text = "back" +neutral_size = Vector2(96, 90) +desktop_anchor = Vector2(60, 460) +compact_anchor = Vector2(55, 410) +compact_minimum_size = Vector2(82, 78) +minimum_font_size = 13 +maximum_font_size = 16 +motion_phase = 5.0 [node name="ControlsPage" type="Control" parent="."] unique_name_in_owner = true diff --git a/world/rain_ambience.gd b/world/rain_ambience.gd index dd9900a..acea9fd 100644 --- a/world/rain_ambience.gd +++ b/world/rain_ambience.gd @@ -15,7 +15,7 @@ var _is_active: bool = false func _ready() -> void: - bus = &"SFX" + bus = &"Environment" var rain_stream := load(RAIN_STREAM_PATH) as AudioStreamOggVorbis var runtime_stream := ( rain_stream.duplicate() as AudioStreamOggVorbis diff --git a/world/shoreline_ambience.gd b/world/shoreline_ambience.gd index 959cb93..81e9c65 100644 --- a/world/shoreline_ambience.gd +++ b/world/shoreline_ambience.gd @@ -18,6 +18,7 @@ var _is_active := false func _ready() -> void: + _waves_audio.bus = &"Environment" _configure_audio_loop(_waves_audio.stream) _waves_audio.volume_db = -80.0 set_process(false)