extends RigidBody3D @export var drive_speed := 18.0 @export var drive_accel := 20.0 @export var brake_strength := 28.0 @export var turn_speed := 4.0 @export var turn_accel := 8.0 @export var lateral_damp := 10.0 @export var launch_impulse := 28.0 @export var launch_up_impulse := 6.0 @export var seat_path: NodePath @export var exit_path: NodePath @export var camera_path: NodePath @export var interact_area_path: NodePath @export var engine_audio_fade_speed: float = 8.0 @export var hit_sound_cooldown: float = 0.25 @onready var seat: Node3D = get_node(seat_path) if seat_path != NodePath("") else null @onready var exit_point: Node3D = get_node(exit_path) if exit_path != NodePath("") else null @onready var car_camera: Camera3D = get_node(camera_path) if camera_path != NodePath("") else null @onready var interact_area: Area3D = get_node(interact_area_path) if interact_area_path != NodePath("") else null @onready var engine_idle_audio: AudioStreamPlayer3D = get_node_or_null("Audio/EngineIdle") @onready var engine_drive_audio: AudioStreamPlayer3D = get_node_or_null("Audio/EngineDrive") @onready var engine_accel_audio: AudioStreamPlayer3D = get_node_or_null("Audio/EngineAccel") @onready var enter_audio: AudioStreamPlayer3D = get_node_or_null("Audio/Enter") @onready var exit_audio: AudioStreamPlayer3D = get_node_or_null("Audio/Exit") @onready var brake_audio: AudioStreamPlayer3D = get_node_or_null("Audio/Brake") @onready var hit_audio: AudioStreamPlayer3D = get_node_or_null("Audio/Hit") var _nearby_driver: Node = null var _driver: Node = null var _speed_factor: float = 0.0 var _throttle_input: float = 0.0 var _braking: bool = false var _was_braking: bool = false var _hit_sound_time_left: float = 0.0 func _ready() -> void: add_to_group("vehicle") if interact_area: interact_area.collision_layer = 2 interact_area.collision_mask = 1 interact_area.body_entered.connect(_on_interact_body_entered) interact_area.body_exited.connect(_on_interact_body_exited) if car_camera: car_camera.current = false contact_monitor = true max_contacts_reported = 8 func _process(delta: float) -> void: if _driver == null and _nearby_driver != null and Input.is_action_just_pressed("interact"): _enter_vehicle(_nearby_driver) elif _driver != null and Input.is_action_just_pressed("interact"): _exit_vehicle() _update_audio(delta) _hit_sound_time_left = maxf(0.0, _hit_sound_time_left - delta) func _integrate_forces(state: PhysicsDirectBodyState3D) -> void: if _driver == null: _speed_factor = 0.0 _throttle_input = 0.0 _braking = false return var input2v := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") _throttle_input = input2v.y var forward := global_transform.basis.z forward.y = 0.0 forward = forward.normalized() var current_speed := linear_velocity.dot(forward) var target_speed := input2v.y * drive_speed var accel := drive_accel if abs(input2v.y) > 0.01 else brake_strength var is_coasting_brake: bool = abs(input2v.y) <= 0.01 and abs(current_speed) > drive_speed * 0.35 var is_reverse_brake: bool = abs(input2v.y) > 0.01 and signf(input2v.y) != signf(current_speed) and abs(current_speed) > drive_speed * 0.25 _braking = is_coasting_brake or is_reverse_brake var new_forward_speed := move_toward(current_speed, target_speed, accel * state.step) var forward_vel := forward * new_forward_speed var lateral_vel := linear_velocity - (forward * current_speed) lateral_vel = lateral_vel.move_toward(Vector3.ZERO, lateral_damp * state.step) linear_velocity = forward_vel + lateral_vel var speed_factor: float = clamp(abs(new_forward_speed) / drive_speed, 0.0, 1.0) _speed_factor = speed_factor var turn_input := input2v.x if new_forward_speed < -0.1: turn_input = -turn_input var target_turn: float = turn_input * turn_speed * speed_factor angular_velocity.y = move_toward(angular_velocity.y, target_turn, turn_accel * state.step) if speed_factor > 0.2: var hit_ids := {} for i in state.get_contact_count(): var collider := state.get_contact_collider_object(i) if collider is RigidBody3D and collider != self: var collider_id := (collider as Node).get_instance_id() if hit_ids.has(collider_id): continue hit_ids[collider_id] = true var contact_pos := state.get_contact_collider_position(i) var launch_dir := (contact_pos - global_position).normalized() if launch_dir.length() <= 0.001: var normal_world := (global_transform.basis * state.get_contact_local_normal(i)).normalized() launch_dir = normal_world if normal_world.length() > 0.001 else forward.normalized() var impulse := launch_dir * (launch_impulse * speed_factor) + Vector3.UP * launch_up_impulse (collider as RigidBody3D).apply_central_impulse(impulse) _play_hit_audio(speed_factor) func _enter_vehicle(player: Node) -> void: if seat == null: return _driver = player player.call("enter_vehicle", self, seat, car_camera) _play_one_shot(enter_audio) _start_engine_audio() if car_camera: car_camera.current = true func _exit_vehicle() -> void: if _driver == null: return _driver.call("exit_vehicle", exit_point, car_camera) _driver = null _speed_factor = 0.0 _throttle_input = 0.0 _braking = false _play_one_shot(exit_audio) if car_camera: car_camera.current = false func _on_interact_body_entered(body: Node) -> void: if body.has_method("enter_vehicle"): _nearby_driver = body func _on_interact_body_exited(body: Node) -> void: if body == _nearby_driver: _nearby_driver = null func _start_engine_audio() -> void: _start_engine_player(engine_idle_audio) _start_engine_player(engine_drive_audio) _start_engine_player(engine_accel_audio) func _update_audio(delta: float) -> void: if _driver != null: _start_engine_audio() var active: bool = _driver != null var throttle_weight: float = clamp(abs(_throttle_input), 0.0, 1.0) var accel_weight: float = throttle_weight * clamp(_speed_factor * 1.25, 0.0, 1.0) var drive_weight: float = clamp(_speed_factor * 1.25, 0.0, 1.0) * (1.0 - accel_weight * 0.45) var idle_weight: float = 0.9 - clamp(_speed_factor * 1.35, 0.0, 0.72) _set_engine_player(engine_idle_audio, idle_weight if active else 0.0, 0.86 + _speed_factor * 0.18, delta) _set_engine_player(engine_drive_audio, drive_weight if active else 0.0, 0.92 + _speed_factor * 0.35, delta) _set_engine_player(engine_accel_audio, accel_weight if active else 0.0, 0.96 + _speed_factor * 0.5, delta) if _braking and not _was_braking: _play_one_shot(brake_audio) _was_braking = _braking func _set_engine_player(player: AudioStreamPlayer3D, weight: float, pitch: float, delta: float) -> void: if player == null: return var target_volume: float = linear_to_db(maxf(weight, 0.0001)) - 2.0 if weight <= 0.001: target_volume = -80.0 player.volume_db = move_toward(player.volume_db, target_volume, engine_audio_fade_speed * 10.0 * delta) player.pitch_scale = move_toward(player.pitch_scale, pitch, engine_audio_fade_speed * delta) func _start_engine_player(player: AudioStreamPlayer3D) -> void: if player == null or player.playing: return player.play() func _play_one_shot(player: AudioStreamPlayer3D) -> void: if player == null: return player.stop() player.play() func _play_hit_audio(speed_factor: float) -> void: if hit_audio == null or _hit_sound_time_left > 0.0: return hit_audio.pitch_scale = randf_range(0.92, 1.08) var hit_volume: float = clamp(speed_factor, 0.25, 1.0) hit_audio.volume_db = linear_to_db(hit_volume) - 3.0 hit_audio.play() _hit_sound_time_left = hit_sound_cooldown