Add character calls, settings controls, and gameplay fixes

This commit is contained in:
Alexander Sellite 2026-08-09 11:22:35 -04:00
parent 7b2898c11a
commit 40696b6bd9
33 changed files with 1120 additions and 78 deletions

View file

@ -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/

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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(

View file

@ -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

View file

@ -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([])

View file

@ -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(

View file

@ -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 = ""

View file

@ -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(

View file

@ -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),
]

View file

@ -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,10 +1093,20 @@ 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(
@ -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:

View file

@ -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)

View file

@ -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/

View file

@ -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,

View file

@ -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)

Binary file not shown.

View file

@ -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

Binary file not shown.

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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,6 +321,12 @@ func is_collapsed() -> bool:
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("open_chat"):
if event is InputEventKey:
if _opened:
_focus_entry_after_open()
else:
open_chat()
else:
toggle_chat()
get_viewport().set_input_as_handled()
return
@ -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:

View file

@ -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

View file

@ -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

View file

@ -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
)

View file

@ -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
)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)