class_name WaterRecoveryController extends Node signal recovery_starting enum RecoveryState { IDLE, BOBBING, FADING_OUT, FADING_IN, } @export_range(-1.0, 1.0, 0.01) var water_center_offset: float = 0.0 @export_range(0.0, 0.5, 0.005) var bob_amplitude: float = 0.08 @export_range(0.1, 10.0, 0.1) var bob_speed: float = 2.4 @export_range(0.0, 5.0, 0.05) var pre_fade_bob_duration: float = 1.0 @export_range(0.0, 1.0, 0.01) var respawn_height_offset: float = 0.08 var state: RecoveryState = RecoveryState.IDLE var _player: Player var _fishing_spot: FishingSpot var _game_ui: GameUI var _screen_fade: ScreenFade var _water_triggers: Array[PlayerWaterTrigger] = [] var _safe_points: Array[SafeRespawnPoint] = [] var _initial_spawn_transform: Transform3D var _entry_position: Vector3 var _bob_base_position: Vector3 var _recovery_root_basis: Basis var _bob_elapsed: float = 0.0 var _prior_movement_enabled: bool = true var _prior_camera_input_enabled: bool = true var _generation: int = 0 var _recovery_enabled: bool = false func setup( player: Player, fishing_spot: FishingSpot, game_ui: GameUI, screen_fade: ScreenFade, water_triggers: Array[PlayerWaterTrigger], safe_points: Array[SafeRespawnPoint], ) -> void: _player = player _fishing_spot = fishing_spot _game_ui = game_ui _screen_fade = screen_fade _water_triggers = water_triggers _safe_points = safe_points _initial_spawn_transform = player.global_transform for water_trigger: PlayerWaterTrigger in _water_triggers: if ( water_trigger != null and not water_trigger.recovery_requested.is_connected( _on_recovery_requested ) ): water_trigger.recovery_requested.connect( _on_recovery_requested ) if not _screen_fade.transition_completed.is_connected( _on_fade_transition_completed ): _screen_fade.transition_completed.connect( _on_fade_transition_completed ) func _process(delta: float) -> void: if state != RecoveryState.BOBBING or _player == null: return _bob_elapsed += delta var bob_offset: float = sin(_bob_elapsed * bob_speed * TAU) * bob_amplitude _player.global_position = _bob_base_position + Vector3.UP * bob_offset if _bob_elapsed >= pre_fade_bob_duration: state = RecoveryState.FADING_OUT _screen_fade.fade_to_black(_generation) func set_recovery_enabled(enabled: bool) -> void: _recovery_enabled = enabled func is_recovery_active() -> bool: return state != RecoveryState.IDLE func _exit_tree() -> void: _generation += 1 if _screen_fade != null and is_instance_valid(_screen_fade): _screen_fade.reset_immediately() if _player != null and is_instance_valid(_player): _player.restore_gameplay_orientation_after_recovery() _player.set_water_recovery_active(false) _player.set_movement_enabled(_prior_movement_enabled) _player.set_camera_input_enabled(_prior_camera_input_enabled) state = RecoveryState.IDLE func _on_recovery_requested( triggered_player: Player, surface_height: float, ) -> void: if ( not _recovery_enabled or state != RecoveryState.IDLE or triggered_player != _player or not is_instance_valid(triggered_player) ): return recovery_starting.emit() _generation += 1 _entry_position = _player.global_position _recovery_root_basis = _player.global_basis _fishing_spot.begin_water_recovery() _player.prepare_for_water_recovery() _prior_movement_enabled = _player.is_movement_enabled() _prior_camera_input_enabled = _player.is_camera_input_enabled() _player.set_movement_enabled(false) _player.set_camera_input_enabled(false) _player.set_water_recovery_active(true) _bob_base_position = _player.global_position _bob_base_position.y = ( surface_height + water_center_offset - _player.get_body_center_height() ) _player.global_position = _bob_base_position _bob_elapsed = 0.0 state = RecoveryState.BOBBING func _on_fade_transition_completed( generation: int, faded_to_black: bool, ) -> void: if generation != _generation: return if faded_to_black and state == RecoveryState.FADING_OUT: _respawn_player() state = RecoveryState.FADING_IN _screen_fade.fade_from_black(_generation) elif not faded_to_black and state == RecoveryState.FADING_IN: _finish_recovery() func _respawn_player() -> void: var target_position: Vector3 = _initial_spawn_transform.origin var nearest_distance: float = INF for point: SafeRespawnPoint in _safe_points: if point == null or not is_instance_valid(point) or not point.enabled: continue var distance: float = point.get_horizontal_distance_squared( _entry_position ) if distance < nearest_distance: nearest_distance = distance target_position = point.global_position target_position.y += respawn_height_offset var respawn_transform: Transform3D = _player.global_transform respawn_transform.basis = _recovery_root_basis respawn_transform.origin = target_position _player.global_transform = respawn_transform _player.restore_gameplay_orientation_after_recovery() _player.velocity = Vector3.ZERO func _finish_recovery() -> void: _player.restore_gameplay_orientation_after_recovery() _player.set_water_recovery_active(false) _player.set_movement_enabled(_prior_movement_enabled) _player.set_camera_input_enabled(_prior_camera_input_enabled) _fishing_spot.end_water_recovery() state = RecoveryState.IDLE _bob_elapsed = 0.0