Skip to main content

gizmo_physics_dynamics/
vehicle.rs

1use gizmo_physics_core::{Collider, Transform};
2use gizmo_physics_rigid::components::{RigidBody, Velocity};
3use gizmo_physics_core::raycast::{Ray, Raycast, RaycastHit};
4use gizmo_core::entity::Entity;
5use gizmo_math::{Quat, Vec3};
6
7// ============================================================
8// PACEJKA MF — Kombine Slip Modeli (MF 5.2 benzeri)
9// F_x = Fx_pure(σx) * Gx(σy)   — uzunlamasına weighting
10// F_y = Fy_pure(σy) * Gy(σx)   — yanal weighting
11// ============================================================
12
13#[derive(Clone, Debug)]
14pub struct PacejkaParams {
15    pub b: f32, // Stiffness factor
16    pub c: f32, // Shape factor
17    pub d: f32, // Peak factor (normal load ile ölçeklenir)
18    pub e: f32, // Curvature factor
19}
20
21impl Default for PacejkaParams {
22    fn default() -> Self {
23        Self {
24            b: 10.0,
25            c: 1.9,
26            d: 1.0,
27            e: 0.97,
28        }
29    }
30}
31
32impl PacejkaParams {
33    /// Tek eksen için saf Pacejka değeri ([-∞,+∞] slip → kuvvet)
34    pub fn calculate_force(&self, slip: f32, normal_load: f32) -> f32 {
35        let bx = self.b * slip;
36        let d = self.d * normal_load;
37        let inner = self.c * (bx - self.e * (bx - bx.atan())).atan();
38        d * inner.sin()
39    }
40
41    /// Kombine slip weighting fonksiyonu (Lorentzian falloff)
42    /// σ_other = dikey yöndeki normalize kayma miktarı
43    fn weighting_lorentzian(&self, sigma_other: f32) -> f32 {
44        let k = self.b * sigma_other;
45        1.0 / (1.0 + k * k).sqrt() // Lorentzian scaling
46    }
47}
48
49/// Geriye uyum için PacejkaLat alias
50pub type PacejkaLat = PacejkaParams;
51
52/// Kombine Pacejka: uzunlamasına ve yanal kuvvetleri birlikte hesapla
53/// Sürtünme çemberi dahilinde tutulur.
54pub fn pacejka_combined(
55    long: &PacejkaParams,
56    lat: &PacejkaParams,
57    slip_ratio: f32, // longitudinal (σx)
58    slip_angle: f32, // lateral (radyan, σy)
59    normal_load: f32,
60) -> (f32, f32) {
61    let fx_pure = long.calculate_force(slip_ratio, normal_load);
62    let fy_pure = lat.calculate_force(slip_angle, normal_load);
63
64    // Kombine weighting: her eksen diğerini kısmen bastırır
65    let gx = long.weighting_lorentzian(slip_angle);
66    let gy = lat.weighting_lorentzian(slip_ratio);
67
68    let fx = fx_pure * gx;
69    let fy = fy_pure * gy;
70
71    // Sürtünme çemberi: μ * Fz sınırı
72    let mu_peak = long.d.max(lat.d) * 1.2;
73    let limit = normal_load * mu_peak;
74    let mag = (fx * fx + fy * fy).sqrt();
75    if mag > limit && mag > 0.0 {
76        let scale = limit / mag;
77        (fx * scale, fy * scale)
78    } else {
79        (fx, fy)
80    }
81}
82
83// ============================================================
84// WHEEL & VEHICLE STRUCTS
85// ============================================================
86
87#[derive(Clone, Debug, PartialEq)]
88pub enum Axle {
89    Front,
90    Rear,
91}
92
93#[derive(Clone, Debug)]
94pub struct Wheel {
95    pub attachment_local_pos: Vec3,
96    pub direction_local: Vec3,
97    pub axle_type: Axle,
98    pub is_left: bool,
99
100    pub radius: f32,
101    pub suspension_rest_length: f32,
102    pub suspension_max_travel: f32,
103    pub suspension_stiffness: f32,
104    pub suspension_damping: f32,
105
106    pub pacejka_long: PacejkaParams,
107    pub pacejka_lat: PacejkaLat,
108    pub wheel_mass: f32,
109
110    // Inputs (set by VehicleController each frame)
111    pub steering_angle: f32,
112    pub drive_torque: f32,
113    pub brake_torque: f32,
114
115    // State
116    pub is_grounded: bool,
117    pub ground_hit: Option<RaycastHit>,
118    pub suspension_length: f32,
119    pub rotation_angle: f32,
120    pub angular_velocity: f32,
121    pub suspension_force: f32,
122}
123
124impl Default for Wheel {
125    fn default() -> Self {
126        Self {
127            attachment_local_pos: Vec3::ZERO,
128            direction_local: Vec3::new(0.0, -1.0, 0.0),
129            axle_type: Axle::Front,
130            is_left: true,
131            radius: 0.35,
132            suspension_rest_length: 0.5,
133            suspension_max_travel: 0.2,
134            suspension_stiffness: 25000.0,
135            suspension_damping: 3000.0,
136            pacejka_long: PacejkaParams::default(),
137            pacejka_lat: PacejkaLat::default(),
138            wheel_mass: 20.0,
139            steering_angle: 0.0,
140            drive_torque: 0.0,
141            brake_torque: 0.0,
142            is_grounded: false,
143            ground_hit: None,
144            suspension_length: 0.5,
145            rotation_angle: 0.0,
146            angular_velocity: 0.0,
147            suspension_force: 0.0,
148        }
149    }
150}
151
152/// Eski uyum için `pacejka` getter
153impl Wheel {
154    #[inline]
155    pub fn pacejka(&self) -> &PacejkaParams {
156        &self.pacejka_long
157    }
158}
159
160/// Aerodinamik paket
161#[derive(Clone, Debug)]
162pub struct AeroPackage {
163    pub drag_coefficient: f32,     // Cd
164    pub lift_coefficient: f32,     // Cl (negatif = downforce)
165    pub frontal_area: f32,         // m²
166    pub center_of_pressure: Vec3,  // CoM'dan offset (yerel)
167    pub ground_effect_height: f32, // Bu yüksekliğin altında zemin etkisi devreye girer
168    pub ground_effect_multiplier: f32,
169}
170
171impl Default for AeroPackage {
172    fn default() -> Self {
173        Self {
174            drag_coefficient: 0.32,
175            lift_coefficient: -0.8, // downforce
176            frontal_area: 2.2,      // m²
177            center_of_pressure: Vec3::new(0.0, 0.3, 0.2),
178            ground_effect_height: 0.15,
179            ground_effect_multiplier: 1.8,
180        }
181    }
182}
183
184#[derive(Clone, Debug)]
185pub struct VehicleTuning {
186    pub idle_rpm: f32,
187    pub max_rpm: f32,
188    /// [0]=Geri, [1]=Nötr, [2..]=İleri vitesler
189    pub gear_ratios: Vec<f32>,
190    pub final_drive_ratio: f32,
191    /// Otomatik vites: upshift RPM eşiği
192    pub upshift_rpm: f32,
193    /// Otomatik vites: downshift RPM eşiği
194    pub downshift_rpm: f32,
195    pub wheelbase: f32,
196    pub track_width: f32,
197    pub anti_roll_stiffness: f32,
198    pub max_engine_torque: f32,
199    pub max_brake_torque: f32,
200    pub aero: AeroPackage,
201}
202
203impl Default for VehicleTuning {
204    fn default() -> Self {
205        Self {
206            idle_rpm: 800.0,
207            max_rpm: 7000.0,
208            gear_ratios: vec![-2.5, 0.0, 3.0, 2.0, 1.4, 1.0, 0.75],
209            final_drive_ratio: 3.73,
210            upshift_rpm: 6200.0,
211            downshift_rpm: 2200.0,
212            wheelbase: 2.8,
213            track_width: 1.6,
214            anti_roll_stiffness: 3000.0,
215            max_engine_torque: 350.0,
216            max_brake_torque: 1500.0,
217            aero: AeroPackage::default(),
218        }
219    }
220}
221
222#[derive(Clone)]
223pub struct VehicleController {
224    pub wheels: Vec<Wheel>,
225    pub tuning: VehicleTuning,
226
227    pub throttle_input: f32, // 0..1
228    pub brake_input: f32,    // 0..1
229    pub steering_input: f32, // -1..1
230    pub reverse_input: bool,
231    pub auto_shift: bool, // Otomatik vites etkin mi?
232
233    pub current_gear: usize, // gear_ratios index
234    pub max_steering_angle: f32,
235    pub shift_cooldown: f32, // Vites değişimi sonrası bekleme süresi (s)
236
237    pub engine_rpm: f32,
238    pub current_speed_kmh: f32,
239    pub engine_angular_vel: f32, // rad/s — şanzıman simülasyonu için
240    pub flywheel_inertia: f32,   // kg·m²
241}
242
243impl gizmo_core::component::Component for VehicleController {}
244
245impl Default for VehicleController {
246    fn default() -> Self {
247        Self {
248            wheels: Vec::new(),
249            tuning: VehicleTuning::default(),
250            throttle_input: 0.0,
251            brake_input: 0.0,
252            steering_input: 0.0,
253            reverse_input: false,
254            auto_shift: true,
255            current_gear: 2,
256            max_steering_angle: 0.52,
257            shift_cooldown: 0.0,
258            engine_rpm: 800.0,
259            current_speed_kmh: 0.0,
260            engine_angular_vel: 800.0 / 9.549,
261            flywheel_inertia: 0.25, // kg·m²
262        }
263    }
264}
265
266impl VehicleController {
267    pub fn new() -> Self {
268        Self::default()
269    }
270    pub fn add_wheel(&mut self, wheel: Wheel) {
271        self.wheels.push(wheel);
272    }
273
274    /// Motor tork eğrisi — parametrik çan eğrisi
275    pub fn get_engine_torque(&self) -> f32 {
276        let t = &self.tuning;
277        let ratio = (self.engine_rpm - t.idle_rpm).max(0.0) / (t.max_rpm - t.idle_rpm).max(1.0);
278        let curve = (1.0 - (ratio - 0.4).powi(2) * 2.5).clamp(0.05, 1.0);
279        t.max_engine_torque * curve * self.throttle_input.abs()
280    }
281
282    pub fn set_reverse(&mut self, on: bool) {
283        self.current_gear = if on {
284            0
285        } else if self.current_gear == 0 {
286            2
287        } else {
288            self.current_gear
289        };
290        self.reverse_input = on;
291    }
292
293    /// Otomatik vites — RPM eşiğine göre upshift/downshift
294    pub fn auto_shift_tick(&mut self, dt: f32) {
295        if !self.auto_shift || self.reverse_input {
296            return;
297        }
298        self.shift_cooldown = (self.shift_cooldown - dt).max(0.0);
299        if self.shift_cooldown > 0.0 {
300            return;
301        }
302        let max_gear = self.tuning.gear_ratios.len() - 1;
303        if self.engine_rpm > self.tuning.upshift_rpm && self.current_gear < max_gear {
304            self.current_gear += 1;
305            self.shift_cooldown = 0.4; // 400ms bekleme
306        } else if self.engine_rpm < self.tuning.downshift_rpm && self.current_gear > 2 {
307            self.current_gear -= 1;
308            self.shift_cooldown = 0.3;
309        }
310    }
311}
312
313// ============================================================
314// ARAÇ GÜNCELLEME FONKSİYONU
315// ============================================================
316
317pub fn update_vehicle(
318    vehicle_entity: Entity,
319    vehicle: &mut VehicleController,
320    vehicle_rb: &mut RigidBody,
321    vehicle_transform: &Transform,
322    vehicle_vel: &mut Velocity,
323    all_colliders: &[(Entity, Transform, Collider)],
324    dt: f32,
325) {
326    if vehicle_rb.is_static() {
327        return;
328    }
329
330    // Yerel eksenler
331    let up = vehicle_transform
332        .rotation
333        .mul_vec3(Vec3::new(0.0, 1.0, 0.0));
334    let forward = vehicle_transform
335        .rotation
336        .mul_vec3(Vec3::new(0.0, 0.0, -1.0));
337    let right = vehicle_transform
338        .rotation
339        .mul_vec3(Vec3::new(1.0, 0.0, 0.0));
340
341    let v_com = vehicle_vel.linear;
342    let forward_speed = v_com.dot(forward);
343    vehicle.current_speed_kmh = forward_speed * 3.6;
344
345    // --------------------------------------------------------
346    // 1. GÜÇ AKTARMA ORGANı
347    // --------------------------------------------------------
348    let gear_ratio = vehicle
349        .tuning
350        .gear_ratios
351        .get(vehicle.current_gear)
352        .copied()
353        .unwrap_or(0.0);
354    let total_ratio = gear_ratio * vehicle.tuning.final_drive_ratio;
355
356    // RPM ← arka tekerlek angular_velocity ortalamasından
357    let mut avg_rear_ω = 0.0f32;
358    let mut rear_count = 0.0f32;
359    for w in &vehicle.wheels {
360        if w.axle_type == Axle::Rear {
361            avg_rear_ω += w.angular_velocity;
362            rear_count += 1.0;
363        }
364    }
365    if rear_count > 0.0 {
366        avg_rear_ω /= rear_count;
367    }
368
369    let wheel_rpm = avg_rear_ω.abs() * 9.549; // rad/s → rpm
370    vehicle.engine_rpm =
371        (wheel_rpm * total_ratio.abs()).clamp(vehicle.tuning.idle_rpm, vehicle.tuning.max_rpm);
372
373    let engine_torque = vehicle.get_engine_torque();
374    // Geri viteste tork yönü ters
375    let torque_sign = if total_ratio < 0.0 { -1.0 } else { 1.0 };
376    let drive_torque_total = engine_torque * total_ratio.abs() * torque_sign;
377
378    // --------------------------------------------------------
379    // 1.5 Otomatik vites
380    // --------------------------------------------------------
381    vehicle.auto_shift_tick(dt);
382
383    // --------------------------------------------------------
384    // 2. AERODİNAMİK (fiziksel — ½ρCdAv²)
385    // --------------------------------------------------------
386    const AIR_DENSITY: f32 = 1.225; // kg/m³
387    let spd = v_com.length();
388    let spd_sq = spd * spd;
389    let a = &vehicle.tuning.aero;
390    let q = 0.5 * AIR_DENSITY * spd_sq; // dinamik basınç
391
392    // Zemin etkisi: alçak araçlarda downforce artar
393    let height_above_ground = vehicle
394        .wheels
395        .iter()
396        .filter(|w| w.is_grounded)
397        .filter_map(|w| w.ground_hit.as_ref().map(|hit| hit.distance - 0.5)) // 0.5 is ray_origin_offset
398        .fold(f32::MAX, f32::min);
399    let ge_factor = if height_above_ground < a.ground_effect_height {
400        a.ground_effect_multiplier
401    } else {
402        1.0
403    };
404
405    let drag_dir = if spd > 0.1 { -v_com / spd } else { Vec3::ZERO };
406    let drag_force = drag_dir * (a.drag_coefficient * a.frontal_area * q);
407    let lift_force = up * (a.lift_coefficient * a.frontal_area * q * ge_factor);
408
409    // Aero kuvvetini basınç merkezinden uygula (tork üretir)
410    let cop_world =
411        vehicle_transform.position + vehicle_transform.rotation.mul_vec3(a.center_of_pressure);
412    let com = vehicle_transform.position
413        + vehicle_transform
414            .rotation
415            .mul_vec3(vehicle_rb.center_of_mass);
416    apply_force_at_point(
417        vehicle_rb,
418        vehicle_vel,
419        com,
420        vehicle_transform.rotation,
421        drag_force + lift_force,
422        cop_world,
423        dt,
424    );
425
426    // --------------------------------------------------------
427    // 3. ACKERMANN DİREKSİYON
428    // --------------------------------------------------------
429    let steer_angle = vehicle.steering_input * vehicle.max_steering_angle;
430    let turn_radius = if steer_angle.abs() > 0.01 {
431        vehicle.tuning.wheelbase / steer_angle.tan()
432    } else {
433        f32::MAX
434    };
435
436    // --------------------------------------------------------
437    // 4. TEKERLEK DÖNGÜSÜ — 1. geçiş: Raycast + Süspansiyon setup
438    // --------------------------------------------------------
439    let rear_count_f = rear_count.max(1.0);
440
441    for wheel in &mut vehicle.wheels {
442        let attach_world = vehicle_transform.position
443            + vehicle_transform
444                .rotation
445                .mul_vec3(wheel.attachment_local_pos);
446        let ray_dir = vehicle_transform
447            .rotation
448            .mul_vec3(wheel.direction_local)
449            .normalize();
450
451        // Ray origin'i attach_world'den biraz geriye al (yukarıya) ki araç yere tam oturduğunda
452        // raycast origin'i yerin içinde kalıp çarpışmayı kaçırmasın!
453        let ray_origin_offset = 0.5;
454        let ray_start = attach_world - ray_dir * ray_origin_offset;
455        let ray_max = wheel.suspension_rest_length
456            + wheel.radius
457            + wheel.suspension_max_travel
458            + ray_origin_offset;
459        let ray = Ray::new(ray_start, ray_dir);
460
461        // Raycast
462        let mut closest_hit: Option<RaycastHit> = None;
463        let mut closest_dist = ray_max;
464
465        for (other_ent, other_trans, other_col) in all_colliders {
466            if *other_ent == vehicle_entity || other_col.is_trigger {
467                continue;
468            }
469            let aabb = other_col.compute_aabb(other_trans.position, other_trans.rotation);
470            if Raycast::ray_aabb(&ray, &aabb).is_none() {
471                continue;
472            }
473            if let Some((dist, normal)) = Raycast::ray_shape(&ray, &other_col.shape, other_trans) {
474                if dist < closest_dist {
475                    closest_dist = dist;
476                    closest_hit = Some(RaycastHit {
477                        entity: *other_ent,
478                        point: ray.point_at(dist),
479                        normal,
480                        distance: dist,
481                    });
482                }
483            }
484        }
485
486        if let Some(hit) = closest_hit {
487            wheel.is_grounded = true;
488            wheel.ground_hit = Some(hit);
489
490            // Gerçek mesafe için eklediğimiz offseti çıkarıyoruz
491            let actual_dist = closest_dist - ray_origin_offset;
492
493            // Süspansiyon sıkışması: yay uzunluğu = çarpma mesafesi - tekerlek yarıçapı
494            let raw_len = (actual_dist - wheel.radius).clamp(
495                wheel.suspension_rest_length - wheel.suspension_max_travel,
496                wheel.suspension_rest_length + wheel.suspension_max_travel,
497            );
498            wheel.suspension_length = raw_len;
499        } else {
500            wheel.is_grounded = false;
501            wheel.ground_hit = None;
502            wheel.suspension_length = wheel.suspension_rest_length;
503            wheel.suspension_force = 0.0;
504        }
505
506        // Ackermann açısı (ön tekerlek)
507        if wheel.axle_type == Axle::Front {
508            let sign = if wheel.is_left { 1.0 } else { -1.0 };
509            wheel.steering_angle = if turn_radius.abs() < 1e4 {
510                (vehicle.tuning.wheelbase / (turn_radius + sign * vehicle.tuning.track_width * 0.5))
511                    .atan()
512            } else {
513                steer_angle
514            };
515        }
516
517        // Tork dağıtımı (RWD)
518        wheel.drive_torque = if wheel.axle_type == Axle::Rear {
519            drive_torque_total / rear_count_f
520        } else {
521            0.0
522        };
523
524        // Fren dağıtımı (%60 ön / %40 arka)
525        let bias = if wheel.axle_type == Axle::Front {
526            0.6
527        } else {
528            0.4
529        };
530        wheel.brake_torque = vehicle.brake_input * vehicle.tuning.max_brake_torque * bias;
531    }
532
533    // --------------------------------------------------------
534    // 5. Anti-roll bar farkları
535    // --------------------------------------------------------
536    let (mut fl, mut fr, mut rl, mut rr) = (0.0f32, 0.0f32, 0.0f32, 0.0f32);
537    for w in &vehicle.wheels {
538        let travel = w.suspension_rest_length - w.suspension_length;
539        match (&w.axle_type, w.is_left) {
540            (Axle::Front, true) => fl = travel,
541            (Axle::Front, false) => fr = travel,
542            (Axle::Rear, true) => rl = travel,
543            (Axle::Rear, false) => rr = travel,
544        }
545    }
546    let front_diff = fl - fr;
547    let rear_diff = rl - rr;
548
549    // --------------------------------------------------------
550    // 6. TEKERLEK DÖNGÜSÜ — 2. geçiş: Kuvvetler + Tekerlek integrasyon
551    // --------------------------------------------------------
552
553    for wheel in &mut vehicle.wheels {
554        let attach_world = vehicle_transform.position
555            + vehicle_transform
556                .rotation
557                .mul_vec3(wheel.attachment_local_pos);
558        let ray_dir = vehicle_transform
559            .rotation
560            .mul_vec3(wheel.direction_local)
561            .normalize();
562
563        // --- YAY KUVVET ENTEGRASYONu (her zaman, grounded veya değil) ---
564        // Tekerlek ataletini (I = 0.5 m r²) hesapla
565        let wheel_inertia = 0.5 * wheel.wheel_mass * wheel.radius.powi(2);
566
567        if wheel.is_grounded {
568            if let Some(hit) = wheel.ground_hit.as_ref() {
569                // 6.1 Gelişmiş Süspansiyon: baskı/geri dönüş ayrı damper
570                let point_rel = attach_world - vehicle_transform.position;
571                let point_vel = vehicle_vel.linear + vehicle_vel.angular.cross(point_rel);
572                let susp_vel = point_vel.dot(ray_dir); // pozitif = yay sıkışıyor
573                let compression = wheel.suspension_rest_length - wheel.suspension_length;
574
575                let spring_force = wheel.suspension_stiffness * compression;
576
577                // Baskı: damping_compression, geri dönüş: damping_rebound (genelde 2-3x baskı)
578                let damper_coeff = if susp_vel > 0.0 {
579                    wheel.suspension_damping // baskı katsayısı
580                } else {
581                    wheel.suspension_damping * 2.5 // rebound (daha sert)
582                };
583                let damper_force = damper_coeff * susp_vel;
584
585                // Bump stop: max seyahat sonunda sert non-linear yay
586                let bump_stop_travel = wheel.suspension_max_travel * 0.1;
587                let bump_excess = compression - (wheel.suspension_max_travel - bump_stop_travel);
588                let bump_stop_force = if bump_excess > 0.0 {
589                    bump_excess * wheel.suspension_stiffness * 8.0
590                } else {
591                    0.0
592                };
593
594                // Anti-roll bar
595                let arb_force = match wheel.axle_type {
596                    Axle::Front => {
597                        if wheel.is_left {
598                            -front_diff
599                        } else {
600                            front_diff
601                        }
602                    }
603                    Axle::Rear => {
604                        if wheel.is_left {
605                            -rear_diff
606                        } else {
607                            rear_diff
608                        }
609                    }
610                } * vehicle.tuning.anti_roll_stiffness;
611
612                wheel.suspension_force =
613                    (spring_force + damper_force + bump_stop_force + arb_force).max(0.0);
614                let susp_impulse = (-ray_dir) * wheel.suspension_force;
615                apply_force_at_point(
616                    vehicle_rb,
617                    vehicle_vel,
618                    com,
619                    vehicle_transform.rotation,
620                    susp_impulse,
621                    attach_world,
622                    dt,
623                );
624
625                // 6.2 Pacejka Kuvvetleri
626                let steering_rot = Quat::from_axis_angle(up, wheel.steering_angle);
627                let wheel_forward = steering_rot.mul_vec3(forward).normalize();
628                let wheel_right = steering_rot.mul_vec3(right).normalize();
629
630                let v_long = point_vel.dot(wheel_forward);
631                let v_lat = point_vel.dot(wheel_right);
632
633                // Denom: düşük hızda sıfır bölünmeyi önle
634                let ref_vel = v_long.abs().max(0.5);
635
636                // Longitudinal slip ratio
637                let wheel_linear_vel = wheel.angular_velocity * wheel.radius;
638                let slip_ratio = (wheel_linear_vel - v_long) / ref_vel;
639
640                // Lateral slip angle [rad]
641                let slip_angle = -(v_lat / ref_vel).atan();
642
643                let normal_load = wheel.suspension_force;
644
645                // Kombine Pacejka MF — sürtünme çemberi dahilinde
646                let (final_long, final_lat) = pacejka_combined(
647                    &wheel.pacejka_long,
648                    &wheel.pacejka_lat,
649                    slip_ratio,
650                    slip_angle,
651                    normal_load,
652                );
653
654                // Lastik kuvvetini temas noktasından uygula
655                let tire_force = wheel_forward * final_long + wheel_right * final_lat;
656                let contact_pt = hit.point;
657                apply_force_at_point(
658                    vehicle_rb,
659                    vehicle_vel,
660                    com,
661                    vehicle_transform.rotation,
662                    tire_force,
663                    contact_pt,
664                    dt,
665                );
666
667                // 6.3 Tekerlek angular_velocity entegrasyonu (Semi-implicit Euler)
668                // Reaksiyon torku lastikten gelen geri tepme
669                let reaction_torque = final_long * wheel.radius;
670
671                // Fren torku: tekerlek dönüşünün tersine
672                let brake_dir = if wheel.angular_velocity.abs() > 0.01 {
673                    -wheel.angular_velocity.signum()
674                } else {
675                    0.0
676                };
677                let effective_brake = wheel.brake_torque * brake_dir;
678
679                // Net tork
680                let net_torque = wheel.drive_torque + effective_brake - reaction_torque;
681
682                // Semi-implicit: önce hızı güncelle, sonra pozisyonu
683                wheel.angular_velocity += (net_torque / wheel_inertia) * dt;
684
685                // Fren kilitleme: abs >= tekerlek hızı değilse sıfırla
686                let max_brake_decel = wheel.brake_torque / wheel_inertia * dt;
687                if vehicle.brake_input > 0.01 && wheel.angular_velocity.abs() < max_brake_decel {
688                    wheel.angular_velocity = 0.0;
689                }
690            }
691        } else {
692            // Havada: sadece motor + fren, yay kuvveti yok
693            wheel.suspension_force = 0.0;
694
695            let brake_dir = if wheel.angular_velocity.abs() > 0.01 {
696                -wheel.angular_velocity.signum()
697            } else {
698                0.0
699            };
700
701            let effective_brake = wheel.brake_torque * brake_dir;
702            let net_torque = wheel.drive_torque + effective_brake;
703            wheel.angular_velocity += (net_torque / wheel_inertia) * dt;
704
705            // Fren kilitleme: abs >= tekerlek hızı değilse sıfırla
706            let max_brake_decel = wheel.brake_torque / wheel_inertia * dt;
707            if vehicle.brake_input > 0.01 && wheel.angular_velocity.abs() < max_brake_decel {
708                wheel.angular_velocity = 0.0;
709            }
710        }
711
712        // dt-doğru sönümleme: (1 - coeff * dt) ≈ exp(-coeff * dt)
713        let damping_coeff = 2.0; // rad/s² / (rad/s)
714        wheel.angular_velocity *= (1.0 - damping_coeff * dt).max(0.0);
715
716        // Çok yavaşsa ve girdi yoksa dur
717        if wheel.angular_velocity.abs() < 0.05
718            && wheel.drive_torque.abs() < 1.0
719            && wheel.brake_torque < 1.0
720        {
721            wheel.angular_velocity = 0.0;
722        }
723
724        // Görsel rotasyon
725        wheel.rotation_angle += wheel.angular_velocity * dt;
726        wheel.rotation_angle %= std::f32::consts::TAU;
727    }
728}
729
730// ============================================================
731// YARDIMCI FONKSİYONLAR
732// ============================================================
733
734/// Merkezi kuvvet (tork olmadan)
735#[allow(dead_code)]
736fn apply_force_central(rb: &RigidBody, vel: &mut Velocity, force: Vec3, dt: f32) {
737    if rb.is_static() {
738        return;
739    }
740    vel.linear += force * rb.inv_mass() * dt;
741}
742
743/// Belirli bir noktadan kuvvet uygulama — tork üretir
744fn apply_force_at_point(
745    rb: &RigidBody,
746    vel: &mut Velocity,
747    center_of_mass: Vec3,
748    rotation: Quat,
749    force: Vec3,
750    point: Vec3,
751    dt: f32,
752) {
753    if rb.is_static() {
754        return;
755    }
756    vel.linear += (force * rb.inv_mass()) * dt;
757    let torque = (point - center_of_mass).cross(force);
758    vel.angular += (rb.inv_world_inertia_tensor(rotation) * torque) * dt;
759}
760
761#[cfg(test)]
762mod tests {
763    use super::*;
764
765    #[test]
766    fn test_suspension_spring_and_damper_math() {
767        // "Kuvvet Testi: Bir yaya 10cm sıkışma uygulandığında, sönümleme katsayısı X iken..."
768        let stiffness = 25000.0; // N/m (Süspansiyon yay sertliği)
769        let compression = 0.1; // 0.1 m (10 cm sıkışma)
770        let spring_force = stiffness * compression;
771
772        // Yay tam 0.1 metre sıkıştığında, Hooke Kanunu'na göre (F = k*x) 2500N kuvvet üretmeli.
773        assert_eq!(spring_force, 2500.0, "Hooke's Law spring force failed");
774
775        // Sönümleme (Damper) Testi
776        let damping_compression = 3000.0; // N*s/m (Sönümleme katsayısı)
777        let susp_vel_compressing = 1.0; // 1 m/s hızla sıkışıyor (amortisör direnci)
778
779        // Baskı sırasında damper kuvveti hıza zıt (dirençli) ve pozitif olmalı (F = c*v)
780        let damper_force = damping_compression * susp_vel_compressing;
781        assert_eq!(damper_force, 3000.0, "Damper force calculation failed");
782
783        // Toplam Süspansiyon Kuvveti (Yay + Amortisör)
784        let total_suspension_force = spring_force + damper_force;
785        assert_eq!(
786            total_suspension_force, 5500.0,
787            "Total suspension force calculation failed"
788        );
789    }
790
791    #[test]
792    fn test_pacejka_combined_slip() {
793        let long = PacejkaParams::default();
794        let lat = PacejkaLat::default();
795        let normal_load = 5000.0; // 500 kg tekerlek yükü (Fz)
796
797        // 1. Durum: Sıfır Slip (Kayma Yok)
798        let (fx1, fy1) = pacejka_combined(&long, &lat, 0.0, 0.0, normal_load);
799        assert!(
800            fx1.abs() < 1e-4,
801            "Expected zero longitudinal force at zero slip"
802        );
803        assert!(fy1.abs() < 1e-4, "Expected zero lateral force at zero slip");
804
805        // 2. Durum: Sadece İleri Kayma (Burnout/Frenleme)
806        let (fx2, fy2) = pacejka_combined(&long, &lat, 0.15, 0.0, normal_load);
807        let expected_fx2 = long.calculate_force(0.15, normal_load);
808        assert!(
809            (fx2 - expected_fx2).abs() < 1e-4,
810            "Expected combined force to match pure force when no lateral slip is present"
811        );
812        assert!(
813            fy2.abs() < 1e-4,
814            "Expected zero lateral force when purely accelerating straight"
815        );
816
817        // 3. Durum: Kombine Kayma (Virajda Gazlama - Friction Circle Test)
818        // Her iki yönde kayma olduğunda (Drift durumu), eksenler birbirinin tutuşunu düşürmeli (Weighting)
819        let (fx3, fy3) = pacejka_combined(&long, &lat, 0.15, 0.15, normal_load);
820
821        // fx3, fx2'den (sadece düz gitmekten) çok daha düşük olmalıdır çünkü yanal kuvvet (fy3) de yol tutuşundan pay alıyor
822        assert!(
823            fx3 < fx2,
824            "Combined slip should reduce longitudinal grip (Friction Circle violated)"
825        );
826        assert!(
827            fy3 > 1000.0,
828            "Expected significant lateral force during cornering"
829        );
830    }
831}