diff --git a/items/icons/equipment/64_cooler_plus.png b/items/icons/equipment/64_cooler_plus.png new file mode 100644 index 0000000..acbd889 Binary files /dev/null and b/items/icons/equipment/64_cooler_plus.png differ diff --git a/items/icons/equipment/64_cooler_plus.png.import b/items/icons/equipment/64_cooler_plus.png.import new file mode 100644 index 0000000..44e493a --- /dev/null +++ b/items/icons/equipment/64_cooler_plus.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://baht2hredfky8" +path="res://.godot/imported/64_cooler_plus.png-d0daa39246f92fd9774ec1721970efad.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://items/icons/equipment/64_cooler_plus.png" +dest_files=["res://.godot/imported/64_cooler_plus.png-d0daa39246f92fd9774ec1721970efad.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/jobs/player_job_service.gd b/jobs/player_job_service.gd index 5f764f8..9955ccb 100644 --- a/jobs/player_job_service.gd +++ b/jobs/player_job_service.gd @@ -36,7 +36,7 @@ var _pending_rewards: Array[Dictionary] = [] var _lifetime_claimed: Array[String] = [] var _total_catches: int = 0 var _total_sold: int = 0 -var _previous_hour: float = WorldTimeService.DEFAULT_START_HOUR +var _daily_clock_hour: float = WorldTimeService.DEFAULT_START_HOUR func setup( @@ -55,8 +55,8 @@ func setup( _world_time = world_time _world_weather = world_weather _session = session - _previous_hour = _world_time.get_time_hours() - _world_time.time_changed.connect(_on_time_changed) + _daily_clock_hour = _world_time.get_time_hours() + _world_time.natural_time_advanced.connect(_on_natural_time_advanced) _session.state_changed.connect(_on_session_state_changed) _collection.collection_changed.connect(_on_current_state_changed) _experience.experience_changed.connect(_on_experience_changed) @@ -86,7 +86,6 @@ func set_save_manager(save_manager: PlayerSaveManager) -> void: func begin_progression_session() -> void: _progression_ready = true - _previous_hour = _world_time.get_time_hours() if _session.is_host(): _activate_host_board() changed.emit() @@ -155,7 +154,7 @@ func get_time_until_refresh_text() -> String: if _active_plan_id.is_empty() or _world_time == null: return "daily jobs unavailable" var elapsed: float = fposmod( - _world_time.get_time_hours() - DAILY_REFRESH_HOUR, + _daily_clock_hour - DAILY_REFRESH_HOUR, WorldTimeService.HOURS_PER_DAY, ) var hours_remaining: float = WorldTimeService.HOURS_PER_DAY - elapsed @@ -185,7 +184,10 @@ func claim(claim_id: String) -> bool: func get_host_board_network_data() -> Dictionary: - return _host_board.duplicate(true) + var board := _host_board.duplicate(true) + if not board.is_empty(): + board["daily_clock_hour"] = _daily_clock_hour + return board func apply_remote_board(board: Dictionary) -> bool: @@ -198,6 +200,8 @@ func apply_remote_board(board: Dictionary) -> bool: _daily_progress.clear() _daily_completions.clear() _active_board = board.duplicate(true) + if board.has("daily_clock_hour"): + _daily_clock_hour = float(board["daily_clock_hour"]) changed.emit() return true @@ -223,6 +227,7 @@ func to_save_data() -> Dictionary: "active_plan_id": _active_plan_id, "daily_progress": progress, "daily_completions": completions, + "daily_clock_hour": _daily_clock_hour, "pending_rewards": _pending_rewards.duplicate(true), "lifetime_claimed": _lifetime_claimed.duplicate(), "statistics": { @@ -246,6 +251,12 @@ func restore_from_save_data(data: Dictionary) -> bool: var completions: Dictionary = data.get("daily_completions", {}) for key: Variant in completions: _daily_completions[str(key)] = mini(int(completions[key]), 1) + _daily_clock_hour = float(data.get( + "daily_clock_hour", + _world_time.get_time_hours() + if _world_time != null + else WorldTimeService.DEFAULT_START_HOUR, + )) _pending_rewards.clear() var rewards: Array = data.get("pending_rewards", []) var restored_daily_rewards: Dictionary[String, bool] = {} @@ -290,6 +301,7 @@ func reset_to_defaults() -> void: _lifetime_claimed.clear() _total_catches = 0 _total_sold = 0 + _daily_clock_hour = WorldTimeService.DEFAULT_START_HOUR changed.emit() @@ -300,6 +312,7 @@ static func default_save_data() -> Dictionary: "active_plan_id": "", "daily_progress": {}, "daily_completions": {}, + "daily_clock_hour": WorldTimeService.DEFAULT_START_HOUR, "pending_rewards": [], "lifetime_claimed": [], "statistics": {"fish_caught": 0, "fish_sold": 0}, @@ -363,6 +376,16 @@ static func validate_save_data(value: Variant) -> bool: var host_board: Dictionary = data.get("host_board", {}) if not host_board.is_empty() and not validate_board(host_board): return false + if ( + data.has("daily_clock_hour") + and ( + typeof(data["daily_clock_hour"]) not in [TYPE_FLOAT, TYPE_INT] + or not is_finite(float(data["daily_clock_hour"])) + or float(data["daily_clock_hour"]) < 0.0 + or float(data["daily_clock_hour"]) >= WorldTimeService.HOURS_PER_DAY + ) + ): + return false var progress: Dictionary = data.get("daily_progress", {}) if progress.size() > 8: return false @@ -475,19 +498,22 @@ func _apply_host_weather_plan() -> void: ) -func _on_time_changed( - time_hours: float, - _phase: WorldTimeService.Phase, -) -> void: +func _on_natural_time_advanced(hours: float) -> void: + if not is_finite(hours) or hours <= 0.0: + return + var previous_hour := _daily_clock_hour + _daily_clock_hour = fposmod( + _daily_clock_hour + hours, + WorldTimeService.HOURS_PER_DAY, + ) if ( _progression_ready and _session != null and _session.is_host() - and _crossed_daily_refresh(_previous_hour, time_hours) + and _crossed_daily_refresh(previous_hour, _daily_clock_hour) ): var cycle: int = int(_host_board.get("cycle", -1)) + 1 _generate_host_board(maxi(cycle, 0)) - _previous_hour = time_hours func _on_session_state_changed(state: NetworkSession.State) -> void: diff --git a/network/network_chat_service.gd b/network/network_chat_service.gd index ebb65e9..bed0d01 100644 --- a/network/network_chat_service.gd +++ b/network/network_chat_service.gd @@ -79,6 +79,21 @@ func send_local_message(body: String) -> bool: return true +func broadcast_system_message(body: String) -> bool: + if _session == null or not _session.is_host(): + return false + var clean := NetworkChatProtocol.sanitize_body(body) + if clean.is_empty(): + return false + _broadcast(_make_message( + NetworkChatProtocol.Kind.SYSTEM, + 0, + "", + clean, + )) + return true + + func get_history() -> Array[Dictionary]: var result: Array[Dictionary] = [] for message: Dictionary in _history: diff --git a/network/network_profile_preferences.gd b/network/network_profile_preferences.gd index c90cd7d..2c3924e 100644 --- a/network/network_profile_preferences.gd +++ b/network/network_profile_preferences.gd @@ -2,8 +2,13 @@ class_name NetworkProfilePreferences extends Node const FORMAT_VERSION: int = 1 +const VoiceProfilesType = preload( + "res://player/animalese_voice_profiles.gd" +) 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 created_at_unix: int = 0 var _profile_path := "" var _expected_hash := "" @@ -48,19 +53,39 @@ func load_or_create() -> bool: Time.get_ticks_usec(), ] display_name = "Player" + voice_id = VoiceProfilesType.DEFAULT_ID + speech_speed_id = VoiceProfilesType.DEFAULT_SPEED_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) + + +func set_profile_identity( + value: String, + selected_voice_id: String, + selected_speech_speed_id: String, +) -> bool: var clean_name: String = value.strip_edges() - if not is_valid_display_name(clean_name): + 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) + ): return false - var previous: String = display_name + var previous_name: String = display_name + var previous_voice_id: String = voice_id + var previous_speech_speed_id: String = speech_speed_id display_name = clean_name + voice_id = selected_voice_id + speech_speed_id = selected_speech_speed_id if _save_atomic(): return true - display_name = previous + display_name = previous_name + voice_id = previous_voice_id + speech_speed_id = previous_speech_speed_id return false @@ -109,6 +134,12 @@ func _load_existing() -> bool: return false profile_id = loaded_id display_name = loaded_name + voice_id = VoiceProfilesType.sanitized_id( + str(data.get("voice_id", VoiceProfilesType.DEFAULT_ID)) + ) + speech_speed_id = VoiceProfilesType.sanitized_speed_id( + str(data.get("speech_speed_id", VoiceProfilesType.DEFAULT_SPEED_ID)) + ) created_at_unix = int(data["created_at_unix"]) return true @@ -118,6 +149,8 @@ func _save_atomic() -> bool: "format_version": FORMAT_VERSION, "profile_id": profile_id, "display_name": display_name, + "voice_id": voice_id, + "speech_speed_id": speech_speed_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 2666d26..4a0a5bd 100644 --- a/network/network_profile_service.gd +++ b/network/network_profile_service.gd @@ -1,6 +1,10 @@ class_name NetworkProfileService extends Node +const VoiceProfilesType = preload( + "res://player/animalese_voice_profiles.gd" +) + signal conflict_result( request_id: String, has_conflict: bool, @@ -18,6 +22,8 @@ var _preferences: NetworkProfilePreferences var _appearance_store: PlayerAppearanceStore 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 _host_pending_apply: Dictionary[String, Dictionary] = {} var _latest_check_id: String = "" var _latest_check_name: String = "" @@ -40,6 +46,7 @@ func setup( _session.join_authenticated.connect(_on_join_authenticated) _session.state_changed.connect(_on_session_state_changed) _apply_to_avatar(1, _appearance_store.get_snapshot()) + _apply_local_voice_to_avatar() func get_persisted_name() -> String: @@ -50,6 +57,14 @@ func get_persisted_appearance() -> Dictionary: return _appearance_store.get_snapshot() +func get_persisted_voice_id() -> String: + return _preferences.voice_id + + +func get_persisted_speech_speed_id() -> String: + return _preferences.speech_speed_id + + func get_identity_fingerprint() -> String: return ( _session.get_local_identity_fingerprint() @@ -87,11 +102,15 @@ func apply_profile( display_name: String, appearance: Dictionary, use_anyway: bool, + voice_id: String = VoiceProfilesType.DEFAULT_ID, + speech_speed_id: String = VoiceProfilesType.DEFAULT_SPEED_ID, ) -> bool: var clean_name := display_name.strip_edges() if ( not NetworkProfilePreferences.is_valid_display_name(clean_name) or not CharacterCustomizationCatalog.validate_snapshot(appearance) + or not VoiceProfilesType.is_valid(voice_id) + or not VoiceProfilesType.is_valid_speed(speech_speed_id) ): apply_finished.emit(false, "Check the player name and appearance choices.") return false @@ -108,6 +127,8 @@ func apply_profile( "profile_update", NetworkProfileProtocol.signature_fields(request) ) _pending_apply[request_id] = request + _pending_voice_ids[request_id] = voice_id + _pending_speech_speed_ids[request_id] = speech_speed_id if _session == null or not _session.is_gameplay_session_active(): _apply_local_result(request_id, true, "", false, PackedStringArray()) elif _session.is_host(): @@ -264,20 +285,46 @@ func _apply_local_result( return if not accepted: _pending_apply.erase(request_id) + _pending_voice_ids.erase(request_id) + _pending_speech_speed_ids.erase(request_id) conflict_result.emit(request_id, conflict, suggestions) apply_finished.emit(false, message) return var previous_name := _preferences.display_name - if not _preferences.set_display_name(str(request["display_name"])): + var previous_voice_id := _preferences.voice_id + var previous_speech_speed_id := _preferences.speech_speed_id + var requested_voice_id: String = _pending_voice_ids.get( + request_id, + VoiceProfilesType.DEFAULT_ID, + ) + var requested_speech_speed_id: String = _pending_speech_speed_ids.get( + request_id, + VoiceProfilesType.DEFAULT_SPEED_ID, + ) + if not _preferences.set_profile_identity( + str(request["display_name"]), + requested_voice_id, + requested_speech_speed_id, + ): _pending_apply.erase(request_id) + _pending_voice_ids.erase(request_id) + _pending_speech_speed_ids.erase(request_id) apply_finished.emit(false, "Profile could not be saved.") return if not _appearance_store.save_snapshot(request["appearance"]): - _preferences.set_display_name(previous_name) + _preferences.set_profile_identity( + previous_name, + previous_voice_id, + previous_speech_speed_id, + ) _pending_apply.erase(request_id) + _pending_voice_ids.erase(request_id) + _pending_speech_speed_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) if _session != null and _session.is_gameplay_session_active(): if _session.is_host(): _session.apply_canonical_profile( @@ -304,6 +351,7 @@ func _apply_local_result( _session.get_local_peer_id() if _session != null else 1, _appearance_store.get_snapshot(), ) + _apply_local_voice_to_avatar() apply_finished.emit(true, "Profile saved.") @@ -361,6 +409,7 @@ func _on_join_authenticated() -> void: _apply_to_avatar( _session.get_local_peer_id(), _appearance_store.get_snapshot() ) + _apply_local_voice_to_avatar() func _on_peer_removed(_peer_id: int) -> void: @@ -400,6 +449,8 @@ func _refresh_latest_check() -> void: func _on_session_state_changed(state: NetworkSession.State) -> void: if state == NetworkSession.State.INACTIVE: _pending_apply.clear() + _pending_voice_ids.clear() + _pending_speech_speed_ids.clear() _host_pending_apply.clear() _latest_check_id = "" _latest_check_name = "" @@ -465,5 +516,16 @@ func _apply_to_avatar(peer_id: int, appearance: Dictionary) -> void: avatar.apply_appearance_snapshot(appearance) +func _apply_local_voice_to_avatar() -> void: + if _spawn_service == null: + return + var local_peer_id := ( + _session.get_local_peer_id() if _session != null else 1 + ) + var avatar := _spawn_service.get_avatar(local_peer_id) + if avatar != null: + avatar.apply_animalese_voice_id(_preferences.voice_id) + + func _new_id() -> String: return Crypto.new().generate_random_bytes(16).hex_encode() diff --git a/network/network_world_time_service.gd b/network/network_world_time_service.gd index 91b4060..246af77 100644 --- a/network/network_world_time_service.gd +++ b/network/network_world_time_service.gd @@ -17,6 +17,12 @@ func setup(session: NetworkSession, world_time: WorldTimeService) -> void: _world_time = world_time _session.state_changed.connect(_on_session_state_changed) _session.peer_authenticated.connect(_on_peer_authenticated) + if not _world_time.authoritative_time_set.is_connected( + _on_authoritative_time_set + ): + _world_time.authoritative_time_set.connect( + _on_authoritative_time_set + ) set_process(true) @@ -82,6 +88,13 @@ func _on_peer_authenticated(peer_id: int, _display_name: String) -> void: _send_snapshot(peer_id) +func _on_authoritative_time_set(_time_hours: float) -> void: + if _session == null or not _session.is_host(): + return + _sync_elapsed = 0.0 + _broadcast_snapshot() + + func _broadcast_snapshot() -> void: if _session == null or not _session.is_host(): return diff --git a/player/animalese_voice_profiles.gd b/player/animalese_voice_profiles.gd new file mode 100644 index 0000000..aa135c1 --- /dev/null +++ b/player/animalese_voice_profiles.gd @@ -0,0 +1,57 @@ +class_name AnimaleseVoiceProfiles +extends RefCounted + +const DEFAULT_ID: String = "natural" +const DEFAULT_SPEED_ID: String = "normal" +const OPTIONS: Array[Dictionary] = [ + {"id": "tiny", "label": "tiny", "pitch": 1.24}, + {"id": "bright", "label": "bright", "pitch": 1.10}, + {"id": "natural", "label": "natural", "pitch": 1.0}, + {"id": "mellow", "label": "mellow", "pitch": 0.89}, + {"id": "deep", "label": "deep", "pitch": 0.77}, +] +const SPEED_OPTIONS: Array[Dictionary] = [ + {"id": "slow", "label": "slow", "characters_per_second": 20.0}, + {"id": "relaxed", "label": "relaxed", "characters_per_second": 24.0}, + {"id": "normal", "label": "normal", "characters_per_second": 28.0}, + {"id": "quick", "label": "quick", "characters_per_second": 34.0}, + {"id": "rapid", "label": "rapid", "characters_per_second": 40.0}, +] + + +static func is_valid(voice_id: String) -> bool: + for option: Dictionary in OPTIONS: + if str(option.get("id", "")) == voice_id: + return true + return false + + +static func sanitized_id(voice_id: String) -> String: + return voice_id if is_valid(voice_id) else DEFAULT_ID + + +static func pitch_for(voice_id: String) -> float: + var resolved_id := sanitized_id(voice_id) + for option: Dictionary in OPTIONS: + if str(option.get("id", "")) == resolved_id: + return float(option.get("pitch", 1.0)) + return 1.0 + + +static func is_valid_speed(speed_id: String) -> bool: + for option: Dictionary in SPEED_OPTIONS: + if str(option.get("id", "")) == speed_id: + return true + return false + + +static func sanitized_speed_id(speed_id: String) -> String: + return speed_id if is_valid_speed(speed_id) else DEFAULT_SPEED_ID + + +static func speed_for(speed_id: String) -> float: + var resolved_id := sanitized_speed_id(speed_id) + for option: Dictionary in SPEED_OPTIONS: + if str(option.get("id", "")) == resolved_id: + return float(option.get("characters_per_second", 28.0)) + return 28.0 diff --git a/player/animalese_voice_profiles.gd.uid b/player/animalese_voice_profiles.gd.uid new file mode 100644 index 0000000..1817007 --- /dev/null +++ b/player/animalese_voice_profiles.gd.uid @@ -0,0 +1 @@ +uid://dlut32tq2v66j diff --git a/player/player.gd b/player/player.gd index 4686df5..d4c0515 100644 --- a/player/player.gd +++ b/player/player.gd @@ -50,6 +50,7 @@ const CONTROLLER_ZOOM_TRIGGER_AXIS: JoyAxis = JOY_AXIS_TRIGGER_LEFT var appearance_snapshot: Dictionary = ( CharacterCustomizationCatalog.default_snapshot() ) +var animalese_voice_id: String = "natural" var active_bait_id: StringName = StringName() signal active_bait_changed(item_id: StringName) @@ -76,6 +77,14 @@ func apply_appearance_snapshot(snapshot: Dictionary) -> void: appearance_snapshot = snapshot.duplicate(true) PlayerVisualPresenter.apply_appearance(_visuals, appearance_snapshot) + +func apply_animalese_voice_id(voice_id: String) -> void: + animalese_voice_id = voice_id + + +func get_animalese_voice_id() -> String: + return animalese_voice_id + class ShowcaseCameraSnapshot: extends RefCounted @@ -129,6 +138,8 @@ class ShowcaseCameraSnapshot: @export var zoom_smoothing: float = 12.0 @export_range(0.1, 12.0, 0.1) var controller_zoom_speed: float = 4.0 @export_range(0.0, 1.0, 0.01) var controller_trigger_threshold: float = 0.55 +@export_range(1.0, 40.0, 0.5) var free_camera_speed: float = 10.0 +@export_range(1.0, 10.0, 0.5) var free_camera_sprint_multiplier: float = 3.0 @onready var _visuals: Node3D = %Visuals @onready var _character_rig: Node3D = get_node_or_null( @@ -141,6 +152,7 @@ class ShowcaseCameraSnapshot: @onready var _camera_pitch: Node3D = %CameraPitch @onready var _spring_arm: SpringArm3D = %SpringArm3D @onready var _camera: Camera3D = %Camera3D +@onready var _player_collision_shape: CollisionShape3D = $CollisionShape3D @onready var inventory: FishInventoryType = %Inventory @onready var collection_log: CollectionLogType = %CollectionLog @onready var wallet: PlayerWalletType = %Wallet @@ -165,6 +177,11 @@ 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 _free_camera_active: bool = false +var _free_camera_body: CharacterBody3D +var _free_camera: Camera3D +var _free_camera_yaw: float = 0.0 +var _free_camera_pitch: float = 0.0 var _movement_enabled: bool = true var _water_recovery_active: bool = false var _remote_recovery_presentation_active: bool = false @@ -197,6 +214,8 @@ var _network_target_visual_yaw: float = 0.0 var _network_snapshot_ready: bool = false var _character_animation_name: StringName = &"" var _sitting: bool = false +var _sitting_intent_pending: bool = false +var _sitting_intent_sequence: int = -1 var _held_fish_visible: bool = false var _showcase_animation_active: bool = false var _fishing_rod: Node3D @@ -205,6 +224,8 @@ var _controller_mapping_manager: ControllerMappingManagerType func _ready() -> void: + if not bag.contents_changed.is_connected(_on_bag_contents_changed): + bag.contents_changed.connect(_on_bag_contents_changed) PlayerVisualPresenter.apply_appearance(_visuals, appearance_snapshot) _initialize_fishing_rod() _target_zoom = clampf(_spring_arm.spring_length, minimum_zoom, maximum_zoom) @@ -213,6 +234,14 @@ func _ready() -> void: _initialize_held_item_attachment() +func _on_bag_contents_changed() -> void: + if ( + not active_bait_id.is_empty() + and bag.get_quantity(active_bait_id) <= 0 + ): + unequip_bait() + + func set_controller_mapping_manager( mapping_manager: ControllerMappingManagerType, ) -> void: @@ -267,8 +296,18 @@ func _physics_process(delta: float) -> void: if _network_interpolation_enabled: _update_network_interpolation(delta) return + if local_control_enabled and _free_camera_active: + _update_free_camera_physics() + velocity.x = 0.0 + velocity.z = 0.0 + if not is_on_floor(): + velocity.y -= _gravity * fall_gravity_multiplier * delta + move_and_slide() + _network_jump_pending = false + return var jump_requested: bool = ( _movement_enabled + and not _free_camera_active and ( (local_control_enabled and Input.is_action_just_pressed("jump")) or (_network_authoritative_simulation and _network_jump_pending) @@ -276,7 +315,7 @@ func _physics_process(delta: float) -> void: ) if _sitting: if jump_requested: - _set_sitting(false) + _set_sitting(false, local_control_enabled) else: velocity = Vector3.ZERO _network_jump_pending = false @@ -370,6 +409,8 @@ func _process(delta: float) -> void: ) if not local_control_enabled: return + if _free_camera_active: + return if ( _camera_input_enabled @@ -519,7 +560,7 @@ func toggle_sitting() -> void: ]) ): return - _set_sitting(should_sit) + _set_sitting(should_sit, local_control_enabled) func _has_any_character_animation(candidates: Array[StringName]) -> bool: @@ -531,10 +572,16 @@ func _has_any_character_animation(candidates: Array[StringName]) -> bool: return false -func _set_sitting(should_sit: bool) -> void: +func _set_sitting( + should_sit: bool, + is_local_intent: bool = false, +) -> void: if _sitting == should_sit: return _sitting = should_sit + if is_local_intent: + _sitting_intent_pending = true + _sitting_intent_sequence = -1 if _sitting: velocity = Vector3.ZERO _character_animation_name = &"" @@ -548,6 +595,10 @@ func is_sitting() -> bool: func _unhandled_input(event: InputEvent) -> void: if not local_control_enabled or not _camera_input_enabled: return + if event.is_action_pressed("toggle_free_camera"): + _set_free_camera_active(not _free_camera_active) + get_viewport().set_input_as_handled() + return if event.is_action("camera_drag"): _camera_dragging = event.is_pressed() @@ -555,9 +606,14 @@ func _unhandled_input(event: InputEvent) -> void: return if event is InputEventMouseMotion and _camera_dragging: - _rotate_camera(event.relative * mouse_sensitivity) + if _free_camera_active: + _rotate_free_camera(event.relative * mouse_sensitivity) + else: + _rotate_camera(event.relative * mouse_sensitivity) get_viewport().set_input_as_handled() return + if _free_camera_active: + return var mouse_zoom_in: bool = ( event is InputEventMouseButton @@ -617,6 +673,98 @@ func _rotate_camera(delta_rotation: Vector2) -> void: ) +func _set_free_camera_active(active: bool) -> void: + if _free_camera_active == active: + return + if active: + var camera_transform: Transform3D = _camera.global_transform + var duplicated_camera := _camera.duplicate() as Camera3D + if duplicated_camera == null: + return + var camera_body := CharacterBody3D.new() + camera_body.name = "FreeCameraBody" + camera_body.collision_layer = 0 + camera_body.collision_mask = collision_mask + camera_body.motion_mode = CharacterBody3D.MOTION_MODE_FLOATING + add_child(camera_body) + camera_body.top_level = true + camera_body.global_position = camera_transform.origin + var camera_collision := CollisionShape3D.new() + camera_collision.name = "FreeCameraCollision" + camera_collision.shape = _player_collision_shape.shape + camera_body.add_child(camera_collision) + duplicated_camera.name = "FreeCamera" + duplicated_camera.unique_name_in_owner = false + camera_body.add_child(duplicated_camera) + duplicated_camera.transform = Transform3D( + camera_transform.basis, + Vector3.ZERO, + ) + _free_camera_body = camera_body + _free_camera = duplicated_camera + var camera_rotation: Vector3 = _free_camera.global_rotation + _free_camera_yaw = camera_rotation.y + _free_camera_pitch = camera_rotation.x + _camera.current = false + _free_camera.current = true + _free_camera_active = true + return + _free_camera_active = false + _camera_dragging = false + if _free_camera != null: + _free_camera.current = false + _free_camera = null + if _free_camera_body != null: + _free_camera_body.queue_free() + _free_camera_body = null + _camera.current = local_control_enabled + + +func _update_free_camera_physics() -> void: + if _free_camera == null or _free_camera_body == null: + _set_free_camera_active(false) + return + var input_vector: Vector2 = Input.get_vector( + "move_left", + "move_right", + "move_forward", + "move_backward", + ) + var vertical_input: float = ( + Input.get_action_strength("jump") + - Input.get_action_strength("sneak") + ) + var movement: Vector3 = ( + _free_camera.global_basis.x * input_vector.x + + _free_camera.global_basis.z * input_vector.y + + Vector3.UP * vertical_input + ) + if movement.length_squared() > 1.0: + movement = movement.normalized() + var movement_speed: float = free_camera_speed + if Input.is_action_pressed("sprint"): + movement_speed *= free_camera_sprint_multiplier + _free_camera_body.velocity = movement * movement_speed + _free_camera_body.move_and_slide() + + +func _rotate_free_camera(delta_rotation: Vector2) -> void: + if _free_camera == null: + return + _free_camera_yaw -= delta_rotation.x + var vertical_direction: float = -1.0 if invert_camera_y else 1.0 + _free_camera_pitch = clampf( + _free_camera_pitch - delta_rotation.y * vertical_direction, + deg_to_rad(minimum_pitch_degrees), + deg_to_rad(maximum_pitch_degrees), + ) + _free_camera.global_rotation = Vector3( + _free_camera_pitch, + _free_camera_yaw, + 0.0, + ) + + func _set_target_zoom(value: float) -> void: _target_zoom = clampf(value, minimum_zoom, maximum_zoom) @@ -633,6 +781,8 @@ func _apply_axis_deadzone(value: float) -> float: func set_local_control(enabled: bool) -> void: + if not enabled and _free_camera_active: + _set_free_camera_active(false) local_control_enabled = enabled if is_node_ready(): _camera.current = enabled @@ -657,7 +807,9 @@ func configure_network_remote(authoritative_simulation: bool) -> void: func capture_network_input(sequence: int) -> Dictionary: - if not _movement_enabled or _water_recovery_active: + if _sitting_intent_pending and _sitting_intent_sequence < 0: + _sitting_intent_sequence = sequence + if not _movement_enabled or _water_recovery_active or _free_camera_active: return { "sequence": sequence, "axis": [0.0, 0.0], @@ -734,7 +886,17 @@ func apply_local_prediction_correction(snapshot: Dictionary) -> void: var parsed: Dictionary = _parse_network_snapshot(snapshot) if parsed.is_empty(): return - _set_sitting(bool(parsed["sitting"])) + var acknowledged_input: int = parsed["acknowledged_input"] + var sitting_intent_acknowledged: bool = ( + _sitting_intent_pending + and _sitting_intent_sequence >= 0 + and acknowledged_input >= _sitting_intent_sequence + ) + if sitting_intent_acknowledged: + _sitting_intent_pending = false + _sitting_intent_sequence = -1 + if not _sitting_intent_pending: + _set_sitting(bool(parsed["sitting"])) var authoritative_position: Vector3 = parsed["position"] var error_distance: float = global_position.distance_to( authoritative_position @@ -780,6 +942,17 @@ func _parse_network_snapshot(snapshot: Dictionary) -> Dictionary: float(network_velocity[2]) ) var visual_yaw: float = float(snapshot.get("visual_yaw", 0.0)) + if ( + snapshot.has("acknowledged_input") + and ( + typeof(snapshot.get("acknowledged_input")) != TYPE_INT + or int(snapshot.get("acknowledged_input")) < 0 + ) + ): + return {} + var acknowledged_input: int = int( + snapshot.get("acknowledged_input", 0) + ) if snapshot.has("sitting") and typeof(snapshot.get("sitting")) != TYPE_BOOL: return {} var sitting: bool = bool(snapshot.get("sitting", false)) @@ -793,6 +966,7 @@ func _parse_network_snapshot(snapshot: Dictionary) -> Dictionary: "position": parsed_position, "velocity": parsed_velocity, "visual_yaw": visual_yaw, + "acknowledged_input": acknowledged_input, "sitting": sitting, } @@ -835,7 +1009,10 @@ func set_camera_input_enabled(enabled: bool) -> void: func set_camera_active(active: bool) -> void: - _camera.current = active + if _free_camera_active and _free_camera != null: + _free_camera.current = active + else: + _camera.current = active func apply_camera_settings( diff --git a/scripts/generate_placeholder_animalese.gd b/scripts/generate_placeholder_animalese.gd new file mode 100644 index 0000000..8119fd3 --- /dev/null +++ b/scripts/generate_placeholder_animalese.gd @@ -0,0 +1,71 @@ +extends SceneTree + +const OUTPUT_DIRECTORY := "res://sound/dialogue/animalese/placeholder" +const SAMPLE_RATE: int = 22050 +const SAMPLE_SECONDS: float = 0.055 +const CHARACTERS := "abcdefghijklmnopqrstuvwxyz" +const VOWELS := "aeiou" + + +func _initialize() -> void: + var absolute_directory := ProjectSettings.globalize_path(OUTPUT_DIRECTORY) + var directory_error: Error = DirAccess.make_dir_recursive_absolute( + absolute_directory + ) + if directory_error != OK: + push_error("Could not create animalese sample directory.") + quit(1) + return + for character_index: int in range(CHARACTERS.length()): + var character := CHARACTERS.substr(character_index, 1) + _save_sample(character, character_index) + _save_sample("fallback", CHARACTERS.length()) + quit() + + +func _save_sample(sample_name: String, sample_index: int) -> void: + var stream := _build_sample(sample_name, sample_index) + var output_path := "%s/%s" % [OUTPUT_DIRECTORY, sample_name] + var save_error: Error = stream.save_to_wav(output_path) + if save_error != OK: + push_error("Could not save animalese sample: %s" % sample_name) + + +func _build_sample(sample_name: String, sample_index: int) -> AudioStreamWAV: + var frame_count := roundi(SAMPLE_RATE * SAMPLE_SECONDS) + var pcm := PackedByteArray() + pcm.resize(frame_count * 2) + var base_frequency := 245.0 + float(sample_index % 7) * 18.0 + var formant_one := 620.0 + float(sample_index % 5) * 85.0 + var formant_two := 1320.0 + float(sample_index % 6) * 115.0 + var is_vowel := VOWELS.contains(sample_name) + var noise_seed := sample_index * 7919 + 104729 + for frame_index: int in range(frame_count): + var time := float(frame_index) / SAMPLE_RATE + var attack := minf(time / 0.004, 1.0) + var release := minf((SAMPLE_SECONDS - time) / 0.014, 1.0) + var envelope := maxf(minf(attack, release), 0.0) + noise_seed = int( + (noise_seed * 1103515245 + 12345) & 0x7fffffff + ) + var noise := float(noise_seed % 65536) / 32767.5 - 1.0 + var voiced := ( + sin(TAU * base_frequency * time) * 0.42 + + sin(TAU * base_frequency * 2.0 * time) * 0.16 + ) + var formants := ( + sin(TAU * formant_one * time) * 0.18 + + sin(TAU * formant_two * time) * 0.10 + ) + var consonant_noise := noise * (0.08 if is_vowel else 0.24) + var value := envelope * (voiced + formants + consonant_noise) * 0.72 + var signed_sample := clampi(roundi(value * 32767.0), -32768, 32767) + var encoded_sample := signed_sample if signed_sample >= 0 else signed_sample + 65536 + pcm[frame_index * 2] = encoded_sample & 0xff + pcm[frame_index * 2 + 1] = (encoded_sample >> 8) & 0xff + var stream := AudioStreamWAV.new() + stream.set("format", AudioStreamWAV.FORMAT_16_BITS) + stream.mix_rate = SAMPLE_RATE + stream.stereo = false + stream.data = pcm + return stream diff --git a/scripts/generate_placeholder_animalese.gd.uid b/scripts/generate_placeholder_animalese.gd.uid new file mode 100644 index 0000000..9399a64 --- /dev/null +++ b/scripts/generate_placeholder_animalese.gd.uid @@ -0,0 +1 @@ +uid://cwqm75icgcr4u diff --git a/sound/dialogue/animalese/placeholder/a.wav b/sound/dialogue/animalese/placeholder/a.wav new file mode 100644 index 0000000..ca652d4 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/a.wav differ diff --git a/sound/dialogue/animalese/placeholder/a.wav.import b/sound/dialogue/animalese/placeholder/a.wav.import new file mode 100644 index 0000000..86de7ca --- /dev/null +++ b/sound/dialogue/animalese/placeholder/a.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://dvajjy4bwadio" +path="res://.godot/imported/a.wav-9ab4b1a9a555e6fcf07951f399769b30.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/a.wav" +dest_files=["res://.godot/imported/a.wav-9ab4b1a9a555e6fcf07951f399769b30.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/animalese/placeholder/b.wav b/sound/dialogue/animalese/placeholder/b.wav new file mode 100644 index 0000000..71ceb88 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/b.wav differ diff --git a/sound/dialogue/animalese/placeholder/b.wav.import b/sound/dialogue/animalese/placeholder/b.wav.import new file mode 100644 index 0000000..4aea49c --- /dev/null +++ b/sound/dialogue/animalese/placeholder/b.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://cs30tdyuo6wtc" +path="res://.godot/imported/b.wav-eed860af2033bda258bd172899061b45.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/b.wav" +dest_files=["res://.godot/imported/b.wav-eed860af2033bda258bd172899061b45.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/animalese/placeholder/c.wav b/sound/dialogue/animalese/placeholder/c.wav new file mode 100644 index 0000000..8b5eb64 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/c.wav differ diff --git a/sound/dialogue/animalese/placeholder/c.wav.import b/sound/dialogue/animalese/placeholder/c.wav.import new file mode 100644 index 0000000..bbd8074 --- /dev/null +++ b/sound/dialogue/animalese/placeholder/c.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://2inox7x31xam" +path="res://.godot/imported/c.wav-61dc49533634805d70c71d509b63e37b.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/c.wav" +dest_files=["res://.godot/imported/c.wav-61dc49533634805d70c71d509b63e37b.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/animalese/placeholder/d.wav b/sound/dialogue/animalese/placeholder/d.wav new file mode 100644 index 0000000..978009a Binary files /dev/null and b/sound/dialogue/animalese/placeholder/d.wav differ diff --git a/sound/dialogue/animalese/placeholder/d.wav.import b/sound/dialogue/animalese/placeholder/d.wav.import new file mode 100644 index 0000000..ffc5bb9 --- /dev/null +++ b/sound/dialogue/animalese/placeholder/d.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://yvha8eukw6l7" +path="res://.godot/imported/d.wav-910bc02bc42bdb1b37561959a317c28a.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/d.wav" +dest_files=["res://.godot/imported/d.wav-910bc02bc42bdb1b37561959a317c28a.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/animalese/placeholder/e.wav b/sound/dialogue/animalese/placeholder/e.wav new file mode 100644 index 0000000..76f7b8c Binary files /dev/null and b/sound/dialogue/animalese/placeholder/e.wav differ diff --git a/sound/dialogue/animalese/placeholder/e.wav.import b/sound/dialogue/animalese/placeholder/e.wav.import new file mode 100644 index 0000000..49de309 --- /dev/null +++ b/sound/dialogue/animalese/placeholder/e.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://8ujopf8aqs83" +path="res://.godot/imported/e.wav-7da5f8fcd017bf931de2bd49dc00f652.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/e.wav" +dest_files=["res://.godot/imported/e.wav-7da5f8fcd017bf931de2bd49dc00f652.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/animalese/placeholder/f.wav b/sound/dialogue/animalese/placeholder/f.wav new file mode 100644 index 0000000..bec92c1 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/f.wav differ diff --git a/sound/dialogue/animalese/placeholder/f.wav.import b/sound/dialogue/animalese/placeholder/f.wav.import new file mode 100644 index 0000000..dc85ee3 --- /dev/null +++ b/sound/dialogue/animalese/placeholder/f.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://c0gv4cphnfcxf" +path="res://.godot/imported/f.wav-8eb31f2de6240647a0d6d2520aadcd6a.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/f.wav" +dest_files=["res://.godot/imported/f.wav-8eb31f2de6240647a0d6d2520aadcd6a.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/animalese/placeholder/fallback.wav b/sound/dialogue/animalese/placeholder/fallback.wav new file mode 100644 index 0000000..63a759e Binary files /dev/null and b/sound/dialogue/animalese/placeholder/fallback.wav differ diff --git a/sound/dialogue/animalese/placeholder/fallback.wav.import b/sound/dialogue/animalese/placeholder/fallback.wav.import new file mode 100644 index 0000000..e116a3c --- /dev/null +++ b/sound/dialogue/animalese/placeholder/fallback.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://bw0pod4tmexm0" +path="res://.godot/imported/fallback.wav-7a40516b188f10028ac39bdc24fb52e5.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/fallback.wav" +dest_files=["res://.godot/imported/fallback.wav-7a40516b188f10028ac39bdc24fb52e5.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/animalese/placeholder/g.wav b/sound/dialogue/animalese/placeholder/g.wav new file mode 100644 index 0000000..39dccf1 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/g.wav differ diff --git a/sound/dialogue/animalese/placeholder/g.wav.import b/sound/dialogue/animalese/placeholder/g.wav.import new file mode 100644 index 0000000..1709852 --- /dev/null +++ b/sound/dialogue/animalese/placeholder/g.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://c7706p0fc8y7c" +path="res://.godot/imported/g.wav-45b2cbd9653a31477e3f00c9432d560c.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/g.wav" +dest_files=["res://.godot/imported/g.wav-45b2cbd9653a31477e3f00c9432d560c.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/animalese/placeholder/h.wav b/sound/dialogue/animalese/placeholder/h.wav new file mode 100644 index 0000000..acebfed Binary files /dev/null and b/sound/dialogue/animalese/placeholder/h.wav differ diff --git a/sound/dialogue/animalese/placeholder/h.wav.import b/sound/dialogue/animalese/placeholder/h.wav.import new file mode 100644 index 0000000..96b2359 --- /dev/null +++ b/sound/dialogue/animalese/placeholder/h.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://cl66wka105xmo" +path="res://.godot/imported/h.wav-2da53a6638eebb75f0867eaf75dc24ba.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/h.wav" +dest_files=["res://.godot/imported/h.wav-2da53a6638eebb75f0867eaf75dc24ba.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/animalese/placeholder/i.wav b/sound/dialogue/animalese/placeholder/i.wav new file mode 100644 index 0000000..fbd10e1 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/i.wav differ diff --git a/sound/dialogue/animalese/placeholder/i.wav.import b/sound/dialogue/animalese/placeholder/i.wav.import new file mode 100644 index 0000000..0c11fd6 --- /dev/null +++ b/sound/dialogue/animalese/placeholder/i.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://jicgdbnyt6bm" +path="res://.godot/imported/i.wav-1e8d7f13980a6f2f61d73f65a2efc91c.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/i.wav" +dest_files=["res://.godot/imported/i.wav-1e8d7f13980a6f2f61d73f65a2efc91c.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/animalese/placeholder/j.wav b/sound/dialogue/animalese/placeholder/j.wav new file mode 100644 index 0000000..0087db5 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/j.wav differ diff --git a/sound/dialogue/animalese/placeholder/j.wav.import b/sound/dialogue/animalese/placeholder/j.wav.import new file mode 100644 index 0000000..46a0baf --- /dev/null +++ b/sound/dialogue/animalese/placeholder/j.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://cji2evr0u3m6c" +path="res://.godot/imported/j.wav-2f355374ffcea77f8dc630ee45879444.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/j.wav" +dest_files=["res://.godot/imported/j.wav-2f355374ffcea77f8dc630ee45879444.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/animalese/placeholder/k.wav b/sound/dialogue/animalese/placeholder/k.wav new file mode 100644 index 0000000..c1b5e69 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/k.wav differ diff --git a/sound/dialogue/animalese/placeholder/k.wav.import b/sound/dialogue/animalese/placeholder/k.wav.import new file mode 100644 index 0000000..4af149c --- /dev/null +++ b/sound/dialogue/animalese/placeholder/k.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://eypaqf1iaq5" +path="res://.godot/imported/k.wav-b85d045dc022b9055c25a650b2e3af2a.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/k.wav" +dest_files=["res://.godot/imported/k.wav-b85d045dc022b9055c25a650b2e3af2a.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/animalese/placeholder/l.wav b/sound/dialogue/animalese/placeholder/l.wav new file mode 100644 index 0000000..fe2624b Binary files /dev/null and b/sound/dialogue/animalese/placeholder/l.wav differ diff --git a/sound/dialogue/animalese/placeholder/l.wav.import b/sound/dialogue/animalese/placeholder/l.wav.import new file mode 100644 index 0000000..8193ecf --- /dev/null +++ b/sound/dialogue/animalese/placeholder/l.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://bixgnaq4idkhj" +path="res://.godot/imported/l.wav-b21006c3b048e0c797d6baf19ab0f663.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/l.wav" +dest_files=["res://.godot/imported/l.wav-b21006c3b048e0c797d6baf19ab0f663.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/animalese/placeholder/m.wav b/sound/dialogue/animalese/placeholder/m.wav new file mode 100644 index 0000000..4660d96 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/m.wav differ diff --git a/sound/dialogue/animalese/placeholder/m.wav.import b/sound/dialogue/animalese/placeholder/m.wav.import new file mode 100644 index 0000000..2a2439b --- /dev/null +++ b/sound/dialogue/animalese/placeholder/m.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://dan4o17oreugv" +path="res://.godot/imported/m.wav-aa394c4b1d5309d859ce73a368754264.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/m.wav" +dest_files=["res://.godot/imported/m.wav-aa394c4b1d5309d859ce73a368754264.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/animalese/placeholder/n.wav b/sound/dialogue/animalese/placeholder/n.wav new file mode 100644 index 0000000..97e5457 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/n.wav differ diff --git a/sound/dialogue/animalese/placeholder/n.wav.import b/sound/dialogue/animalese/placeholder/n.wav.import new file mode 100644 index 0000000..f5e432c --- /dev/null +++ b/sound/dialogue/animalese/placeholder/n.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://dxup6pw80km61" +path="res://.godot/imported/n.wav-84e9a6fa6664222868d647187a09f374.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/n.wav" +dest_files=["res://.godot/imported/n.wav-84e9a6fa6664222868d647187a09f374.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/animalese/placeholder/o.wav b/sound/dialogue/animalese/placeholder/o.wav new file mode 100644 index 0000000..bee6bc1 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/o.wav differ diff --git a/sound/dialogue/animalese/placeholder/o.wav.import b/sound/dialogue/animalese/placeholder/o.wav.import new file mode 100644 index 0000000..b094a9f --- /dev/null +++ b/sound/dialogue/animalese/placeholder/o.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://lntkug4avr2k" +path="res://.godot/imported/o.wav-6c18faa9701875d7753d91afcd1b09e6.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/o.wav" +dest_files=["res://.godot/imported/o.wav-6c18faa9701875d7753d91afcd1b09e6.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/animalese/placeholder/p.wav b/sound/dialogue/animalese/placeholder/p.wav new file mode 100644 index 0000000..29fdd60 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/p.wav differ diff --git a/sound/dialogue/animalese/placeholder/p.wav.import b/sound/dialogue/animalese/placeholder/p.wav.import new file mode 100644 index 0000000..a431c1f --- /dev/null +++ b/sound/dialogue/animalese/placeholder/p.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://crubsdrd220eh" +path="res://.godot/imported/p.wav-c32b759c822f258d8d947827adf8127a.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/p.wav" +dest_files=["res://.godot/imported/p.wav-c32b759c822f258d8d947827adf8127a.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/animalese/placeholder/q.wav b/sound/dialogue/animalese/placeholder/q.wav new file mode 100644 index 0000000..df184ca Binary files /dev/null and b/sound/dialogue/animalese/placeholder/q.wav differ diff --git a/sound/dialogue/animalese/placeholder/q.wav.import b/sound/dialogue/animalese/placeholder/q.wav.import new file mode 100644 index 0000000..574c906 --- /dev/null +++ b/sound/dialogue/animalese/placeholder/q.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://ct0decr76agoq" +path="res://.godot/imported/q.wav-d494b927c6f2b8bfbaa44d1f81887731.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/q.wav" +dest_files=["res://.godot/imported/q.wav-d494b927c6f2b8bfbaa44d1f81887731.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/animalese/placeholder/r.wav b/sound/dialogue/animalese/placeholder/r.wav new file mode 100644 index 0000000..41fda60 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/r.wav differ diff --git a/sound/dialogue/animalese/placeholder/r.wav.import b/sound/dialogue/animalese/placeholder/r.wav.import new file mode 100644 index 0000000..1f196b1 --- /dev/null +++ b/sound/dialogue/animalese/placeholder/r.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://dpq7rin4pr1n3" +path="res://.godot/imported/r.wav-d1d419e3246c0e918ff51a9dd662a742.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/r.wav" +dest_files=["res://.godot/imported/r.wav-d1d419e3246c0e918ff51a9dd662a742.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/animalese/placeholder/s.wav b/sound/dialogue/animalese/placeholder/s.wav new file mode 100644 index 0000000..f9c542a Binary files /dev/null and b/sound/dialogue/animalese/placeholder/s.wav differ diff --git a/sound/dialogue/animalese/placeholder/s.wav.import b/sound/dialogue/animalese/placeholder/s.wav.import new file mode 100644 index 0000000..a794fdf --- /dev/null +++ b/sound/dialogue/animalese/placeholder/s.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://c00meumj2blfa" +path="res://.godot/imported/s.wav-15abb20075aacc8d5fe95cd206ae25ce.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/s.wav" +dest_files=["res://.godot/imported/s.wav-15abb20075aacc8d5fe95cd206ae25ce.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/animalese/placeholder/t.wav b/sound/dialogue/animalese/placeholder/t.wav new file mode 100644 index 0000000..76e0948 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/t.wav differ diff --git a/sound/dialogue/animalese/placeholder/t.wav.import b/sound/dialogue/animalese/placeholder/t.wav.import new file mode 100644 index 0000000..ce35ba2 --- /dev/null +++ b/sound/dialogue/animalese/placeholder/t.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://dr0dp5v8fdb8r" +path="res://.godot/imported/t.wav-3a26c5be0f8e3007adf1daa05a6a1ea2.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/t.wav" +dest_files=["res://.godot/imported/t.wav-3a26c5be0f8e3007adf1daa05a6a1ea2.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/animalese/placeholder/u.wav b/sound/dialogue/animalese/placeholder/u.wav new file mode 100644 index 0000000..81ebbe5 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/u.wav differ diff --git a/sound/dialogue/animalese/placeholder/u.wav.import b/sound/dialogue/animalese/placeholder/u.wav.import new file mode 100644 index 0000000..8ccd096 --- /dev/null +++ b/sound/dialogue/animalese/placeholder/u.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://bln1bkdu20muo" +path="res://.godot/imported/u.wav-06e44c423f3d1cd6cdd34b54270211bb.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/u.wav" +dest_files=["res://.godot/imported/u.wav-06e44c423f3d1cd6cdd34b54270211bb.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/animalese/placeholder/v.wav b/sound/dialogue/animalese/placeholder/v.wav new file mode 100644 index 0000000..d5c0d12 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/v.wav differ diff --git a/sound/dialogue/animalese/placeholder/v.wav.import b/sound/dialogue/animalese/placeholder/v.wav.import new file mode 100644 index 0000000..ff876a8 --- /dev/null +++ b/sound/dialogue/animalese/placeholder/v.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://y3g1s3pw3bn0" +path="res://.godot/imported/v.wav-5a9986e91dbdd0da4f0583b35a986b1c.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/v.wav" +dest_files=["res://.godot/imported/v.wav-5a9986e91dbdd0da4f0583b35a986b1c.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/animalese/placeholder/w.wav b/sound/dialogue/animalese/placeholder/w.wav new file mode 100644 index 0000000..5122f9c Binary files /dev/null and b/sound/dialogue/animalese/placeholder/w.wav differ diff --git a/sound/dialogue/animalese/placeholder/w.wav.import b/sound/dialogue/animalese/placeholder/w.wav.import new file mode 100644 index 0000000..36f4e0f --- /dev/null +++ b/sound/dialogue/animalese/placeholder/w.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://5ml2iuf8cpgt" +path="res://.godot/imported/w.wav-d3e9035a1fc5e7946be782a0d4f95e64.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/w.wav" +dest_files=["res://.godot/imported/w.wav-d3e9035a1fc5e7946be782a0d4f95e64.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/animalese/placeholder/x.wav b/sound/dialogue/animalese/placeholder/x.wav new file mode 100644 index 0000000..e00f085 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/x.wav differ diff --git a/sound/dialogue/animalese/placeholder/x.wav.import b/sound/dialogue/animalese/placeholder/x.wav.import new file mode 100644 index 0000000..c91ffb4 --- /dev/null +++ b/sound/dialogue/animalese/placeholder/x.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://b5al0m4de1m5x" +path="res://.godot/imported/x.wav-b1beaddcc7d6663882ddd8b86e65db56.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/x.wav" +dest_files=["res://.godot/imported/x.wav-b1beaddcc7d6663882ddd8b86e65db56.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/animalese/placeholder/y.wav b/sound/dialogue/animalese/placeholder/y.wav new file mode 100644 index 0000000..07b45ef Binary files /dev/null and b/sound/dialogue/animalese/placeholder/y.wav differ diff --git a/sound/dialogue/animalese/placeholder/y.wav.import b/sound/dialogue/animalese/placeholder/y.wav.import new file mode 100644 index 0000000..2c7d8d3 --- /dev/null +++ b/sound/dialogue/animalese/placeholder/y.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://c6hust77yamoe" +path="res://.godot/imported/y.wav-5ef178912b2921bb7c790b12e2e82337.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/y.wav" +dest_files=["res://.godot/imported/y.wav-5ef178912b2921bb7c790b12e2e82337.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/animalese/placeholder/z.wav b/sound/dialogue/animalese/placeholder/z.wav new file mode 100644 index 0000000..8f94654 Binary files /dev/null and b/sound/dialogue/animalese/placeholder/z.wav differ diff --git a/sound/dialogue/animalese/placeholder/z.wav.import b/sound/dialogue/animalese/placeholder/z.wav.import new file mode 100644 index 0000000..12b1b54 --- /dev/null +++ b/sound/dialogue/animalese/placeholder/z.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://djvv63b0jlll" +path="res://.godot/imported/z.wav-3e6ff24f53b1014b381cc80ee9fb1a84.sample" + +[deps] + +source_file="res://sound/dialogue/animalese/placeholder/z.wav" +dest_files=["res://.godot/imported/z.wav-3e6ff24f53b1014b381cc80ee9fb1a84.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/ui/animalese_voice.gd b/ui/animalese_voice.gd new file mode 100644 index 0000000..b963a47 --- /dev/null +++ b/ui/animalese_voice.gd @@ -0,0 +1,123 @@ +class_name AnimaleseVoice +extends Node + +const SAMPLE_DIRECTORY := "res://sound/dialogue/animalese/placeholder" +const SUPPORTED_CHARACTERS := "abcdefghijklmnopqrstuvwxyz" +const DEFAULT_CHARACTERS_PER_SECOND: float = 28.0 +const POLYPHONY: int = 8 +const VoiceProfilesType = preload( + "res://player/animalese_voice_profiles.gd" +) +const TypewriterRevealType = preload("res://ui/typewriter_reveal.gd") + +var base_pitch: float = 1.0 +var volume_db: float = -13.0 + +var _samples: Dictionary[String, AudioStream] = {} +var _player: AudioStreamPlayer +var _playback: AudioStreamPlaybackPolyphonic + + +func _ready() -> void: + _load_samples() + var polyphonic_stream := AudioStreamPolyphonic.new() + polyphonic_stream.polyphony = POLYPHONY + _player = AudioStreamPlayer.new() + _player.name = "AnimaleseAudio" + _player.stream = polyphonic_stream + add_child(_player) + _player.play() + _playback = ( + _player.get_stream_playback() as AudioStreamPlaybackPolyphonic + ) + + +func speak_text( + owner: Node, + text: String, + voice_key: String, + voice_profile_id: String = VoiceProfilesType.DEFAULT_ID, + characters_per_second: float = -1.0, +) -> Tween: + var speech_tween := owner.create_tween() + var resolved_characters_per_second := ( + characters_per_second + if characters_per_second > 0.0 + else TypewriterRevealType.get_characters_per_second() + ) + var character_seconds := 1.0 / maxf( + resolved_characters_per_second, + 1.0, + ) + var voice_pitch := ( + base_pitch * VoiceProfilesType.pitch_for(voice_profile_id) + ) + for character_index: int in range(text.length()): + speech_tween.tween_interval(character_seconds) + speech_tween.tween_callback( + _play_character.bind( + text.substr(character_index, 1), + character_index, + text, + voice_key, + voice_pitch, + ) + ) + return speech_tween + + +func _load_samples() -> void: + for character_index: int in range(SUPPORTED_CHARACTERS.length()): + var character := SUPPORTED_CHARACTERS.substr(character_index, 1) + _load_sample(character) + _load_sample("fallback") + + +func _load_sample(sample_name: String) -> void: + var sample_path := "%s/%s.wav" % [SAMPLE_DIRECTORY, sample_name] + var sample := load(sample_path) as AudioStream + if sample != null: + _samples[sample_name] = sample + + +func _play_character( + character: String, + character_index: int, + full_text: String, + voice_key: String, + voice_pitch: float, +) -> void: + if _playback == null or character.strip_edges().is_empty(): + return + var normalized := character.to_lower() + if normalized in [".", ",", "!", "?", ":", ";", "-", "_"]: + return + var sample_name := ( + normalized if SUPPORTED_CHARACTERS.contains(normalized) else "fallback" + ) + var sample: AudioStream = _samples.get(sample_name) + if sample == null: + return + var speaker_variation := ( + float(posmod(hash(voice_key), 1001)) / 1000.0 - 0.5 + ) * 0.16 + var character_variation := ( + float(posmod(hash("%s:%d" % [voice_key, character_index]), 1001)) + / 1000.0 + - 0.5 + ) * 0.10 + var question_lift := 0.0 + if full_text.ends_with("?") and character_index >= full_text.length() * 3 / 4: + var final_progress := ( + float(character_index) / maxf(float(full_text.length() - 1), 1.0) + ) + question_lift = lerpf(0.0, 0.14, final_progress) + var pitch := clampf( + voice_pitch + + speaker_variation + + character_variation + + question_lift, + 0.72, + 1.45, + ) + _playback.play_stream(sample, 0.0, volume_db, pitch) diff --git a/ui/animalese_voice.gd.uid b/ui/animalese_voice.gd.uid new file mode 100644 index 0000000..4cfdf9b --- /dev/null +++ b/ui/animalese_voice.gd.uid @@ -0,0 +1 @@ +uid://djst00wf8yntj diff --git a/ui/assets/logbook/ui_logbook.png b/ui/assets/logbook/ui_logbook.png new file mode 100644 index 0000000..2e6126d Binary files /dev/null and b/ui/assets/logbook/ui_logbook.png differ diff --git a/ui/assets/logbook/ui_logbook.png.import b/ui/assets/logbook/ui_logbook.png.import new file mode 100644 index 0000000..1ffd643 --- /dev/null +++ b/ui/assets/logbook/ui_logbook.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bnwbcopsjmi3s" +path="res://.godot/imported/ui_logbook.png-e45558834671114c04857bb4a6091575.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/assets/logbook/ui_logbook.png" +dest_files=["res://.godot/imported/ui_logbook.png-e45558834671114c04857bb4a6091575.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index 6cba3c4..3268b28 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -6,6 +6,7 @@ const RECENT_SECONDS: float = 8.0 const SPEECH_SECONDS: float = 6.0 const DRAFT_SAVE_DELAY: float = 0.4 const IDLE_ALPHA: float = 0.58 +const CHAT_SURFACE_COLOR := Color(0.025, 0.13, 0.19, 0.94) const ORIGINAL_PANEL_WIDTH: float = 390.0 const PANEL_WIDTH: float = ORIGINAL_PANEL_WIDTH * 0.85 const COMPACT_HEIGHT: float = 250.0 @@ -33,12 +34,31 @@ const CLOCK_SIZE := Vector2(116.0, 34.0) const CLOCK_EDGE_MARGIN: float = 10.0 const WEATHER_ICON_SIZE := Vector2(34.0, 34.0) const WEATHER_ICON_GAP: float = 6.0 +const CHAT_SHOW_ICON: Texture2D = preload( + "res://ui/icons/pictograms/arrow_light_right_more.png" +) +const CHAT_HIDE_ICON: Texture2D = preload( + "res://ui/icons/pictograms/arrow_light_left_more.png" +) +const CHAT_EXPAND_ICON: Texture2D = preload( + "res://ui/icons/pictograms/arrow_light_up_more.png" +) +const CHAT_COMPACT_ICON: Texture2D = preload( + "res://ui/icons/pictograms/arrow_light_down_more.png" +) const WorldTimeServiceType = preload("res://world/world_time_service.gd") const WorldWeatherServiceType = preload( "res://world/world_weather_service.gd" ) const WeatherIconType = preload("res://ui/weather_icon.gd") +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" +) + + enum PresentationState { COLLAPSED, COMPACT, @@ -62,6 +82,7 @@ var _height_button: Button var _unread_indicator: Label var _hint: Label var _speech_layer: Control +var _animalese_voice: AnimaleseVoiceType var _clock_panel: PanelContainer var _clock_label: Label var _weather_icon: WeatherIconType @@ -379,6 +400,15 @@ func _build_ui() -> void: _collapse_button.mouse_filter = Control.MOUSE_FILTER_STOP _collapse_button.focus_mode = Control.FOCUS_ALL _collapse_button.z_index = 3 + _collapse_button.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + _collapse_button.expand_icon = true + _collapse_button.alignment = HORIZONTAL_ALIGNMENT_CENTER + _collapse_button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER + _collapse_button.vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER + _collapse_button.add_theme_constant_override( + "icon_max_width", + roundi(minf(HANDLE_SIZE.x, HANDLE_SIZE.y) * 0.5), + ) _collapse_button.tooltip_text = "Collapse chat" _collapse_button.pressed.connect(func() -> void: if _presentation_state == PresentationState.COLLAPSED: @@ -405,6 +435,15 @@ func _build_ui() -> void: _height_button.mouse_filter = Control.MOUSE_FILTER_STOP _height_button.focus_mode = Control.FOCUS_ALL _height_button.z_index = 3 + _height_button.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + _height_button.expand_icon = true + _height_button.alignment = HORIZONTAL_ALIGNMENT_CENTER + _height_button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER + _height_button.vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER + _height_button.add_theme_constant_override( + "icon_max_width", + roundi(minf(HANDLE_SIZE.x, HANDLE_SIZE.y) * 0.5), + ) _height_button.pressed.connect(func() -> void: if _presentation_state == PresentationState.COLLAPSED: _visible_state_before_collapse = ( @@ -461,6 +500,9 @@ func _build_ui() -> void: _speech_layer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _speech_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_speech_layer) + _animalese_voice = AnimaleseVoiceType.new() + _animalese_voice.name = "PlayerAnimaleseVoice" + add_child(_animalese_voice) _draft_save_timer = Timer.new() _draft_save_timer.one_shot = true _draft_save_timer.wait_time = DRAFT_SAVE_DELAY @@ -478,7 +520,7 @@ func _build_ui() -> void: func _chat_panel_style() -> StyleBoxFlat: - var style := _borderless_style(Color(0.025, 0.13, 0.19, 0.94)) + var style := _borderless_style(CHAT_SURFACE_COLOR) if _mobile_mode: style.corner_radius_top_left = 0 style.corner_radius_top_right = 0 @@ -509,7 +551,7 @@ func _refresh_input_ownership() -> void: func _clock_panel_style() -> StyleBoxFlat: - var style := _borderless_style(Color(0.025, 0.13, 0.19, 0.94)) + var style := _borderless_style(CHAT_SURFACE_COLOR) style.set_corner_radius_all(12) style.content_margin_left = 10 style.content_margin_right = 10 @@ -522,6 +564,10 @@ func _borderless_style(color: Color) -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = color style.set_border_width_all(0) + style.anti_aliasing = false + style.shadow_size = 0 + style.shadow_color = Color.TRANSPARENT + style.shadow_offset = Vector2.ZERO return style @@ -530,6 +576,12 @@ func _flat_chat_button_style(color: Color) -> StyleBoxFlat: style.bg_color = color style.set_border_width_all(0) style.set_corner_radius_all(0) + if _dock_right: + style.corner_radius_top_left = 10 + style.corner_radius_bottom_left = 10 + else: + style.corner_radius_top_right = 10 + style.corner_radius_bottom_right = 10 style.anti_aliasing = false style.shadow_size = 0 style.shadow_color = Color.TRANSPARENT @@ -572,6 +624,18 @@ func _apply_flat_chat_button(button: Button) -> void: button.add_theme_stylebox_override( "disabled", _flat_chat_button_style(UtilityPageStyle.OCEAN_DISABLED) ) + for state: StringName in [ + &"normal", + &"hover", + &"pressed", + &"hover_pressed", + &"focus", + &"disabled", + ]: + button.add_theme_stylebox_override( + state, + _flat_chat_button_style(CHAT_SURFACE_COLOR), + ) func _speech_panel_style() -> StyleBoxFlat: @@ -605,6 +669,8 @@ func _send() -> void: if _send_pending: return var body := _entry.text + if _handle_chat_command(body): + return _send_pending = true _pending_send_body = NetworkChatProtocol.sanitize_body(body) _entry.editable = false @@ -616,6 +682,59 @@ func _send() -> void: _set_status("Sending…") +func _handle_chat_command(body: String) -> bool: + var command_text := body.strip_edges() + if not command_text.begins_with("/"): + return false + var parts: PackedStringArray = command_text.split(" ", false) + var command := String(parts[0]).trim_prefix("/").to_lower() + match command: + "time": + _handle_time_command(parts) + "": + _set_status("Enter a command after /.") + _: + _set_status("Unknown command: /%s" % command) + _entry.clear() + _flush_draft() + return true + + +func _handle_time_command(parts: PackedStringArray) -> void: + if parts.size() != 2: + _set_status("Usage: /time [dawn, day, dusk, night]") + return + if _session == null or not _session.is_host(): + _set_status("Only the host can change world time.") + return + if _world_time == null: + _set_status("World time is unavailable.") + return + var phase_name := String(parts[1]).to_lower() + var target_hour: float + match phase_name: + "dawn": + target_hour = WorldTimeServiceType.DAWN_START_HOUR + "day": + target_hour = WorldTimeServiceType.DAWN_END_HOUR + "dusk": + target_hour = WorldTimeServiceType.DUSK_START_HOUR + "night": + target_hour = WorldTimeServiceType.DUSK_END_HOUR + _: + _set_status("Usage: /time [dawn, day, dusk, night]") + return + if not _world_time.set_authoritative_time(target_hour): + _set_status("World time could not be changed.") + return + _set_status("") + _service.broadcast_system_message( + "World time set to %s (%s)." + % [phase_name, _world_time.get_clock_text()] + ) + close_chat() + + func _on_local_message_confirmed(message: Dictionary) -> void: if ( not _send_pending @@ -664,10 +783,27 @@ func _on_message(message: Dictionary) -> void: ) bubble.add_child(label) _speech_layer.add_child(bubble) + var reveal_seconds := TypewriterRevealType.start(label) + var voice_profile_id: String = VoiceProfilesType.DEFAULT_ID + var speaker_avatar := _spawn.get_avatar(peer_id) + if speaker_avatar != null: + voice_profile_id = VoiceProfilesType.sanitized_id( + speaker_avatar.get_animalese_voice_id() + ) + _animalese_voice.speak_text( + label, + label.text, + str(peer_id), + voice_profile_id, + ) _speech[peer_id] = { "bubble": bubble, "pointer": pointer, - "expires": Time.get_ticks_msec() / 1000.0 + SPEECH_SECONDS, + "expires": ( + Time.get_ticks_msec() / 1000.0 + + reveal_seconds + + SPEECH_SECONDS + ), "fingerprint": str(message.get("sender_fingerprint", "")), } @@ -770,6 +906,8 @@ func set_dock_right(should_dock_right: bool) -> void: if not is_node_ready() or _panel == null: return _panel.add_theme_stylebox_override("panel", _chat_panel_style()) + _apply_flat_chat_button(_collapse_button) + _apply_flat_chat_button(_height_button) _refresh_handle_labels(_presentation_state) _layout_presentation(false) @@ -800,21 +938,24 @@ func is_mobile_mode() -> bool: func _refresh_handle_labels(state: PresentationState) -> void: var collapsed := state == PresentationState.COLLAPSED var height_state := _visible_state_before_collapse if collapsed else state - if _mobile_mode: - _collapse_button.text = "v" if collapsed else "^" - elif _dock_right: - _collapse_button.text = "<" if collapsed else ">" - else: - _collapse_button.text = ">" if collapsed else "<" + _collapse_button.text = "" + _collapse_button.icon = CHAT_SHOW_ICON if collapsed else CHAT_HIDE_ICON _collapse_button.tooltip_text = ( "Show chat" if collapsed else "Hide chat" ) - _height_button.text = "v" if height_state == PresentationState.EXPANDED else "^" + _collapse_button.accessibility_name = _collapse_button.tooltip_text + _height_button.text = "" + _height_button.icon = ( + CHAT_COMPACT_ICON + if height_state == PresentationState.EXPANDED + else CHAT_EXPAND_ICON + ) _height_button.tooltip_text = ( "Compact chat" if height_state == PresentationState.EXPANDED else "Expand chat history" ) + _height_button.accessibility_name = _height_button.tooltip_text func _update_panel_opacity(immediate_recheck: bool = false) -> void: @@ -823,10 +964,8 @@ func _update_panel_opacity(immediate_recheck: bool = false) -> void: if _presentation_state == PresentationState.COLLAPSED: if _height_tween == null or not _height_tween.is_running(): _panel.modulate.a = IDLE_ALPHA - _collapse_button.modulate.a = ( - 1.0 if _collapsed_has_unread else 0.82 - ) - _height_button.modulate.a = 0.82 + _collapse_button.modulate.a = IDLE_ALPHA + _height_button.modulate.a = IDLE_ALPHA return var recent := ( Time.get_ticks_msec() / 1000.0 - _last_message_time < RECENT_SECONDS diff --git a/ui/components/bubble_menu/bubble_button.gd b/ui/components/bubble_menu/bubble_button.gd index 15acb0e..32cdef3 100644 --- a/ui/components/bubble_menu/bubble_button.gd +++ b/ui/components/bubble_menu/bubble_button.gd @@ -1,6 +1,8 @@ class_name BubbleButton extends Button +const ICON_FILL_RATIO: float = 0.76 + @export var profile: BubbleMenuProfile @export_group("Authored Layout") @@ -37,6 +39,7 @@ func _ready() -> void: resized.connect(_update_pivot) _update_pivot() apply_profile() + _apply_icon_presentation(neutral_size) func apply_layout( @@ -49,6 +52,7 @@ func apply_layout( neutral_position = position presented_size = bubble_size _update_pivot() + _apply_icon_presentation(bubble_size) var label_control: Control = get_label_control() var font_size := clampi( roundi(minf(bubble_size.x, bubble_size.y) * font_size_ratio), @@ -63,6 +67,9 @@ func get_layout_size(layout_scale: float, compact: bool) -> Vector2: if compact: bubble_size.x = maxf(bubble_size.x, compact_minimum_size.x) bubble_size.y = maxf(bubble_size.y, compact_minimum_size.y) + if _uses_icon_only_presentation(): + var diameter: float = maxf(bubble_size.x, bubble_size.y) + bubble_size = Vector2(diameter, diameter) return bubble_size @@ -78,6 +85,24 @@ func get_label_control() -> Control: return self +func _uses_icon_only_presentation() -> bool: + return icon != null and text.is_empty() and label_control_path.is_empty() + + +func _apply_icon_presentation(bubble_size: Vector2) -> void: + if not _uses_icon_only_presentation(): + return + alignment = HORIZONTAL_ALIGNMENT_CENTER + icon_alignment = HORIZONTAL_ALIGNMENT_CENTER + vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER + expand_icon = true + texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + add_theme_constant_override( + "icon_max_width", + maxi(1, roundi(minf(bubble_size.x, bubble_size.y) * ICON_FILL_RATIO)), + ) + + func advance_emphasis(delta: float) -> void: var target: float = 1.0 if _hovered or _focused else 0.0 emphasis = move_toward( @@ -122,6 +147,8 @@ func calculate_visual_scale( * motion_scale * lerpf(1.0, profile.emphasized_deformation_scale, emphasis) ) + if _uses_icon_only_presentation(): + deformation_amount = 0.0 var hover_scale: float = lerpf( 1.0, profile.hover_focus_scale, diff --git a/ui/fishing_shop.tscn b/ui/fishing_shop.tscn index 2b1914d..80f6e21 100644 --- a/ui/fishing_shop.tscn +++ b/ui/fishing_shop.tscn @@ -5,7 +5,7 @@ [ext_resource type="Texture2D" path="res://items/icons/placeholder/reel_speed_upgrade.svg" id="3_reel"] [ext_resource type="Texture2D" path="res://items/icons/placeholder/barrier_power_upgrade.svg" id="4_barrier"] [ext_resource type="Texture2D" path="res://items/icons/placeholder/fish_coin.svg" id="5_coin"] -[ext_resource type="Texture2D" path="res://items/icons/placeholder/cooler_expansion.svg" id="6_cooler"] +[ext_resource type="Texture2D" path="res://items/icons/equipment/64_cooler_plus.png" id="6_cooler"] [node name="FishingShop" type="Control"] unique_name_in_owner = true @@ -196,7 +196,7 @@ layout_mode = 2 theme_override_constants/separation = 9 [node name="Icon" type="TextureRect" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row"] -custom_minimum_size = Vector2(44, 44) +custom_minimum_size = Vector2(88, 88) layout_mode = 2 texture = ExtResource("3_reel") expand_mode = 1 @@ -250,7 +250,7 @@ layout_mode = 2 theme_override_constants/separation = 9 [node name="Icon" type="TextureRect" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row"] -custom_minimum_size = Vector2(44, 44) +custom_minimum_size = Vector2(88, 88) layout_mode = 2 texture = ExtResource("4_barrier") expand_mode = 1 @@ -304,7 +304,7 @@ layout_mode = 2 theme_override_constants/separation = 9 [node name="Icon" type="TextureRect" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row"] -custom_minimum_size = Vector2(44, 44) +custom_minimum_size = Vector2(88, 88) layout_mode = 2 texture = ExtResource("6_cooler") expand_mode = 1 diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 830f291..a684e8a 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -12,6 +12,7 @@ const PlayerMenuType = preload("res://ui/player_menu.gd") const PlayerType = preload("res://player/player.gd") const PlayerWalletType = preload("res://economy/player_wallet.gd") const ItemCatalogType = preload("res://items/item_catalog.gd") +const ItemDataType = preload("res://items/item_data.gd") const PlayerBagType = preload("res://inventory/player_bag.gd") const PlayerHotbarType = preload("res://inventory/player_hotbar.gd") const HotbarUIType = preload("res://ui/hotbar.gd") @@ -57,6 +58,12 @@ const PlayerExperienceType = preload( const UIReferencePresentationType = preload( "res://ui/ui_reference_presentation.gd" ) +const MAIN_BUBBLE_PROFILE: BubbleMenuProfile = preload( + "res://ui/components/bubble_menu/bubble_menu_profile.tres" +) +const ACTIVE_BAIT_EMPTY_ICON: Texture2D = preload( + "res://ui/icons/pictograms/x_dark.png" +) signal pixelation_settings_visibility_changed(is_visible: bool) signal crisp_reset_focus_requested @@ -85,6 +92,9 @@ const FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET: float = -104.0 @onready var _status_label: Label = %StatusLabel @onready var _gameplay_transient_hud: Control = %GameplayTransientHUD +@onready var _active_bait_button: Button = %ActiveBaitButton +@onready var _active_bait_quantity_badge: Panel = %ActiveBaitQuantityBadge +@onready var _active_bait_quantity: Label = %ActiveBaitQuantity @onready var _experience_presentation: Control = %ExperiencePresentation @onready var _catch_track: Control = %CatchTrack @onready var _green_catch_progress: ProgressBar = %GreenCatchProgress @@ -101,6 +111,13 @@ const FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET: float = -104.0 @onready var _experience_progress: ProgressBar = %ExperienceProgress @onready var _experience_bubble: PanelContainer = %ExperienceBubble @onready var _experience_bubble_label: Label = %ExperienceBubbleLabel +const TypewriterRevealType = preload("res://ui/typewriter_reveal.gd") +const AnimaleseVoiceType = preload("res://ui/animalese_voice.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 + + @onready var _canonical_stage: Control = %CanonicalStage @onready var _ui_root: Control = %UIRoot @onready var _player_menu: PlayerMenuType = %PlayerMenu @@ -109,7 +126,13 @@ const FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET: float = -104.0 @onready var _pause_menu: PauseMenuType = %PauseMenu @onready var _hotbar_ui: HotbarUIType = %Hotbar @onready var _fishing_shop: FishingShopType = %FishingShop -@onready var _shop_prompt: PanelContainer = %ShopPrompt +@onready var _shop_prompt: Control = %ShopPrompt +@onready var _shop_prompt_bubble: PanelContainer = %ShopPromptBubble +@onready var _shop_prompt_message: Label = %ShopPromptMessage +@onready var _shop_prompt_key_badge: Panel = %ShopPromptKeyBadge +@onready var _shop_prompt_key: Label = %ShopPromptKey +@onready var _shop_prompt_pointer: Polygon2D = %ShopPromptPointer +var _shop_animalese_voice: AnimaleseVoiceType @onready var _effect_status: Label = %EffectStatus @onready var _chat_ui: ChatUIType = %ChatUI @onready var _emote_radial_menu: EmoteRadialMenuType = %EmoteRadialMenu @@ -129,6 +152,8 @@ const FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET: float = -104.0 var _showcase_active: bool = false var _player: PlayerType +var _bag: PlayerBagType +var _item_catalog: ItemCatalogType var _player_menu_open: bool = false var _gameplay_ui_enabled: bool = false var _fishing_spot: FishingSpotType @@ -163,6 +188,8 @@ var _settings_manager: PlayerSettingsManagerType func _ready() -> void: + _apply_active_bait_indicator_style() + _refresh_active_bait_indicator() # Reward feedback must remain above full-screen canonical menus. Keeping the # overlay as the final stage child makes that ownership explicit instead of # relying on scene declaration order when another menu adds high-z children. @@ -236,10 +263,27 @@ func setup( world_sun: DirectionalLight3D, ) -> void: _player = player + _bag = bag + _item_catalog = item_catalog _settings_manager = settings_manager _fishing_spot = fishing_spot _item_effects = item_effects _experience = experience + if ( + _player != null + and not _player.active_bait_changed.is_connected( + _on_hud_active_bait_changed + ) + ): + _player.active_bait_changed.connect(_on_hud_active_bait_changed) + if ( + _bag != null + and not _bag.contents_changed.is_connected( + _on_hud_bait_inventory_changed + ) + ): + _bag.contents_changed.connect(_on_hud_bait_inventory_changed) + _refresh_active_bait_indicator() if ( _experience != null and not _experience.experience_awarded.is_connected( @@ -1174,6 +1218,88 @@ func get_pause_menu() -> PauseMenuType: return _pause_menu +func _apply_active_bait_indicator_style() -> void: + var normal_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_normal_style() + var hover_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_hover_style() + var pressed_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_pressed_style() + var disabled_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_disabled_style() + for style: StyleBoxFlat in [ + normal_style, + hover_style, + pressed_style, + disabled_style, + ]: + style.set_corner_radius_all(36) + style.content_margin_left = 13.5 + style.content_margin_top = 13.5 + style.content_margin_right = 13.5 + style.content_margin_bottom = 13.5 + _active_bait_button.add_theme_stylebox_override("normal", normal_style) + _active_bait_button.add_theme_stylebox_override("hover", hover_style) + _active_bait_button.add_theme_stylebox_override("focus", hover_style) + _active_bait_button.add_theme_stylebox_override("pressed", pressed_style) + _active_bait_button.add_theme_stylebox_override("disabled", disabled_style) + for state: StringName in [ + &"icon_normal_color", + &"icon_hover_color", + &"icon_focus_color", + &"icon_pressed_color", + ]: + _active_bait_button.add_theme_color_override(state, Color.WHITE) + var badge_style := StyleBoxFlat.new() + badge_style.bg_color = Color("0b5558") + badge_style.set_corner_radius_all(12) + badge_style.anti_aliasing = false + _active_bait_quantity_badge.add_theme_stylebox_override( + "panel", + badge_style, + ) + + +func _refresh_active_bait_indicator() -> void: + if not is_node_ready(): + return + var item: ItemDataType + if ( + _player != null + and _item_catalog != null + and not _player.active_bait_id.is_empty() + ): + item = _item_catalog.get_item_by_id(_player.active_bait_id) + var has_active_bait: bool = item != null and item.is_bait() + _active_bait_button.icon = ( + item.icon + if has_active_bait and item.icon != null + else ACTIVE_BAIT_EMPTY_ICON + ) + _active_bait_quantity_badge.visible = has_active_bait + if not has_active_bait: + _active_bait_button.tooltip_text = "no bait selected" + return + var quantity: int = ( + _bag.get_quantity(item.item_id) + if _bag != null + else 0 + ) + _active_bait_quantity.text = str(quantity) + _active_bait_quantity.add_theme_font_size_override( + "font_size", + 13 if quantity >= 100 else 15, + ) + _active_bait_button.tooltip_text = "%s ×%d" % [ + item.display_name, + quantity, + ] + + +func _on_hud_active_bait_changed(_item_id: StringName) -> void: + _refresh_active_bait_indicator() + + +func _on_hud_bait_inventory_changed() -> void: + _refresh_active_bait_indicator() + + func set_gameplay_ui_enabled(enabled: bool) -> void: _gameplay_ui_enabled = enabled _gameplay_transient_hud.visible = enabled and not _player_menu_open @@ -1231,7 +1357,12 @@ func get_fishing_shop() -> FishingShopType: return _fishing_shop -func set_shop_prompt_visible(is_visible: bool) -> void: +func set_shop_prompt_visible( + is_visible: bool, + world_anchor: Vector3 = Vector3(0.0, INF, 0.0), +) -> void: + _apply_shop_prompt_style() + var was_visible := _shop_prompt.visible _shop_prompt.visible = ( is_visible and _gameplay_ui_enabled @@ -1239,6 +1370,112 @@ func set_shop_prompt_visible(is_visible: bool) -> void: and not _player_menu_open and not _shop_open ) + if _shop_prompt.visible: + if not was_visible: + _ensure_shop_animalese_voice() + TypewriterRevealType.start( + _shop_prompt_message, + SHOP_SPEECH_CHARACTERS_PER_SECOND, + ) + _shop_animalese_voice.speak_text( + _shop_prompt_message, + _shop_prompt_message.text, + "shopkeeper", + SHOP_ANIMALESE_VOICE_ID, + SHOP_SPEECH_CHARACTERS_PER_SECOND, + ) + if world_anchor.is_finite(): + _position_shop_prompt(world_anchor) + + +func _ensure_shop_animalese_voice() -> void: + if _shop_animalese_voice != null: + return + _shop_animalese_voice = AnimaleseVoiceType.new() + _shop_animalese_voice.name = "ShopAnimaleseVoice" + _shop_animalese_voice.base_pitch = SHOP_ANIMALESE_BASE_PITCH + add_child(_shop_animalese_voice) + + +func _apply_shop_prompt_style() -> void: + if _shop_prompt.has_meta(&"shop_prompt_styled"): + return + _shop_prompt.set_meta(&"shop_prompt_styled", true) + var bubble_style := StyleBoxFlat.new() + bubble_style.bg_color = Color(UtilityPageStyle.OCEAN_PANEL_MID, 0.96) + bubble_style.set_border_width_all(0) + bubble_style.set_corner_radius_all(12) + bubble_style.anti_aliasing = false + _shop_prompt_bubble.add_theme_stylebox_override("panel", bubble_style) + var badge_style := StyleBoxFlat.new() + badge_style.bg_color = Color("0b5558") + badge_style.set_border_width_all(0) + badge_style.set_corner_radius_all(12) + badge_style.anti_aliasing = false + _shop_prompt_key_badge.add_theme_stylebox_override("panel", badge_style) + for label: Label in [_shop_prompt_message, _shop_prompt_key]: + label.add_theme_font_override("font", UtilityPageStyle.TuffyFont) + label.add_theme_color_override( + "font_color", + UtilityPageStyle.OCEAN_TEXT_PRIMARY, + ) + _shop_prompt_pointer.color = Color( + UtilityPageStyle.OCEAN_PANEL_MID, + 0.96, + ) + + +func _position_shop_prompt(world_anchor: Vector3) -> void: + if _player == null: + _shop_prompt.hide() + return + var camera: Camera3D = _player.get_gameplay_camera() + if camera == null or camera.is_position_behind(world_anchor): + _shop_prompt.hide() + return + var camera_viewport_size := camera.get_viewport().get_visible_rect().size + var ui_viewport_size: Vector2 = _canonical_stage.size + if camera_viewport_size.x <= 0.0 or camera_viewport_size.y <= 0.0: + _shop_prompt.hide() + return + var screen_position := ( + camera.unproject_position(world_anchor) + * ui_viewport_size + / camera_viewport_size + ) + if ( + screen_position.x < -80.0 + or screen_position.x > ui_viewport_size.x + 80.0 + or screen_position.y < -80.0 + or screen_position.y > ui_viewport_size.y + 80.0 + ): + _shop_prompt.hide() + return + var pointer_height := 10.0 + var desired := screen_position - Vector2( + _shop_prompt.size.x * 0.5, + _shop_prompt.size.y + pointer_height, + ) + _shop_prompt.position = Vector2( + clampf( + desired.x, + 8.0, + ui_viewport_size.x - _shop_prompt.size.x - 8.0, + ), + clampf( + desired.y, + 8.0, + ui_viewport_size.y - _shop_prompt.size.y - pointer_height - 8.0, + ), + ) + _shop_prompt_pointer.position = Vector2( + clampf( + screen_position.x - _shop_prompt.position.x, + 20.0, + _shop_prompt.size.x - 20.0, + ), + _shop_prompt.size.y - 1.0, + ) func get_screen_fade() -> ScreenFade: diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index 0726da0..ed97ac5 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -82,6 +82,71 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 +[node name="ActiveBaitIndicator" type="Control" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] +unique_name_in_owner = true +z_index = 54 +layout_mode = 1 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -88.0 +offset_top = -88.0 +offset_right = -16.0 +offset_bottom = -16.0 +grow_horizontal = 0 +grow_vertical = 0 +mouse_filter = 2 +theme = ExtResource("3_theme") + +[node name="ActiveBaitButton" type="Button" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ActiveBaitIndicator"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +focus_mode = 0 +texture_filter = 1 +expand_icon = true +alignment = 1 +icon_alignment = 1 +vertical_icon_alignment = 1 + +[node name="ActiveBaitQuantityBadge" type="Panel" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ActiveBaitIndicator"] +unique_name_in_owner = true +z_index = 2 +layout_mode = 1 +anchors_preset = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -27.0 +offset_top = 3.0 +offset_right = -3.0 +offset_bottom = 27.0 +grow_horizontal = 0 +mouse_filter = 2 + +[node name="ActiveBaitQuantity" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ActiveBaitIndicator/ActiveBaitQuantityBadge"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 1.0 +offset_right = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +theme_override_colors/font_color = Color(0.905882, 0.960784, 0.956863, 1) +theme_override_font_sizes/font_size = 15 +text = "0" +horizontal_alignment = 1 +vertical_alignment = 1 + [node name="ExperiencePresentation" type="Control" parent="UIRoot/CanonicalStage"] unique_name_in_owner = true z_index = 200 @@ -333,35 +398,67 @@ unique_name_in_owner = true [node name="QuickRadialMenu" parent="UIRoot/CanonicalStage" instance=ExtResource("12_quick")] unique_name_in_owner = true -[node name="ShopPrompt" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] +[node name="ShopPrompt" type="Control" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] unique_name_in_owner = true visible = false z_index = 55 -anchors_preset = 7 -anchor_left = 0.5 -anchor_top = 1.0 -anchor_right = 0.5 -anchor_bottom = 1.0 -offset_left = -150.0 -offset_top = -284.0 -offset_right = 150.0 -offset_bottom = -246.0 -grow_horizontal = 2 -grow_vertical = 0 +offset_right = 300.0 +offset_bottom = 72.0 mouse_filter = 2 theme = ExtResource("3_theme") -[node name="Margin" type="MarginContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ShopPrompt"] +[node name="ShopPromptBubble" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ShopPrompt"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="Margin" type="MarginContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ShopPrompt/ShopPromptBubble"] layout_mode = 2 theme_override_constants/margin_left = 12 -theme_override_constants/margin_top = 7 +theme_override_constants/margin_top = 8 theme_override_constants/margin_right = 12 -theme_override_constants/margin_bottom = 7 +theme_override_constants/margin_bottom = 8 -[node name="Label" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ShopPrompt/Margin"] +[node name="ShopPromptMessage" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ShopPrompt/ShopPromptBubble/Margin"] +unique_name_in_owner = true layout_mode = 2 -text = "press e to open fishing shop" +theme_override_font_sizes/font_size = 18 +text = "Hey, if you've got any fish I've got cash! OwO" horizontal_alignment = 1 +vertical_alignment = 1 +autowrap_mode = 2 + +[node name="ShopPromptKeyBadge" type="Panel" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ShopPrompt"] +unique_name_in_owner = true +layout_mode = 0 +offset_left = 282.0 +offset_top = -8.0 +offset_right = 306.0 +offset_bottom = 16.0 +mouse_filter = 2 + +[node name="ShopPromptKey" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ShopPrompt/ShopPromptKeyBadge"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +theme_override_font_sizes/font_size = 15 +text = "E" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="ShopPromptPointer" type="Polygon2D" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ShopPrompt"] +unique_name_in_owner = true +polygon = PackedVector2Array(-8, 0, 8, 0, 0, 10) [node name="FishingShop" parent="UIRoot/CanonicalStage" instance=ExtResource("8_shop")] unique_name_in_owner = true diff --git a/ui/icons/pictograms/arrow_dark_down_full.png b/ui/icons/pictograms/arrow_dark_down_full.png new file mode 100644 index 0000000..e2bdb9e Binary files /dev/null and b/ui/icons/pictograms/arrow_dark_down_full.png differ diff --git a/ui/icons/pictograms/arrow_dark_down_full.png.import b/ui/icons/pictograms/arrow_dark_down_full.png.import new file mode 100644 index 0000000..2e7726a --- /dev/null +++ b/ui/icons/pictograms/arrow_dark_down_full.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d4aah3j5brkij" +path="res://.godot/imported/arrow_dark_down_full.png-0e7bb59c41a81c0bd02261d6a7b319ef.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/pictograms/arrow_dark_down_full.png" +dest_files=["res://.godot/imported/arrow_dark_down_full.png-0e7bb59c41a81c0bd02261d6a7b319ef.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/pictograms/arrow_dark_up_full.png b/ui/icons/pictograms/arrow_dark_up_full.png new file mode 100644 index 0000000..a9d2f3d Binary files /dev/null and b/ui/icons/pictograms/arrow_dark_up_full.png differ diff --git a/ui/icons/pictograms/arrow_dark_up_full.png.import b/ui/icons/pictograms/arrow_dark_up_full.png.import new file mode 100644 index 0000000..1cbc9ac --- /dev/null +++ b/ui/icons/pictograms/arrow_dark_up_full.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d34x31jx5p5ll" +path="res://.godot/imported/arrow_dark_up_full.png-907f3a8653cf6bf8b6ba95ad31b18e0d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/pictograms/arrow_dark_up_full.png" +dest_files=["res://.godot/imported/arrow_dark_up_full.png-907f3a8653cf6bf8b6ba95ad31b18e0d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/pictograms/arrow_light_down_more.png b/ui/icons/pictograms/arrow_light_down_more.png new file mode 100644 index 0000000..3c7683d Binary files /dev/null and b/ui/icons/pictograms/arrow_light_down_more.png differ diff --git a/ui/icons/pictograms/arrow_light_down_more.png.import b/ui/icons/pictograms/arrow_light_down_more.png.import new file mode 100644 index 0000000..47a516b --- /dev/null +++ b/ui/icons/pictograms/arrow_light_down_more.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cmc7sguttrl2o" +path="res://.godot/imported/arrow_light_down_more.png-a02fb5d09f2022507ddb6c06e3f22732.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/pictograms/arrow_light_down_more.png" +dest_files=["res://.godot/imported/arrow_light_down_more.png-a02fb5d09f2022507ddb6c06e3f22732.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/pictograms/arrow_light_left_more.png b/ui/icons/pictograms/arrow_light_left_more.png new file mode 100644 index 0000000..361adab Binary files /dev/null and b/ui/icons/pictograms/arrow_light_left_more.png differ diff --git a/ui/icons/pictograms/arrow_light_left_more.png.import b/ui/icons/pictograms/arrow_light_left_more.png.import new file mode 100644 index 0000000..07d5a52 --- /dev/null +++ b/ui/icons/pictograms/arrow_light_left_more.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b3lx64prsccyk" +path="res://.godot/imported/arrow_light_left_more.png-a92f7c96dbcafd9c1b2cb612e94fef44.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/pictograms/arrow_light_left_more.png" +dest_files=["res://.godot/imported/arrow_light_left_more.png-a92f7c96dbcafd9c1b2cb612e94fef44.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/pictograms/arrow_light_right_more.png b/ui/icons/pictograms/arrow_light_right_more.png new file mode 100644 index 0000000..39d6148 Binary files /dev/null and b/ui/icons/pictograms/arrow_light_right_more.png differ diff --git a/ui/icons/pictograms/arrow_light_right_more.png.import b/ui/icons/pictograms/arrow_light_right_more.png.import new file mode 100644 index 0000000..e0a72ff --- /dev/null +++ b/ui/icons/pictograms/arrow_light_right_more.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cedstrt57et53" +path="res://.godot/imported/arrow_light_right_more.png-56d3df49848f9fee268e5c4b6c279828.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/pictograms/arrow_light_right_more.png" +dest_files=["res://.godot/imported/arrow_light_right_more.png-56d3df49848f9fee268e5c4b6c279828.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/pictograms/arrow_light_up_more.png b/ui/icons/pictograms/arrow_light_up_more.png new file mode 100644 index 0000000..03e3817 Binary files /dev/null and b/ui/icons/pictograms/arrow_light_up_more.png differ diff --git a/ui/icons/pictograms/arrow_light_up_more.png.import b/ui/icons/pictograms/arrow_light_up_more.png.import new file mode 100644 index 0000000..60f266f --- /dev/null +++ b/ui/icons/pictograms/arrow_light_up_more.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dqu71oklbvofk" +path="res://.godot/imported/arrow_light_up_more.png-7e8472cc9f410835ab24173229e09c9b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/pictograms/arrow_light_up_more.png" +dest_files=["res://.godot/imported/arrow_light_up_more.png-7e8472cc9f410835ab24173229e09c9b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/pictograms/check_mark_dark.png b/ui/icons/pictograms/check_mark_dark.png new file mode 100644 index 0000000..f1c398b Binary files /dev/null and b/ui/icons/pictograms/check_mark_dark.png differ diff --git a/ui/icons/pictograms/check_mark_dark.png.import b/ui/icons/pictograms/check_mark_dark.png.import new file mode 100644 index 0000000..d2fa9e9 --- /dev/null +++ b/ui/icons/pictograms/check_mark_dark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://3x5tryb721th" +path="res://.godot/imported/check_mark_dark.png-d9abda15f33a6a8f8c29c5f148cda086.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/pictograms/check_mark_dark.png" +dest_files=["res://.godot/imported/check_mark_dark.png-d9abda15f33a6a8f8c29c5f148cda086.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/pictograms/check_mark_light.png b/ui/icons/pictograms/check_mark_light.png new file mode 100644 index 0000000..da0af7c Binary files /dev/null and b/ui/icons/pictograms/check_mark_light.png differ diff --git a/ui/icons/pictograms/check_mark_light.png.import b/ui/icons/pictograms/check_mark_light.png.import new file mode 100644 index 0000000..c3af542 --- /dev/null +++ b/ui/icons/pictograms/check_mark_light.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dtibvrik7ug0j" +path="res://.godot/imported/check_mark_light.png-83417416d6580b7b5eb41987b7af5ccf.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/pictograms/check_mark_light.png" +dest_files=["res://.godot/imported/check_mark_light.png-83417416d6580b7b5eb41987b7af5ccf.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/pictograms/online_dark.png b/ui/icons/pictograms/online_dark.png new file mode 100644 index 0000000..1c643ca Binary files /dev/null and b/ui/icons/pictograms/online_dark.png differ diff --git a/ui/icons/pictograms/online_dark.png.import b/ui/icons/pictograms/online_dark.png.import new file mode 100644 index 0000000..f917f4c --- /dev/null +++ b/ui/icons/pictograms/online_dark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://uxkgiwd7py03" +path="res://.godot/imported/online_dark.png-081123db8a385b31ed626b7c95b20c3c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/pictograms/online_dark.png" +dest_files=["res://.godot/imported/online_dark.png-081123db8a385b31ed626b7c95b20c3c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/pictograms/save_dark.png b/ui/icons/pictograms/save_dark.png new file mode 100644 index 0000000..382648b Binary files /dev/null and b/ui/icons/pictograms/save_dark.png differ diff --git a/ui/icons/pictograms/save_dark.png.import b/ui/icons/pictograms/save_dark.png.import new file mode 100644 index 0000000..33bbad8 --- /dev/null +++ b/ui/icons/pictograms/save_dark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b71gds1q8ny61" +path="res://.godot/imported/save_dark.png-6dbadbfe5b8057ede23f77629e3c1226.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/pictograms/save_dark.png" +dest_files=["res://.godot/imported/save_dark.png-6dbadbfe5b8057ede23f77629e3c1226.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/pictograms/settings_dark.png b/ui/icons/pictograms/settings_dark.png new file mode 100644 index 0000000..3777b39 Binary files /dev/null and b/ui/icons/pictograms/settings_dark.png differ diff --git a/ui/icons/pictograms/settings_dark.png.import b/ui/icons/pictograms/settings_dark.png.import new file mode 100644 index 0000000..fabd6c0 --- /dev/null +++ b/ui/icons/pictograms/settings_dark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ul28xl5qed55" +path="res://.godot/imported/settings_dark.png-7196550e6c3f5a58c1402ab0ccc9c9d0.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/pictograms/settings_dark.png" +dest_files=["res://.godot/imported/settings_dark.png-7196550e6c3f5a58c1402ab0ccc9c9d0.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/pictograms/undo_dark.png b/ui/icons/pictograms/undo_dark.png new file mode 100644 index 0000000..e26b579 Binary files /dev/null and b/ui/icons/pictograms/undo_dark.png differ diff --git a/ui/icons/pictograms/undo_dark.png.import b/ui/icons/pictograms/undo_dark.png.import new file mode 100644 index 0000000..e1cce7e --- /dev/null +++ b/ui/icons/pictograms/undo_dark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://deux1h722j33u" +path="res://.godot/imported/undo_dark.png-652c08d6b38e19b70b66927ecfc6222c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/pictograms/undo_dark.png" +dest_files=["res://.godot/imported/undo_dark.png-652c08d6b38e19b70b66927ecfc6222c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/pictograms/undo_light.png b/ui/icons/pictograms/undo_light.png new file mode 100644 index 0000000..aa04205 Binary files /dev/null and b/ui/icons/pictograms/undo_light.png differ diff --git a/ui/icons/pictograms/undo_light.png.import b/ui/icons/pictograms/undo_light.png.import new file mode 100644 index 0000000..161faa8 --- /dev/null +++ b/ui/icons/pictograms/undo_light.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://1qr5t7gx1eag" +path="res://.godot/imported/undo_light.png-83dd161fb13faa93326dee6cb01e205c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/pictograms/undo_light.png" +dest_files=["res://.godot/imported/undo_light.png-83dd161fb13faa93326dee6cb01e205c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/pictograms/x_dark.png b/ui/icons/pictograms/x_dark.png new file mode 100644 index 0000000..d4d183d Binary files /dev/null and b/ui/icons/pictograms/x_dark.png differ diff --git a/ui/icons/pictograms/x_dark.png.import b/ui/icons/pictograms/x_dark.png.import new file mode 100644 index 0000000..36797cb --- /dev/null +++ b/ui/icons/pictograms/x_dark.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cg8vi6sxnpl2t" +path="res://.godot/imported/x_dark.png-5430cf57b7772f059ee1fac9842c6898.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/pictograms/x_dark.png" +dest_files=["res://.godot/imported/x_dark.png-5430cf57b7772f059ee1fac9842c6898.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/pictograms/x_light.png b/ui/icons/pictograms/x_light.png new file mode 100644 index 0000000..68b3180 Binary files /dev/null and b/ui/icons/pictograms/x_light.png differ diff --git a/ui/icons/pictograms/x_light.png.import b/ui/icons/pictograms/x_light.png.import new file mode 100644 index 0000000..36b30e7 --- /dev/null +++ b/ui/icons/pictograms/x_light.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cvb228ypykqaq" +path="res://.godot/imported/x_light.png-b099203c17614ca6f36b269beabfcbab.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/pictograms/x_light.png" +dest_files=["res://.godot/imported/x_light.png-b099203c17614ca6f36b269beabfcbab.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/weather/weather_clear_day.png b/ui/icons/weather/weather_clear_day.png new file mode 100644 index 0000000..266887d Binary files /dev/null and b/ui/icons/weather/weather_clear_day.png differ diff --git a/ui/icons/weather/weather_clear_day.png.import b/ui/icons/weather/weather_clear_day.png.import new file mode 100644 index 0000000..175f21f --- /dev/null +++ b/ui/icons/weather/weather_clear_day.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ce6ge5yypnnpf" +path="res://.godot/imported/weather_clear_day.png-1cd1140830664281637d5589a748ef58.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/weather/weather_clear_day.png" +dest_files=["res://.godot/imported/weather_clear_day.png-1cd1140830664281637d5589a748ef58.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/weather/weather_clear_night_full.png b/ui/icons/weather/weather_clear_night_full.png new file mode 100644 index 0000000..6263b34 Binary files /dev/null and b/ui/icons/weather/weather_clear_night_full.png differ diff --git a/ui/icons/weather/weather_clear_night_full.png.import b/ui/icons/weather/weather_clear_night_full.png.import new file mode 100644 index 0000000..b9cffaf --- /dev/null +++ b/ui/icons/weather/weather_clear_night_full.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://78l0keyrvuiw" +path="res://.godot/imported/weather_clear_night_full.png-3640fcf886e9bd01942e21cc0212bc45.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/weather/weather_clear_night_full.png" +dest_files=["res://.godot/imported/weather_clear_night_full.png-3640fcf886e9bd01942e21cc0212bc45.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/weather/weather_cloudy.png b/ui/icons/weather/weather_cloudy.png new file mode 100644 index 0000000..0e96583 Binary files /dev/null and b/ui/icons/weather/weather_cloudy.png differ diff --git a/ui/icons/weather/weather_cloudy.png.import b/ui/icons/weather/weather_cloudy.png.import new file mode 100644 index 0000000..c74657d --- /dev/null +++ b/ui/icons/weather/weather_cloudy.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b17frlbta1p7f" +path="res://.godot/imported/weather_cloudy.png-c41e4d98007294310395f752b39ddaa6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/weather/weather_cloudy.png" +dest_files=["res://.godot/imported/weather_cloudy.png-c41e4d98007294310395f752b39ddaa6.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/weather/weather_fog.png b/ui/icons/weather/weather_fog.png new file mode 100644 index 0000000..00e93b6 Binary files /dev/null and b/ui/icons/weather/weather_fog.png differ diff --git a/ui/icons/weather/weather_fog.png.import b/ui/icons/weather/weather_fog.png.import new file mode 100644 index 0000000..8887cfb --- /dev/null +++ b/ui/icons/weather/weather_fog.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://wpaj2l01upt6" +path="res://.godot/imported/weather_fog.png-0fe4352261121bbcc8da10ebc1995663.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/weather/weather_fog.png" +dest_files=["res://.godot/imported/weather_fog.png-0fe4352261121bbcc8da10ebc1995663.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/weather/weather_rain.png b/ui/icons/weather/weather_rain.png new file mode 100644 index 0000000..b3de517 Binary files /dev/null and b/ui/icons/weather/weather_rain.png differ diff --git a/ui/icons/weather/weather_rain.png.import b/ui/icons/weather/weather_rain.png.import new file mode 100644 index 0000000..af5d773 --- /dev/null +++ b/ui/icons/weather/weather_rain.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cfqvlqp3nrfok" +path="res://.godot/imported/weather_rain.png-affbb7091de81103538e2740c5b2c762.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/weather/weather_rain.png" +dest_files=["res://.godot/imported/weather_rain.png-affbb7091de81103538e2740c5b2c762.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/logbook_page.gd b/ui/logbook_page.gd index 115b791..504ec1c 100644 --- a/ui/logbook_page.gd +++ b/ui/logbook_page.gd @@ -23,9 +23,26 @@ const HANDWRITTEN_NUMERIC_SCALE: float = 0.8 const PORTRAIT_VIEW_MAX_SIZE := Vector2(860.0, 480.0) const INK := Color("251b10") const MUTED_INK := Color("6d5b45") -const PAPER := Color("f2e6c9") -const PAPER_DARK := Color("e8d7b4") -const LOGBOOK_TAB_LEFT_INSET: float = 34.0 +const LOGBOOK_ARTWORK: Texture2D = preload( + "res://ui/assets/logbook/ui_logbook.png" +) +const SCROLL_UP_TEXTURE: Texture2D = preload( + "res://ui/icons/pictograms/arrow_dark_up_full.png" +) +const SCROLL_DOWN_TEXTURE: Texture2D = preload( + "res://ui/icons/pictograms/arrow_dark_down_full.png" +) +const LOGBOOK_ARTWORK_SOURCE_SIZE := Vector2(512.0, 247.0) +const LEFT_PAGE_CONTENT_RECT := Rect2(56.0, 26.0, 190.0, 198.0) +const RIGHT_PAGE_CONTENT_RECT := Rect2(267.0, 26.0, 210.0, 198.0) +const LOGBOOK_TAB_LEFT_INSET: float = 122.0 +const PAGE_CONTENT_SCALE: float = 0.97 +const CATALOG_PORTRAIT_SIZE := Vector2(78.0, 36.0) +const CATALOG_ENTRY_SIZE := Vector2(92.0, 92.0) +const CATALOG_ROW_STEP: float = 98.0 +const CATALOG_SNAP_DELAY: float = 0.12 +const DETAIL_PORTRAIT_SIZE := Vector2(160.0, 88.0) +const DETAIL_BOTTOM_INSET: float = 35.0 var _collection_log: CollectionLogType var _inventory: FishInventoryType @@ -41,10 +58,14 @@ var _detail_tween: Tween var _category_generation: int = 0 var _category_tween: Tween var _silhouette_material: ShaderMaterial +var _snapping_catalog_scroll: bool = false +var _catalog_scroll_snap_timer: Timer var _category_tabs: Array[Button] = [] var _catalog_scroll: ScrollContainer var _catalog_grid: GridContainer +var _catalog_scroll_up_indicator: TextureRect +var _catalog_scroll_down_indicator: TextureRect var _empty_state: Label var _detail_body: VBoxContainer var _detail_portrait_button: Button @@ -145,7 +166,7 @@ func _build_interface() -> void: var outer := MarginContainer.new() outer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) outer.add_theme_constant_override("margin_left", 52) - outer.add_theme_constant_override("margin_top", 116) + outer.add_theme_constant_override("margin_top", 85) outer.add_theme_constant_override("margin_right", 52) outer.add_theme_constant_override("margin_bottom", 18) add_child(outer) @@ -155,9 +176,8 @@ func _build_interface() -> void: outer.add_child(stack) var tabs := HBoxContainer.new() - # The 26 px page corner plus 8 px clearance keeps the flange behind - # the straight portion of the spread. - tabs.position = Vector2(LOGBOOK_TAB_LEFT_INSET, 20.0) + # Align the first flange with the authored left paper edge. + tabs.position = Vector2(LOGBOOK_TAB_LEFT_INSET, 37.0) tabs.size = Vector2(384.0, 38.0) tabs.z_index = 10 tabs.mouse_filter = Control.MOUSE_FILTER_PASS @@ -179,71 +199,206 @@ func _build_interface() -> void: _category_tabs.append(tab) _configure_category_focus() - var book := HBoxContainer.new() + var book := Control.new() book.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) book.offset_top = 50.0 book.z_index = 20 - book.add_theme_constant_override("separation", 12) + book.mouse_filter = Control.MOUSE_FILTER_IGNORE stack.add_child(book) - var left_page := PanelContainer.new() - left_page.size_flags_horizontal = Control.SIZE_EXPAND_FILL - left_page.size_flags_stretch_ratio = 1.12 - left_page.add_theme_stylebox_override( - "panel", _paper_style(PAPER, true) - ) + var artwork := TextureRect.new() + artwork.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + artwork.texture = LOGBOOK_ARTWORK + artwork.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + artwork.stretch_mode = TextureRect.STRETCH_SCALE + artwork.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + artwork.mouse_filter = Control.MOUSE_FILTER_IGNORE + book.add_child(artwork) + + var left_page := Control.new() + left_page.mouse_filter = Control.MOUSE_FILTER_IGNORE + _apply_artwork_rect(left_page, LEFT_PAGE_CONTENT_RECT) book.add_child(left_page) - var left_margin := MarginContainer.new() - _set_margins(left_margin, 18, 16, 18, 16) - left_page.add_child(left_margin) var left_layout := VBoxContainer.new() + left_layout.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) left_layout.add_theme_constant_override("separation", 8) - left_margin.add_child(left_layout) - var heading := _label("catch catalog", 25) - heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - left_layout.add_child(heading) + left_page.add_child(left_layout) + _apply_page_content_scale(left_layout) + var scroll_indicator_top_lane := Control.new() + scroll_indicator_top_lane.custom_minimum_size.y = 20.0 + scroll_indicator_top_lane.mouse_filter = Control.MOUSE_FILTER_IGNORE + left_layout.add_child(scroll_indicator_top_lane) _empty_state = _label("", 18) _empty_state.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _empty_state.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _empty_state.size_flags_vertical = Control.SIZE_EXPAND_FILL left_layout.add_child(_empty_state) + var catalog_scroll_margin := MarginContainer.new() + catalog_scroll_margin.size_flags_vertical = Control.SIZE_EXPAND_FILL + catalog_scroll_margin.add_theme_constant_override("margin_left", 20) + left_layout.add_child(catalog_scroll_margin) _catalog_scroll = ScrollContainer.new() _catalog_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL _catalog_scroll.horizontal_scroll_mode = ( ScrollContainer.SCROLL_MODE_DISABLED ) - left_layout.add_child(_catalog_scroll) + _catalog_scroll.scroll_vertical_custom_step = CATALOG_ROW_STEP + catalog_scroll_margin.add_child(_catalog_scroll) _catalog_grid = GridContainer.new() - _catalog_grid.columns = 5 + _catalog_grid.columns = 4 _catalog_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL - _catalog_grid.add_theme_constant_override("h_separation", 8) - _catalog_grid.add_theme_constant_override("v_separation", 8) + _catalog_grid.add_theme_constant_override("h_separation", 6) + _catalog_grid.add_theme_constant_override("v_separation", 6) _catalog_scroll.add_child(_catalog_grid) - - var gutter := ColorRect.new() - gutter.custom_minimum_size.x = 10 - gutter.color = Color("795f3f") - gutter.mouse_filter = Control.MOUSE_FILTER_IGNORE - book.add_child(gutter) - - var right_page := PanelContainer.new() - right_page.size_flags_horizontal = Control.SIZE_EXPAND_FILL - right_page.add_theme_stylebox_override( - "panel", _paper_style(PAPER_DARK, false) + var scroll_indicator_bottom_lane := Control.new() + scroll_indicator_bottom_lane.custom_minimum_size.y = 32.0 + scroll_indicator_bottom_lane.mouse_filter = Control.MOUSE_FILTER_IGNORE + left_layout.add_child(scroll_indicator_bottom_lane) + _configure_catalog_scroll_bar() + _catalog_scroll_up_indicator = _make_scroll_indicator( + "CatalogScrollUpIndicator", + SCROLL_UP_TEXTURE, + true, ) + left_page.add_child(_catalog_scroll_up_indicator) + _catalog_scroll_down_indicator = _make_scroll_indicator( + "CatalogScrollDownIndicator", + SCROLL_DOWN_TEXTURE, + false, + ) + left_page.add_child(_catalog_scroll_down_indicator) + + var right_page := Control.new() + right_page.mouse_filter = Control.MOUSE_FILTER_IGNORE + _apply_artwork_rect(right_page, RIGHT_PAGE_CONTENT_RECT) book.add_child(right_page) - var right_margin := MarginContainer.new() - _set_margins(right_margin, 22, 16, 22, 16) - right_page.add_child(right_margin) _detail_body = VBoxContainer.new() + _detail_body.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _detail_body.size_flags_horizontal = Control.SIZE_EXPAND_FILL _detail_body.size_flags_vertical = Control.SIZE_EXPAND_FILL _detail_body.add_theme_constant_override("separation", 16) - right_margin.add_child(_detail_body) + right_page.add_child(_detail_body) + _apply_page_content_scale(_detail_body) _show_no_selection() _build_portrait_overlay() +func _apply_artwork_rect(control: Control, source_rect: Rect2) -> void: + control.anchor_left = source_rect.position.x / LOGBOOK_ARTWORK_SOURCE_SIZE.x + control.anchor_top = source_rect.position.y / LOGBOOK_ARTWORK_SOURCE_SIZE.y + control.anchor_right = source_rect.end.x / LOGBOOK_ARTWORK_SOURCE_SIZE.x + control.anchor_bottom = source_rect.end.y / LOGBOOK_ARTWORK_SOURCE_SIZE.y + control.offset_left = 0.0 + control.offset_top = 0.0 + control.offset_right = 0.0 + control.offset_bottom = 0.0 + + +func _apply_page_content_scale(control: Control) -> void: + control.scale = Vector2.ONE * PAGE_CONTENT_SCALE + control.resized.connect(_center_scaled_content.bind(control)) + _center_scaled_content.call_deferred(control) + + +func _center_scaled_content(control: Control) -> void: + if is_instance_valid(control): + control.pivot_offset = control.size * 0.5 + + +func _configure_catalog_scroll_bar() -> void: + _catalog_scroll_snap_timer = Timer.new() + _catalog_scroll_snap_timer.one_shot = true + _catalog_scroll_snap_timer.wait_time = CATALOG_SNAP_DELAY + _catalog_scroll_snap_timer.timeout.connect(_snap_catalog_scroll_to_row) + _catalog_scroll.add_child(_catalog_scroll_snap_timer) + var scroll_bar: VScrollBar = _catalog_scroll.get_v_scroll_bar() + scroll_bar.mouse_filter = Control.MOUSE_FILTER_IGNORE + scroll_bar.focus_mode = Control.FOCUS_NONE + scroll_bar.self_modulate = Color.TRANSPARENT + scroll_bar.custom_minimum_size.x = 0.0 + scroll_bar.add_theme_constant_override("scroll_size", 0) + var empty_style := StyleBoxEmpty.new() + for style_name: StringName in [ + &"scroll", + &"scroll_focus", + &"grabber", + &"grabber_highlight", + &"grabber_pressed", + ]: + scroll_bar.add_theme_stylebox_override(style_name, empty_style) + scroll_bar.changed.connect(_refresh_catalog_scroll_indicators) + scroll_bar.value_changed.connect(_on_catalog_scroll_value_changed) + + +func _make_scroll_indicator( + indicator_name: String, + indicator_texture: Texture2D, + at_top: bool, +) -> TextureRect: + var indicator := TextureRect.new() + indicator.name = indicator_name + indicator.anchor_left = 0.5 + indicator.anchor_right = 0.5 + indicator.anchor_top = 0.0 if at_top else 1.0 + indicator.anchor_bottom = indicator.anchor_top + indicator.offset_left = -14.0 + indicator.offset_right = 14.0 + indicator.offset_top = -8.0 if at_top else -35.0 + indicator.offset_bottom = 20.0 if at_top else -7.0 + indicator.z_index = 30 + indicator.texture = indicator_texture + indicator.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + indicator.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + indicator.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + indicator.mouse_filter = Control.MOUSE_FILTER_IGNORE + indicator.visible = false + return indicator + + +func _on_catalog_scroll_value_changed(value: float) -> void: + if _snapping_catalog_scroll: + return + _catalog_scroll_snap_timer.start() + _refresh_catalog_scroll_indicators() + + +func _snap_catalog_scroll_to_row() -> void: + var scroll_bar: VScrollBar = _catalog_scroll.get_v_scroll_bar() + var value: float = scroll_bar.value + var maximum_scroll: float = scroll_bar.max_value - scroll_bar.page + var snapped_value: float = clampf( + roundf(value / CATALOG_ROW_STEP) * CATALOG_ROW_STEP, + scroll_bar.min_value, + maximum_scroll, + ) + if not is_equal_approx(value, snapped_value): + _snapping_catalog_scroll = true + _catalog_scroll.scroll_vertical = roundi(snapped_value) + _snapping_catalog_scroll = false + _refresh_catalog_scroll_indicators() + + +func _refresh_catalog_scroll_indicators() -> void: + if ( + _catalog_scroll == null + or _catalog_scroll_up_indicator == null + or _catalog_scroll_down_indicator == null + ): + return + var scroll_bar: VScrollBar = _catalog_scroll.get_v_scroll_bar() + var maximum_scroll: float = scroll_bar.max_value - scroll_bar.page + var has_overflow: bool = ( + _catalog_scroll.visible + and maximum_scroll > scroll_bar.min_value + 0.5 + ) + _catalog_scroll_up_indicator.visible = ( + has_overflow and scroll_bar.value > scroll_bar.min_value + 0.5 + ) + _catalog_scroll_down_indicator.visible = ( + has_overflow and scroll_bar.value < maximum_scroll - 0.5 + ) + + func _refresh_catalog() -> void: if not is_node_ready(): return @@ -293,11 +448,12 @@ func _refresh_catalog() -> void: _refresh_selection_styles() _refresh_details(false) set_interactive(_interactive) + _refresh_catalog_scroll_indicators.call_deferred() func _make_entry(fish: FishDataType, discovered: bool) -> Button: var entry := Button.new() - entry.custom_minimum_size = Vector2(102, 102) + entry.custom_minimum_size = CATALOG_ENTRY_SIZE entry.size_flags_horizontal = Control.SIZE_SHRINK_CENTER entry.size_flags_vertical = Control.SIZE_SHRINK_CENTER entry.toggle_mode = true @@ -344,7 +500,7 @@ func _add_entry_content( var portrait_view := LogbookPortraitType.new() portrait_view.configure( portrait, - LogbookPortraitType.ENTRY_FRAME_SIZE, + CATALOG_PORTRAIT_SIZE, _silhouette_material if unknown else null, ) content.add_child(portrait_view) @@ -495,10 +651,14 @@ func _refresh_details(animate: bool) -> void: func _build_known_details(fish: FishDataType) -> void: _clear_details() var catalog_number: int = LogbookCatalog.catalog_number(fish.id) + var summary_margin := MarginContainer.new() + summary_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _set_margins(summary_margin, 40, 0, 40, 0) + _detail_body.add_child(summary_margin) var summary_columns := HBoxContainer.new() summary_columns.size_flags_horizontal = Control.SIZE_EXPAND_FILL summary_columns.add_theme_constant_override("separation", 24) - _detail_body.add_child(summary_columns) + summary_margin.add_child(summary_columns) var portrait_column := VBoxContainer.new() portrait_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -510,12 +670,11 @@ func _build_known_details(fish: FishDataType) -> void: portrait_column.add_child(title) if fish.display_texture != null: _detail_portrait_button = Button.new() - _detail_portrait_button.custom_minimum_size = ( - LogbookPortraitType.DETAIL_FRAME_SIZE - ) + _detail_portrait_button.custom_minimum_size = DETAIL_PORTRAIT_SIZE _detail_portrait_button.size_flags_horizontal = ( Control.SIZE_SHRINK_CENTER ) + _detail_portrait_button.clip_contents = true _detail_portrait_button.tooltip_text = "View larger artwork" _detail_portrait_button.accessibility_name = ( "View larger artwork for %s" % fish.display_name @@ -545,7 +704,7 @@ func _build_known_details(fish: FishDataType) -> void: var artwork := LogbookPortraitType.new() artwork.configure( fish.display_texture, - LogbookPortraitType.DETAIL_FRAME_SIZE, + DETAIL_PORTRAIT_SIZE, ) artwork_center.add_child(artwork) @@ -562,11 +721,15 @@ func _build_known_details(fish: FishDataType) -> void: facts_column.add_child(facts) _detail_body.add_child(_build_quality_progress(fish.id)) + var stats_anchor := VBoxContainer.new() + stats_anchor.size_flags_horizontal = Control.SIZE_EXPAND_FILL + stats_anchor.size_flags_vertical = Control.SIZE_EXPAND_FILL + stats_anchor.alignment = BoxContainer.ALIGNMENT_END + _detail_body.add_child(stats_anchor) var stats_columns := HBoxContainer.new() stats_columns.size_flags_horizontal = Control.SIZE_EXPAND_FILL - stats_columns.size_flags_vertical = Control.SIZE_EXPAND_FILL stats_columns.add_theme_constant_override("separation", 28) - _detail_body.add_child(stats_columns) + stats_anchor.add_child(stats_columns) var left_stats := _make_stats_column() var right_stats := _make_stats_column() stats_columns.add_child(left_stats) @@ -611,11 +774,16 @@ func _build_known_details(fish: FishDataType) -> void: "number owned", str(_inventory.get_count(fish.id) if _inventory != null else 0), ) + var bottom_inset := Control.new() + bottom_inset.custom_minimum_size.y = DETAIL_BOTTOM_INSET + bottom_inset.mouse_filter = Control.MOUSE_FILTER_IGNORE + stats_anchor.add_child(bottom_inset) func _build_quality_progress(fish_id: StringName) -> VBoxContainer: var quality_section := VBoxContainer.new() - quality_section.add_theme_constant_override("separation", 3) + quality_section.size_flags_horizontal = Control.SIZE_EXPAND_FILL + quality_section.add_theme_constant_override("separation", 6) var discovered_count: int = 0 for quality: int in FishQualityType.TIER_COUNT: if _collection_log.has_discovered_quality(fish_id, quality): @@ -625,29 +793,36 @@ func _build_quality_progress(fish_id: StringName) -> VBoxContainer: discovered_count, FishQualityType.TIER_COUNT, ], - 14, + 17, ) heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER heading.add_theme_color_override("font_color", MUTED_INK) quality_section.add_child(heading) - var tiers := HBoxContainer.new() - tiers.alignment = BoxContainer.ALIGNMENT_CENTER - tiers.add_theme_constant_override("separation", 10) + var tiers := VBoxContainer.new() + tiers.add_theme_constant_override("separation", 6) quality_section.add_child(tiers) for quality: int in FishQualityType.TIER_COUNT: + var row: HBoxContainer + if quality % 2 == 0: + row = HBoxContainer.new() + row.alignment = BoxContainer.ALIGNMENT_CENTER + row.add_theme_constant_override("separation", 28) + tiers.add_child(row) + else: + row = tiers.get_child(tiers.get_child_count() - 1) as HBoxContainer var discovered: bool = _collection_log.has_discovered_quality( fish_id, quality, ) var tier := HBoxContainer.new() tier.alignment = BoxContainer.ALIGNMENT_CENTER - tier.add_theme_constant_override("separation", 3) + tier.add_theme_constant_override("separation", 5) var quality_color: Color = UIPalette.get_quality_color(quality) - var dot := _label("●" if discovered else "○", 13) + var dot := _label("●" if discovered else "○", 17) dot.add_theme_color_override("font_color", quality_color) dot.modulate.a = 1.0 if discovered else 0.48 tier.add_child(dot) - var tier_label := _label(FishQualityType.display_name(quality), 13) + var tier_label := _label(FishQualityType.display_name(quality), 16) tier_label.add_theme_color_override( "font_color", INK if discovered else MUTED_INK, @@ -659,7 +834,7 @@ func _build_quality_progress(fish_id: StringName) -> VBoxContainer: else "%s quality not yet collected" ) % FishQualityType.display_name(quality) tier.add_child(tier_label) - tiers.add_child(tier) + row.add_child(tier) return quality_section @@ -848,18 +1023,6 @@ func _entry_label_text(entry_name: String) -> String: ) -func _paper_style(color: Color, left: bool) -> StyleBoxFlat: - var style := StyleBoxFlat.new() - style.bg_color = color - style.border_color = Color("9b7a4f") - style.set_border_width_all(3) - style.corner_radius_top_left = 26 if left else 6 - style.corner_radius_bottom_left = 32 if left else 6 - style.corner_radius_top_right = 6 if left else 28 - style.corner_radius_bottom_right = 6 if left else 34 - return style - - func _build_portrait_overlay() -> void: _portrait_overlay = Control.new() _portrait_overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) diff --git a/ui/pause_menu.tscn b/ui/pause_menu.tscn index 2477f23..941e26e 100644 --- a/ui/pause_menu.tscn +++ b/ui/pause_menu.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=11 format=3] +[gd_scene load_steps=16 format=3] [ext_resource type="Script" path="res://ui/pause_menu.gd" id="1_script"] [ext_resource type="PackedScene" path="res://ui/settings_panel.tscn" id="2_settings"] @@ -10,6 +10,11 @@ [ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_transition_flurry.gd" id="8_flurry"] [ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_confirmation_page.tscn" id="9_confirmation"] [ext_resource type="PackedScene" path="res://ui/network/join_game_page.tscn" id="10_join_page"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/online_dark.png" id="11_online_dark"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/save_dark.png" id="12_save_dark"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/settings_dark.png" id="13_settings_dark"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/undo_dark.png" id="14_undo_dark"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_dark.png" id="15_x_dark"] [node name="PauseMenu" type="Control"] unique_name_in_owner = true @@ -87,7 +92,9 @@ compact_reference_size = Vector2(608, 448) [node name="ResumeButton" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster" instance=ExtResource("5_bubble")] unique_name_in_owner = true custom_minimum_size = Vector2(0, 0) -text = "return\nto game" +texture_filter = 1 +icon = ExtResource("14_undo_dark") +tooltip_text = "return to game" accessibility_name = "return to game" neutral_size = Vector2(174, 164) desktop_anchor = Vector2(335, 220) @@ -100,7 +107,9 @@ motion_phase = 0.3 [node name="SaveButton" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster" instance=ExtResource("5_bubble")] unique_name_in_owner = true custom_minimum_size = Vector2(0, 0) -text = "save\nnow" +texture_filter = 1 +icon = ExtResource("12_save_dark") +tooltip_text = "save now" accessibility_name = "save now" neutral_size = Vector2(126, 120) desktop_anchor = Vector2(185, 185) @@ -113,7 +122,10 @@ motion_phase = 1.15 [node name="SettingsButton" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster" instance=ExtResource("5_bubble")] unique_name_in_owner = true custom_minimum_size = Vector2(0, 0) -text = "settings" +texture_filter = 1 +icon = ExtResource("13_settings_dark") +tooltip_text = "settings" +accessibility_name = "settings" neutral_size = Vector2(144, 136) desktop_anchor = Vector2(485, 170) compact_anchor = Vector2(515, 175) @@ -125,7 +137,9 @@ motion_phase = 2.05 [node name="JoinGameButton" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster" instance=ExtResource("5_bubble")] unique_name_in_owner = true custom_minimum_size = Vector2(0, 0) -text = "join\ngame" +texture_filter = 1 +icon = ExtResource("11_online_dark") +tooltip_text = "join game" accessibility_name = "join game" neutral_size = Vector2(126, 120) desktop_anchor = Vector2(105, 320) @@ -164,7 +178,10 @@ motion_phase = 3.9 [node name="QuitButton" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster" instance=ExtResource("5_bubble")] unique_name_in_owner = true custom_minimum_size = Vector2(0, 0) -text = "quit" +texture_filter = 1 +icon = ExtResource("15_x_dark") +tooltip_text = "quit" +accessibility_name = "quit" neutral_size = Vector2(102, 98) desktop_anchor = Vector2(390, 395) compact_anchor = Vector2(390, 410) diff --git a/ui/player_menu.tscn b/ui/player_menu.tscn index c2ebff5..4fcf0c0 100644 --- a/ui/player_menu.tscn +++ b/ui/player_menu.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=26 format=3] +[gd_scene load_steps=27 format=3] [ext_resource type="Script" path="res://ui/player_menu.gd" id="1_menu"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] @@ -24,6 +24,7 @@ [ext_resource type="Script" path="res://ui/components/inventory_notepad.gd" id="22_inventory_notepad"] [ext_resource type="Script" path="res://ui/components/organizer_tab.gd" id="23_organizer_tab"] [ext_resource type="PackedScene" path="res://ui/the_net_page.tscn" id="24_the_net_page"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/online_dark.png" id="25_online_dark"] [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_collection"] @@ -700,11 +701,13 @@ layout_mode = 2 text = "Select bait or a lure for details." autowrap_mode = 2 -[node name="TackleEquipButton" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleDetailPanel/Margin/Layout"] +[node name="TackleEquipButton" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleDetailPanel/Margin/Layout" instance=ExtResource("12_ink_action")] unique_name_in_owner = true layout_mode = 2 -custom_minimum_size = Vector2(0, 44) +custom_minimum_size = Vector2(150, 68) +size_flags_horizontal = 4 text = "equip worms" +allow_persistent_mark = true [node name="LogbookPage" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"] unique_name_in_owner = true @@ -1506,7 +1509,9 @@ deformation_period = 6.5 unique_name_in_owner = true layout_mode = 0 toggle_mode = true -text = "online" +texture_filter = 1 +icon = ExtResource("25_online_dark") +tooltip_text = "online" accessibility_name = "online" script = ExtResource("6_bubble") profile = ExtResource("4_profile") diff --git a/ui/profile_page.gd b/ui/profile_page.gd index a5b030c..b895bac 100644 --- a/ui/profile_page.gd +++ b/ui/profile_page.gd @@ -6,6 +6,12 @@ const OPTION_GRID_COLUMNS: int = 6 const ControllerMappingManagerType = preload( "res://settings/controller_mapping_manager.gd" ) +const AnimaleseVoiceType = preload("res://ui/animalese_voice.gd") +const TypewriterRevealType = preload("res://ui/typewriter_reveal.gd") +const VoiceProfilesType = preload( + "res://player/animalese_voice_profiles.gd" +) +const VOICE_CATEGORY_ID: String = "voice" var _service: NetworkProfileService var _experience: PlayerExperience @@ -13,6 +19,10 @@ var _draft_name: String = "" var _draft_appearance: Dictionary = {} var _persisted_name: String = "" var _persisted_appearance: Dictionary = {} +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 _category_id: String = "species" var _dirty: bool = false var _allow_duplicate: bool = false @@ -36,6 +46,8 @@ var _experience_progress: ProgressBar var _experience_value: Label var _feature_preview_cache: Dictionary = {} var _scale_value_label: Label +var _voice_preview: AnimaleseVoiceType +var _voice_preview_tween: Tween func _ready() -> void: @@ -104,6 +116,8 @@ func activate() -> void: func deactivate() -> void: visible = false _debounce.stop() + if _voice_preview_tween != null and _voice_preview_tween.is_valid(): + _voice_preview_tween.kill() _preview.reset_view() @@ -368,15 +382,22 @@ func _build_ui() -> void: UtilityPageStyle.apply_ocean_button(_keep_editing_button) confirm_buttons.add_child(_keep_editing_button) + _voice_preview = AnimaleseVoiceType.new() + _voice_preview.name = "ProfileVoicePreview" + add_child(_voice_preview) _build_categories() func _build_categories() -> void: for child: Node in _category_list.get_children(): child.queue_free() - for category_id: String in CharacterCustomizationCatalog.CATEGORY_IDS: + var category_ids: Array[String] = [] + for appearance_category: String in CharacterCustomizationCatalog.CATEGORY_IDS: + category_ids.append(appearance_category) + category_ids.append(VOICE_CATEGORY_ID) + for category_id: String in category_ids: var button := Button.new() - button.text = CharacterCustomizationCatalog.category_label(category_id) + button.text = _category_label(category_id) button.toggle_mode = true button.custom_minimum_size.x = 120.0 button.button_pressed = category_id == _category_id @@ -392,7 +413,7 @@ func _select_category(category_id: String) -> void: var button := child as Button if button != null: button.button_pressed = button.text == ( - CharacterCustomizationCatalog.category_label(category_id) + _category_label(category_id) ) _refresh_options() @@ -401,12 +422,15 @@ func _refresh_options() -> void: for child: Node in _option_list.get_children(): child.queue_free() var title := Label.new() - title.text = CharacterCustomizationCatalog.category_label(_category_id) + title.text = _category_label(_category_id) title.add_theme_font_size_override("font_size", 18) title.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY ) _option_list.add_child(title) + if _category_id == VOICE_CATEGORY_ID: + _build_voice_options() + return if _category_id == CharacterCustomizationCatalog.SCALE_CATEGORY_ID: _build_scale_option() return @@ -440,6 +464,106 @@ func _refresh_options() -> void: _option_list.add_child(button) +func _category_label(category_id: String) -> String: + return ( + "voice" + if category_id == VOICE_CATEGORY_ID + else CharacterCustomizationCatalog.category_label(category_id) + ) + + +func _build_voice_options() -> void: + var description := Label.new() + description.text = "Choose how your character sounds in chat." + 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) + _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.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 + _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_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.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) + _option_list.add_child(speed_grid) + for option: Dictionary in VoiceProfilesType.SPEED_OPTIONS: + var speed_id := str(option.get("id", "")) + var speed_button := Button.new() + speed_button.text = str(option.get("label", speed_id)) + speed_button.tooltip_text = "%d characters per second" % roundi( + 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.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) + + +func _select_voice_option(voice_id: String) -> void: + if not VoiceProfilesType.is_valid(voice_id): + return + _draft_voice_id = voice_id + _dirty = _draft_differs() + _refresh_options() + _refresh_actions() + _play_voice_preview() + + +func _select_speech_speed_option(speed_id: String) -> void: + if not VoiceProfilesType.is_valid_speed(speed_id): + return + _draft_speech_speed_id = speed_id + _dirty = _draft_differs() + _refresh_options() + _refresh_actions() + _play_voice_preview() + + +func _play_voice_preview() -> void: + if _voice_preview_tween != null and _voice_preview_tween.is_valid(): + _voice_preview_tween.kill() + _voice_preview_tween = _voice_preview.speak_text( + self, + "hello there!", + "profile-preview", + _draft_voice_id, + VoiceProfilesType.speed_for(_draft_speech_speed_id), + ) + + func _build_scale_option() -> void: var description := Label.new() description.text = "Adjust your character's visual size." @@ -741,7 +865,13 @@ func _apply() -> void: if _service == null: return _apply_button.disabled = true - _service.apply_profile(_draft_name, _draft_appearance, _allow_duplicate) + _service.apply_profile( + _draft_name, + _draft_appearance, + _allow_duplicate, + _draft_voice_id, + _draft_speech_speed_id, + ) func _on_apply_finished(accepted: bool, message: String) -> void: @@ -756,8 +886,17 @@ func _load_persisted() -> void: return _persisted_name = _service.get_persisted_name() _persisted_appearance = _service.get_persisted_appearance() + _persisted_voice_id = _service.get_persisted_voice_id() + _persisted_speech_speed_id = ( + _service.get_persisted_speech_speed_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 + TypewriterRevealType.set_characters_per_second( + VoiceProfilesType.speed_for(_persisted_speech_speed_id) + ) _name_edit.text = _draft_name _preview.apply_appearance_profile(_draft_appearance) _dirty = false @@ -793,6 +932,8 @@ func _confirm_pending_action() -> void: _discard_confirmation.visible = false if _confirmation_action == "defaults": _draft_appearance = CharacterCustomizationCatalog.default_snapshot() + _draft_voice_id = VoiceProfilesType.DEFAULT_ID + _draft_speech_speed_id = VoiceProfilesType.DEFAULT_SPEED_ID _preview.apply_appearance_profile(_draft_appearance) _dirty = _draft_differs() _refresh_options() @@ -806,6 +947,8 @@ func _draft_differs() -> bool: return ( _draft_name.strip_edges() != _persisted_name or _draft_appearance != _persisted_appearance + or _draft_voice_id != _persisted_voice_id + or _draft_speech_speed_id != _persisted_speech_speed_id ) diff --git a/ui/settings_panel.tscn b/ui/settings_panel.tscn index e821c82..6d05831 100644 --- a/ui/settings_panel.tscn +++ b/ui/settings_panel.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=7 format=3] +[gd_scene load_steps=9 format=3] [ext_resource type="Script" path="res://ui/settings_panel.gd" id="1_panel"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] @@ -6,6 +6,8 @@ [ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_button.tscn" id="4_bubble"] [ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_cluster.gd" id="5_cluster"] [ext_resource type="Resource" path="res://ui/components/bubble_menu/bubble_menu_profile.tres" id="6_profile"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/undo_dark.png" id="7_undo_dark"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/check_mark_dark.png" id="8_check_mark_dark"] [node name="SettingsPanel" type="Control"] visible = false @@ -109,7 +111,9 @@ offset_left = 474.0 offset_top = 308.0 offset_right = 586.0 offset_bottom = 412.0 -text = "apply" +icon = ExtResource("8_check_mark_dark") +tooltip_text = "apply" +accessibility_name = "apply" neutral_size = Vector2(112, 104) desktop_anchor = Vector2(511, 332) compact_anchor = Vector2(470, 330) @@ -137,7 +141,9 @@ offset_left = 70.0 offset_top = 343.0 offset_right = 170.0 offset_bottom = 437.0 -text = "back" +icon = ExtResource("7_undo_dark") +tooltip_text = "back" +accessibility_name = "back" neutral_size = Vector2(100, 94) desktop_anchor = Vector2(220, 300) compact_anchor = Vector2(155, 275) diff --git a/ui/surface_drawing_toolbar.tscn b/ui/surface_drawing_toolbar.tscn index 61db805..d1d2864 100644 --- a/ui/surface_drawing_toolbar.tscn +++ b/ui/surface_drawing_toolbar.tscn @@ -1,7 +1,10 @@ -[gd_scene load_steps=3 format=3] +[gd_scene load_steps=6 format=3] [ext_resource type="Script" path="res://ui/surface_drawing_toolbar.gd" id="1_script"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/undo_light.png" id="3_undo_light"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/check_mark_light.png" id="4_check_mark_light"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_light.png" id="5_x_light"] [node name="SurfaceDrawingToolbar" type="Control"] visible = false @@ -55,8 +58,11 @@ text = "⌫" [node name="UndoButton" type="Button" parent="TopPanel/Top"] unique_name_in_owner = true layout_mode = 2 +texture_filter = 1 tooltip_text = "undo last stroke" -text = "↶" +accessibility_name = "undo last stroke" +icon = ExtResource("3_undo_light") +expand_icon = true [node name="HideGuideButton" type="Button" parent="TopPanel/Top"] unique_name_in_owner = true @@ -75,15 +81,21 @@ text = "▤" [node name="FinalizeGuideButton" type="Button" parent="TopPanel/Top"] unique_name_in_owner = true layout_mode = 2 +texture_filter = 1 tooltip_text = "finish grid on next click" toggle_mode = true -text = "✓" +accessibility_name = "finish grid on next click" +icon = ExtResource("4_check_mark_light") +expand_icon = true [node name="CloseButton" type="Button" parent="TopPanel/Top"] unique_name_in_owner = true layout_mode = 2 +texture_filter = 1 tooltip_text = "close art tools" -text = "×" +accessibility_name = "close art tools" +icon = ExtResource("5_x_light") +expand_icon = true [node name="ColorPanel" type="PanelContainer" parent="."] unique_name_in_owner = true diff --git a/ui/the_net_page.gd b/ui/the_net_page.gd index 3ee735a..8adc772 100644 --- a/ui/the_net_page.gd +++ b/ui/the_net_page.gd @@ -407,9 +407,9 @@ func _refresh_forecast(force: bool) -> void: slot.mouse_filter = Control.MOUSE_FILTER_IGNORE _forecast_list.add_child(slot) var icon := WeatherIcon.new() - icon.position = Vector2(1.0, 7.0) - icon.size = Vector2(56.0, 56.0) - icon.custom_minimum_size = Vector2(56.0, 56.0) + icon.position = Vector2(10.5, 17.0) + icon.size = Vector2(42.0, 42.0) + icon.custom_minimum_size = Vector2(42.0, 42.0) icon.set_weather(weather) icon.set_nighttime( WorldTimeService.phase_for_hour(start_hour) @@ -417,13 +417,13 @@ func _refresh_forecast(force: bool) -> void: ) slot.add_child(icon) var time_label := Label.new() - time_label.position = Vector2(34.0, 0.0) - time_label.size = Vector2(29.0, 18.0) + time_label.position = Vector2(27.0, 0.0) + time_label.size = Vector2(36.0, 22.0) time_label.mouse_filter = Control.MOUSE_FILTER_IGNORE time_label.text = _forecast_exponent_time(start_hour) time_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER time_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - time_label.add_theme_font_size_override("font_size", 10) + time_label.add_theme_font_size_override("font_size", 13) time_label.add_theme_constant_override("outline_size", 3) time_label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY diff --git a/ui/title_screen.tscn b/ui/title_screen.tscn index 44da14a..d9234c1 100644 --- a/ui/title_screen.tscn +++ b/ui/title_screen.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=14 format=3] +[gd_scene load_steps=17 format=3] [ext_resource type="Script" path="res://ui/title_screen.gd" id="1_script"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] @@ -11,6 +11,9 @@ [ext_resource type="Resource" path="res://ui/components/bubble_menu/bubble_menu_profile.tres" id="9_bubble_profile"] [ext_resource type="PackedScene" path="res://ui/title_confirmation_bubble_page.tscn" id="10_confirmation_page"] [ext_resource type="PackedScene" path="res://ui/network/join_game_page.tscn" id="11_join_page"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/online_dark.png" id="12_online_dark"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/settings_dark.png" id="13_settings_dark"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_dark.png" id="14_x_dark"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water"] shader = ExtResource("4_water_shader") @@ -303,7 +306,10 @@ offset_left = 264.0 offset_top = 17.0 offset_right = 384.0 offset_bottom = 133.0 -text = "settings" +texture_filter = 1 +icon = ExtResource("13_settings_dark") +tooltip_text = "settings" +accessibility_name = "settings" script = ExtResource("7_bubble_button") profile = ExtResource("9_bubble_profile") desktop_anchor = Vector2(324, 75) @@ -322,7 +328,10 @@ offset_left = 126.0 offset_top = 224.0 offset_right = 248.0 offset_bottom = 342.0 -text = "join\ngame" +texture_filter = 1 +icon = ExtResource("12_online_dark") +tooltip_text = "join game" +accessibility_name = "join game" script = ExtResource("7_bubble_button") profile = ExtResource("9_bubble_profile") neutral_size = Vector2(122, 118) @@ -380,7 +389,10 @@ offset_left = 275.0 offset_top = 217.0 offset_right = 369.0 offset_bottom = 309.0 -text = "quit" +texture_filter = 1 +icon = ExtResource("14_x_dark") +tooltip_text = "quit" +accessibility_name = "quit" script = ExtResource("7_bubble_button") profile = ExtResource("9_bubble_profile") neutral_size = Vector2(94, 92) diff --git a/ui/typewriter_reveal.gd b/ui/typewriter_reveal.gd new file mode 100644 index 0000000..aa4f3d6 --- /dev/null +++ b/ui/typewriter_reveal.gd @@ -0,0 +1,47 @@ +class_name TypewriterReveal +extends RefCounted + +const DEFAULT_CHARACTERS_PER_SECOND: float = 28.0 +const MIN_CHARACTERS_PER_SECOND: float = 12.0 +const MAX_CHARACTERS_PER_SECOND: float = 60.0 + +static var _characters_per_second: float = DEFAULT_CHARACTERS_PER_SECOND + + +static func set_characters_per_second(value: float) -> void: + _characters_per_second = clampf( + value, + MIN_CHARACTERS_PER_SECOND, + MAX_CHARACTERS_PER_SECOND, + ) + + +static func get_characters_per_second() -> float: + return _characters_per_second + + +static func start( + label: Label, + characters_per_second: float = -1.0, +) -> float: + if label == null: + return 0.0 + var character_count: int = label.get_total_character_count() + if character_count <= 0: + label.visible_characters = -1 + return 0.0 + label.visible_characters = 0 + var resolved_characters_per_second := ( + characters_per_second + if characters_per_second > 0.0 + else _characters_per_second + ) + var duration := float(character_count) / resolved_characters_per_second + var reveal_tween := label.create_tween() + reveal_tween.tween_property( + label, + "visible_characters", + character_count, + duration, + ).from(0).set_trans(Tween.TRANS_LINEAR) + return duration diff --git a/ui/typewriter_reveal.gd.uid b/ui/typewriter_reveal.gd.uid new file mode 100644 index 0000000..2d96fd3 --- /dev/null +++ b/ui/typewriter_reveal.gd.uid @@ -0,0 +1 @@ +uid://c1as5qlog6ijy diff --git a/ui/weather_icon.gd b/ui/weather_icon.gd index 894e140..e14f3dc 100644 --- a/ui/weather_icon.gd +++ b/ui/weather_icon.gd @@ -1,11 +1,21 @@ class_name WeatherIcon extends Control -const BUBBLE_COLOR := Color(0.025, 0.13, 0.19, 0.94) -const ICON_COLOR := Color(0.78, 0.91, 0.95) -const SUN_COLOR := Color(0.98, 0.82, 0.34) -const MOON_COLOR := Color(0.78, 0.88, 1.0) -const RAIN_COLOR := Color(0.28, 0.73, 0.82) +const CLEAR_DAY_TEXTURE: Texture2D = preload( + "res://ui/icons/weather/weather_clear_day.png" +) +const CLEAR_NIGHT_TEXTURE: Texture2D = preload( + "res://ui/icons/weather/weather_clear_night_full.png" +) +const CLOUDY_TEXTURE: Texture2D = preload( + "res://ui/icons/weather/weather_cloudy.png" +) +const FOG_TEXTURE: Texture2D = preload( + "res://ui/icons/weather/weather_fog.png" +) +const RAIN_TEXTURE: Texture2D = preload( + "res://ui/icons/weather/weather_rain.png" +) var _weather: WorldWeatherService.Weather = WorldWeatherService.Weather.SUNNY var _is_nighttime: bool = false @@ -14,6 +24,7 @@ var _is_nighttime: bool = false func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_PASS focus_mode = Control.FOCUS_NONE + texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST custom_minimum_size = Vector2(34.0, 34.0) _update_tooltip() queue_redraw() @@ -44,55 +55,22 @@ func get_weather() -> WorldWeatherService.Weather: func _draw() -> void: - var center: Vector2 = size * 0.5 - var radius: float = minf(size.x, size.y) * 0.5 - draw_circle(center, radius, BUBBLE_COLOR) + var icon_size := Vector2.ONE * minf(size.x, size.y) + var icon_rect := Rect2((size - icon_size) * 0.5, icon_size) + draw_texture_rect(_get_weather_texture(), icon_rect, false) + + +func _get_weather_texture() -> Texture2D: match _weather: WorldWeatherService.Weather.SUNNY: - if _is_nighttime: - _draw_moon(center) - else: - _draw_sun(center) + return CLEAR_NIGHT_TEXTURE if _is_nighttime else CLEAR_DAY_TEXTURE WorldWeatherService.Weather.CLOUDY: - _draw_cloud(center + Vector2(0.0, 1.0)) + return CLOUDY_TEXTURE WorldWeatherService.Weather.RAINY: - _draw_cloud(center + Vector2(0.0, -2.0)) - for x_offset: float in PackedFloat32Array([-6.0, 0.0, 6.0]): - draw_line( - center + Vector2(x_offset + 1.0, 5.0), - center + Vector2(x_offset - 1.0, 9.0), - RAIN_COLOR, - 2.0, - true, - ) + return RAIN_TEXTURE WorldWeatherService.Weather.FOGGY: - for y_offset: float in PackedFloat32Array([-6.0, 0.0, 6.0]): - draw_line( - center + Vector2(-9.0, y_offset), - center + Vector2(9.0, y_offset), - ICON_COLOR, - 2.0, - true, - ) - - -func _draw_sun(center: Vector2) -> void: - draw_circle(center, 5.0, SUN_COLOR) - for index: int in 8: - var angle: float = TAU * float(index) / 8.0 - var direction := Vector2(cos(angle), sin(angle)) - draw_line( - center + direction * 8.0, - center + direction * 11.0, - SUN_COLOR, - 2.0, - true, - ) - - -func _draw_moon(center: Vector2) -> void: - draw_circle(center, 8.0, MOON_COLOR) - draw_circle(center + Vector2(4.0, -2.0), 7.0, BUBBLE_COLOR) + return FOG_TEXTURE + return CLEAR_DAY_TEXTURE func _update_tooltip() -> void: @@ -101,10 +79,3 @@ func _update_tooltip() -> void: if _weather == WorldWeatherService.Weather.SUNNY else WorldWeatherService.weather_name(_weather) ) - - -func _draw_cloud(center: Vector2) -> void: - draw_circle(center + Vector2(-5.0, 0.0), 5.0, ICON_COLOR) - draw_circle(center + Vector2(0.0, -3.0), 6.0, ICON_COLOR) - draw_circle(center + Vector2(6.0, 0.0), 5.0, ICON_COLOR) - draw_rect(Rect2(center + Vector2(-9.0, 0.0), Vector2(18.0, 5.0)), ICON_COLOR) diff --git a/world/world_time_service.gd b/world/world_time_service.gd index 1285f3e..c3186b4 100644 --- a/world/world_time_service.gd +++ b/world/world_time_service.gd @@ -3,6 +3,8 @@ extends Node signal time_changed(time_hours: float, phase: Phase) signal phase_changed(phase: Phase) +signal authoritative_time_set(time_hours: float) +signal natural_time_advanced(hours: float) enum Phase { DAWN, @@ -54,10 +56,12 @@ func end_session() -> void: func advance_time(real_seconds: float) -> void: if not _running or real_seconds <= 0.0: return + var advanced_hours := real_seconds * HOURS_PER_REAL_SECOND _set_time_hours( - _time_hours + real_seconds * HOURS_PER_REAL_SECOND, + _time_hours + advanced_hours, false, ) + natural_time_advanced.emit(advanced_hours) func synchronize_time(authoritative_time_hours: float) -> void: @@ -66,6 +70,18 @@ func synchronize_time(authoritative_time_hours: float) -> void: _set_time_hours(authoritative_time_hours, true) +func set_authoritative_time(time_hours: float) -> bool: + if ( + not is_finite(time_hours) + or time_hours < 0.0 + or time_hours >= HOURS_PER_DAY + ): + return false + _set_time_hours(time_hours, true) + authoritative_time_set.emit(_time_hours) + return true + + func set_persistence_tracking_enabled(enabled: bool) -> void: if _persistence_tracking_enabled == enabled: return