Иконка ресурса

FREE BetterCars : 0.9.0 0.9.0

Нет прав для скачивания
C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using System;
using System.Collections.Generic;
using System.Linq;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using UnityEngine;
using Rust;
using Network;
namespace Oxide.Plugins {
    [Info("BetterCars", "S0F1st1Kt3dB3ar", "0.9.0")]
    [Description("Improves modular car suspension, grip, and roll control with presets and per-axle tuning.")]
    public class BetterCars : CovalencePlugin {
        private const string PERM_USE = "BetterCars.use";
#region Config
        private ConfigData config;
        private class Preset {
            public float FrontSpring = 38000f;
            public float RearSpring = 42000f;
            public float FrontDamper = 2800f;
            public float RearDamper = 3000f;
            public float SuspDistance = 0.20f;
            // meters of travel
            public float TargetPos = 0.5f;
            // 0..1 spring preload position
            // Tire friction stiffness (multiplies the curve)
            public float ForwardStiffness = 1.8f;
            public float SidewaysStiffness = 2.0f;
            // Anti-roll bar strength (per axle)
            public float AntiRollFront = 16000f;
            public float AntiRollRear = 18000f;
            // Optional helpers
            public bool SpeedSteerAssist = true;
            public float SteerAssistMinSpeed = 12f;
            // m/s (~27mph)
            public float SteerAssistFactor = 0.35f;
            // Downforce (N) at highway speed, scaled by v^2 (small but helps stability)
            public float DownforceN = 900f;
            // Clone helper for tweaks without mutating original preset data in config values
            public Preset Clone() => (Preset)MemberwiseClone();
        }
        private class ConfigData {
            public string DefaultPreset = "sport";
            public Dictionary<string, Preset> Presets = new Dictionary<string, Preset> {
                ["sport"]
                = new Preset {
                    FrontSpring=42000, RearSpring=46000, FrontDamper=3200, RearDamper=3400, SuspDistance=0.18f, TargetPos=0.55f, ForwardStiffness=2.0f, SidewaysStiffness=2.2f, AntiRollFront=20000, AntiRollRear=22000, DownforceN=1100
                }
                , ["offroad"] = new Preset {
                    FrontSpring=32000, RearSpring=36000, FrontDamper=2400, RearDamper=2600, SuspDistance=0.26f, TargetPos=0.45f, ForwardStiffness=1.6f, SidewaysStiffness=1.7f, AntiRollFront=9000, AntiRollRear=10000, DownforceN=700
                }
                , ["drift"] = new Preset {
                    FrontSpring=38000, RearSpring=42000, FrontDamper=2800, RearDamper=3000, SuspDistance=0.20f, TargetPos=0.55f, ForwardStiffness=1.5f, SidewaysStiffness=1.1f,  // looser lateral grip
                    AntiRollFront=12000, AntiRollRear=14000, DownforceN=600, SteerAssistFactor=0.2f
                }
                , ["tow"] = new Preset {
                    FrontSpring=42000, RearSpring=52000,  // stiffer rear for tongue weight
                    FrontDamper=3200, RearDamper=3800, SuspDistance=0.19f, TargetPos=0.6f, ForwardStiffness=2.0f, SidewaysStiffness=2.2f, AntiRollFront=20000, AntiRollRear=26000, DownforceN=1400
                }
                , ["stock"] = new Preset {
                    FrontSpring=32000, RearSpring=32000, FrontDamper=2200, RearDamper=2200, SuspDistance=0.22f, TargetPos=0.5f, ForwardStiffness=1.3f, SidewaysStiffness=1.3f, AntiRollFront=8000, AntiRollRear=8000, DownforceN=0, SteerAssistFactor=0.0f
                }
            }
            ;
            // If true and TowCars link detected, apply "tow" base with mild extra rear bias
            public bool TowAware = true;
            public float TowExtraRearSpring = 4000f;
            // added to preset rear spring when towing
            public float TowExtraRearDamper = 400f;
            // Engine awareness
            public bool UseEngineAwareness = true;
            // watch engine state and swap presets
            public string EngineOffPresetKey = "stock";
            public float EnginePollSeconds = 0.25f;
            // 4x/sec is plenty
        }
#endregion
#region Data & State
        private readonly Dictionary<ulong, string> playerPresetChoice = new Dictionary<ulong, string>();
        private readonly Dictionary<ModularCar, Preset> carPreset = new Dictionary<ModularCar, Preset>();
        // Last-known engine state for cars we've touched
        private readonly Dictionary<ModularCar, bool> _engineOn = new Dictionary<ModularCar, bool>();
        private Timer _engineWatchTimer;
        // Cached probe delegate for “is engine on”
        private static System.Func<ModularCar, bool> _engineProbe;
#endregion
#region Hooks
        private readonly Dictionary<ulong, float> _lastSwitchAt = new Dictionary<ulong, float>();
        private static readonly string[] _modeOrder = {
            "sport", "offroad", "drift", "tow", "stock"
        }
        ;
        private const float _switchCooldown = 0.35f;
        // seconds
        private void OnPlayerInput(BasePlayer player, InputState input) {
            if (player == null || input == null || !player.IsConnected) return;
            if (!permission.UserHasPermission(player.UserIDString, PERM_USE)) return;
            var car = player.GetMountedVehicle() as ModularCar ?? player.GetParentEntity() as ModularCar;
            if (car == null) return;
            if (!input.WasJustPressed(BUTTON.FIRE_THIRD)) return;
            float now = Time.realtimeSinceStartup;
            if (_lastSwitchAt.TryGetValue(player.userID, out float last) && (now - last) < _switchCooldown) return;
            _lastSwitchAt[player.userID] = now;
            string currentKey = config.DefaultPreset;
            if (playerPresetChoice.TryGetValue(player.userID, out var saved) && config.Presets.ContainsKey(saved)) currentKey = saved;
            string nextKey = NextModeKey(currentKey);
            if (!config.Presets.TryGetValue(nextKey, out var nextPreset)) return;
            // 🚫 Do not apply while engine is OFF or engine state is unknown
            if (config.UseEngineAwareness && (!TryIsEngineOn(car, out var on) || !on)) {
                playerPresetChoice[player.userID] = nextKey;
                // remember choice
                player.ChatMessage($"<color=#9cf>BetterCars</color>: <b>{Title(nextKey)}</b> queued — start the engine to apply.");
                return;
            }
            // Engine is ON → apply immediately
            TryApplyToCar(car, PrepareTowAwarePreset(car, nextPreset));
            playerPresetChoice[player.userID] = nextKey;
            player.ChatMessage($"<color=#9cf>BetterCars</color>: Drive mode set to <b>{Title(nextKey)}</b>");
        }
        // Helpers for cycling & label
        private string NextModeKey(string current) {
            int idx = Array.FindIndex(_modeOrder, k => string.Equals(k, current, StringComparison.OrdinalIgnoreCase));
            for (int i = 1; i <= _modeOrder.Length; i++) {
                string candidate = _modeOrder[(idx + i + _modeOrder.Length) % _modeOrder.Length];
                if (config.Presets.ContainsKey(candidate)) return candidate;
            }
            return "sport";
        }
        private string Title(string key) => string.IsNullOrEmpty(key) ? "" : char.ToUpperInvariant(key[0]) + key.Substring(1);
        private void Init() {
            permission.RegisterPermission(PERM_USE, this);
            // Safe read (ReadObject can return null on first run or malformed file)
            try {
                config = Config.ReadObject<ConfigData>() ?? new ConfigData();
            }
            catch {
                config = new ConfigData();
            }
            // If you really want to reset to fresh defaults when presets are missing, do it *before*
            // setting per-field defaults so we don't wipe them afterwards.if (config.Presets == null || config.Presets.Count == 0)
            config = new ConfigData();
            // Now apply/normalize engine-aware defaults
            if (string.IsNullOrEmpty(config.EngineOffPresetKey)) config.EngineOffPresetKey = "stock";
            // Clamp poll interval (protect against 0/negative)
            config.EnginePollSeconds = Mathf.Clamp(config.EnginePollSeconds <= 0f ? 0.25f : config.EnginePollSeconds, 0.1f, 1.0f );
            // Write back so any new fields appear in the JSON
            SaveConfig();
        }
        protected override void LoadDefaultConfig() => config = new ConfigData();
        private void OnServerInitialized() {
            // Apply to existing cars after startup
            NextTick(() => {
                foreach (var car in BaseNetworkable.serverEntities.OfType<ModularCar>()) TryApplyToCar(car, GetDefaultPresetForCar(car));
            }
            );
            if (config.UseEngineAwareness && config.EnginePollSeconds > 0f) {
                _engineWatchTimer?.Destroy();
                _engineWatchTimer = timer.Every(config.EnginePollSeconds, EngineWatchTick);
            }
        }
        private void Unload() {
            _engineWatchTimer?.Destroy();
            _engineWatchTimer = null;
            _engineOn.Clear();
            foreach (var car in carPreset.Keys.ToList()) {
                if (!car || car.IsDestroyed) {
                    carPreset.Remove(car);
                    continue;
                }
                CHP_AntiRollHelper comp = null;
                try {
                    comp = car.GetComponent<CHP_AntiRollHelper>();
                }
                catch {
                    /* car destroyed mid-check */
                }
                if (comp) UnityEngine.Object.Destroy(comp);
            }
            carPreset.Clear();
        }
        private void OnEntitySpawned(BaseNetworkable ent) {
            var car = ent as ModularCar;
            if (car == null) return;
            NextTick(() => TryApplyToCar(car, GetDefaultPresetForCar(car)));
        }
        private void OnEntityMounted(BaseMountable mountable, BasePlayer player) {
            if (player == null || !player.IsConnected) return;
            var car = mountable?.GetComponentInParent<ModularCar>();
            if (car == null) return;
            // Always go to safe physics immediately when someone mounts.ApplyPresetKey(car, config.EngineOffPresetKey);
            // Reset our cached engine state for this car; we'll promote via EngineWatchTick on OFF→ON._engineOn[car] = false;
            // If engine is already ON and we can see it, promote immediately to the driver's choice
            if (config.UseEngineAwareness && TryIsEngineOn(car, out var onNow) && onNow) {
                var wanted = ResolveActivePresetForCar(car);
                ApplyPresetKey(car, wanted);
                _engineOn[car] = true;
                player.ChatMessage($"<color=#9cf>BetterCars</color>: Engine detected ON — applied <b>{Title(wanted)}</b>.");
                return;
            }
            // If they have a saved choice, just tell them we’ll apply it once the engine is started.if (playerPresetChoice.TryGetValue(player.userID, out var key) && config.Presets.ContainsKey(key))
            {
                if (!config.UseEngineAwareness) {
                    // If engine awareness is disabled, apply instantly (old behavior).TryApplyToCar(car, PrepareTowAwarePreset(car, config.Presets[key]));
                    player.ChatMessage($"<color=#9cf>BetterCars</color>: Applied <b>{key}</b>.");
                }
                else {
                    player.ChatMessage($"<color=#9cf>BetterCars</color>: Engine is off — using <b>{Title(config.EngineOffPresetKey)}</b> until you start it.");
                }
            }
        }
#endregion
#region Core Apply
        private Preset GetDefaultPresetForCar(ModularCar car) {
            var key = string.IsNullOrEmpty(config.DefaultPreset) ? "sport" : config.DefaultPreset;
            if (!config.Presets.TryGetValue(key, out var p)) p = new Preset();
            return PrepareTowAwarePreset(car, p);
        }
        private Preset PrepareTowAwarePreset(ModularCar car, Preset basePreset) {
            var p = basePreset.Clone();
            if (!config.TowAware) return p;
            // Lightweight TowCars link detection: look for a child GameObject named with "tow" rope or a joint we tag.// This is intentionally generic to avoid hard dependency.bool looksTowing = car.gameObject.GetComponentsInChildren<Joint>(true).Any(j => j.name.IndexOf("tow", StringComparison.OrdinalIgnoreCase) >= 0)
            || car.gameObject.GetComponentsInChildren<Transform>(true).Any(t => t.name.IndexOf("towrope", StringComparison.OrdinalIgnoreCase) >= 0);
            if (looksTowing) {
                p.RearSpring += config.TowExtraRearSpring;
                p.RearDamper += config.TowExtraRearDamper;
            }
            return p;
        }
        private void TryApplyToCar(ModularCar car, Preset preset) {
            if (car == null || car.IsDestroyed) return;
            var wheels = car.GetComponentsInChildren<WheelCollider>(true);
            if (wheels == null || wheels.Length == 0) return;
            // Rough axle grouping by local Z (front negative-ish, rear positive-ish); adjust as needed if Rust’s car prefab changes
            var ordered = wheels.OrderBy(w => w.transform.localPosition.z).ToList();
            var midZ = ordered.Average(w => w.transform.localPosition.z);
            foreach (var wc in wheels) {
                bool isFront = wc.transform.localPosition.z < midZ;
                wc.suspensionDistance = preset.SuspDistance;
                var spring = wc.suspensionSpring;
                spring.targetPosition = Mathf.Clamp01(preset.TargetPos);
                spring.spring = isFront ? preset.FrontSpring : preset.RearSpring;
                spring.damper = isFront ? preset.FrontDamper : preset.RearDamper;
                wc.suspensionSpring = spring;
                // Build a reasonable friction curve; we preserve existing slip/value geometry, scale stiffness
                var fwd = wc.forwardFriction;
                fwd.stiffness = preset.ForwardStiffness;
                wc.forwardFriction = fwd;
                var side = wc.sidewaysFriction;
                side.stiffness = preset.SidewaysStiffness;
                wc.sidewaysFriction = side;
            }
            // Attach/update helper component
            var helper = car.GetComponent<CHP_AntiRollHelper>() ?? car.gameObject.AddComponent<CHP_AntiRollHelper>();
            helper.Apply(car, preset);
            carPreset[car] = preset;
        }
        private void EngineWatchTick() {
            if (!config.UseEngineAwareness || carPreset.Count == 0) return;
            foreach (var kv in carPreset.ToList()) {
                var car = kv.Key;
                if (!car || car.IsDestroyed) {
                    _engineOn.Remove(car);
                    carPreset.Remove(car);
                    continue;
                }
                // Treat "unknown" as OFF to avoid reapplying the aggressive preset.bool isOn = false;
                TryIsEngineOn(car, out isOn);
                bool prev;
                _engineOn.TryGetValue(car, out prev);
                if (prev == isOn) continue;
                _engineOn[car] = isOn;
                ApplyPresetKey(car, isOn ? ResolveActivePresetForCar(car) : config.EngineOffPresetKey);
            }
        }
        private bool TryIsEngineOn(ModularCar car, out bool isOn) {
            isOn = false;
            if (!car || car.IsDestroyed) return false;
            // If we learned a working probe, use it
            if (_engineProbe != null) {
                try {
                    isOn = _engineProbe(car);
                    return true;
                }
                catch {
                    /* fall through to rebuild */
                }
            }
            // Try to (re)build a probe
            var probe = BuildEngineProbe(car);
            if (probe != null) {
                _engineProbe = probe;
                try {
                    isOn = _engineProbe(car);
                    return true;
                }
                catch {
                    /* ignore */
                }
            }
            return false;
        }
        private System.Func<ModularCar, bool> BuildEngineProbe(ModularCar sample) {
            const System.Reflection.BindingFlags BF = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic;
            // --- (A) direct members on ModularCar -----------------------------------
            var carType = sample.GetType();
            string[] names = {
                "IsEngineOn", "IsOn", "EngineOn", "isEngineOn", "isOn", "IsRunning"
            }
            ;
            // property / field
            foreach (var n in names) {
                var p = carType.GetProperty(n, BF);
                if (p != null && p.PropertyType == typeof(bool)) return (ModularCar c) => (bool)p.GetValue(c);
                var f = carType.GetField(n, BF);
                if (f != null && f.FieldType == typeof(bool)) return (ModularCar c) => (bool)f.GetValue(c);
            }
            // zero-arg method returning bool
            foreach (var n in names) {
                var m = carType.GetMethod(n, BF, null, Type.EmptyTypes, null);
                if (m != null && m.ReturnType == typeof(bool)) return (ModularCar c) => (bool)m.Invoke(c, null);
            }
            // --- (B) find a controller component and read it -------------------------
            // Prefer *EngineController* types to avoid grabbing cosmetic "Engine..." bits.Component engineCompOnSample = null;
            try {
                engineCompOnSample = sample.GetComponentsInChildren<Component>(true).FirstOrDefault(c => {
                    if (c == null) return false;
                    var n = c.GetType().Name;
                    return n.IndexOf("EngineController", StringComparison.OrdinalIgnoreCase) >= 0 || n.IndexOf("VehicleEngineController", StringComparison.OrdinalIgnoreCase) >= 0 || (n.IndexOf("Engine", StringComparison.OrdinalIgnoreCase) >= 0 && n.IndexOf("Controller", StringComparison.OrdinalIgnoreCase) >= 0);
                }
                );
            }
            catch {
                /* ignore */
            }
            if (engineCompOnSample != null) {
                var et = engineCompOnSample.GetType();
                // property / field
                foreach (var n in names) {
                    var p = et.GetProperty(n, BF);
                    if (p != null && p.PropertyType == typeof(bool)) {
                        return (ModularCar c) => {
                            var comp = c ? c.GetComponentsInChildren(et, true).FirstOrDefault() : null;
                            return comp != null && (bool)p.GetValue(comp);
                        }
                        ;
                    }
                    var f = et.GetField(n, BF);
                    if (f != null && f.FieldType == typeof(bool)) {
                        return (ModularCar c) => {
                            var comp = c ? c.GetComponentsInChildren(et, true).FirstOrDefault() : null;
                            return comp != null && (bool)f.GetValue(comp);
                        }
                        ;
                    }
                }
                // zero-arg method returning bool
                foreach (var n in names) {
                    var m = et.GetMethod(n, BF, null, Type.EmptyTypes, null);
                    if (m != null && m.ReturnType == typeof(bool)) {
                        return (ModularCar c) => {
                            var comp = c ? c.GetComponentsInChildren(et, true).FirstOrDefault() : null;
                            return comp != null && (bool)m.Invoke(comp, null);
                        }
                        ;
                    }
                }
            }
            // --- (C) last chance: look for a car field/property that *is* a controller,
            // then query IsOn/IsRunning on that object.var engineHolder =
            carType.GetProperties(BF).FirstOrDefault(pi => pi.PropertyType.Name.IndexOf("Engine", StringComparison.OrdinalIgnoreCase) >= 0 && pi.PropertyType.Name.IndexOf("Controller", StringComparison.OrdinalIgnoreCase) >= 0) ?? carType.GetProperties(BF).FirstOrDefault(pi => pi.PropertyType.Name.IndexOf("VehicleEngineController", StringComparison.OrdinalIgnoreCase) >= 0) ?? null;
            if (engineHolder != null) {
                var ct = engineHolder.PropertyType;
                var pBool = new[] {
                    "IsOn", "IsRunning", "EngineOn"
                }
                .Select(n => ct.GetProperty(n, BF)).FirstOrDefault(pi => pi != null && pi.PropertyType == typeof(bool));
                var fBool = pBool == null ? new[] {
                    "isOn", "running"
                }
                .Select(n => ct.GetField(n, BF)).FirstOrDefault(fi => fi != null && fi.FieldType == typeof(bool)) : null;
                var mBool = (pBool == null && fBool == null) ? new[] {
                    "IsOn", "IsRunning", "EngineOn"
                }
                .Select(n => ct.GetMethod(n, BF, null, Type.EmptyTypes, null)).FirstOrDefault(mi => mi != null && mi.ReturnType == typeof(bool)) : null;
                if (pBool != null) return (ModularCar c) => {
                    var ctrl = engineHolder.GetValue(c);
                    return ctrl != null && (bool)pBool.GetValue(ctrl);
                }
                ;
                if (fBool != null) return (ModularCar c) => {
                    var ctrl = engineHolder.GetValue(c);
                    return ctrl != null && (bool)fBool.GetValue(ctrl);
                }
                ;
                if (mBool != null) return (ModularCar c) => {
                    var ctrl = engineHolder.GetValue(c);
                    return ctrl != null && (bool)mBool.Invoke(ctrl, null);
                }
                ;
            }
            // Give up – caller will treat "unknown" as OFF for safety.return null;
        }
        private void OnEntityDismounted(BaseMountable mountable, BasePlayer player) {
            if (!config.UseEngineAwareness) return;
            if (mountable == null || player == null || !player.IsConnected) return;
            var car = mountable.GetComponentInParent<ModularCar>();
            if (car == null) return;
            // If driver seat is now empty, go safe next tick
            if (car.GetDriver() != null) return;
            NextTick(() => {
                if (!car || car.IsDestroyed) return;
                ApplyPresetKey(car, config.EngineOffPresetKey);
            }
            );
        }
#endregion
#region Chat/Console
        [Command("carhandling")]
        private void CmdCarHandling(IPlayer iplayer, string cmd, string[] args) {
            var bp = iplayer.Object as BasePlayer;
            if (bp == null || !bp.isActiveAndEnabled || !bp.IsConnected) return;
            if (!iplayer.HasPermission(PERM_USE)) {
                iplayer.Reply("You lack permission (BetterCars.use).");
                return;
            }
            if (args.Length == 0) {
                iplayer.Reply("CarHandling+ — Usage:\n" + "/carhandling mode <sport|offroad|drift|tow|stock>\n" + "/carhandling set <param> <value>\n" + "Params: frontSpring, rearSpring, frontDamper, rearDamper, susp, target, fwdFric, sideFric");
                return;
            }
            var sub = args[0].ToLowerInvariant();
            var car = bp.GetMountedVehicle() as ModularCar ?? bp.GetParentEntity() as ModularCar ?? bp.GetComponentInParent<ModularCar>();
            // >>> REPLACE your old 'mode' block with this one <<<
            if (sub == "mode" && args.Length >= 2) {
                var key = args[1].ToLowerInvariant();
                if (!config.Presets.TryGetValue(key, out var preset)) {
                    iplayer.Reply($"Unknown preset '{key}'.");
                    return;
                }
                playerPresetChoice[bp.userID] = key;
                if (car != null) {
                    // Only apply if engine is ON; otherwise just queue it and keep safe physics
                    if (!config.UseEngineAwareness || (TryIsEngineOn(car, out var on) && on)) {
                        TryApplyToCar(car, PrepareTowAwarePreset(car, preset));
                        iplayer.Reply($"Applied <b>{key}</b> to current car.");
                    }
                    else {
                        ApplyPresetKey(car, config.EngineOffPresetKey);
                        iplayer.Reply($"Queued <b>{key}</b>.Engine is off — using <b>{config.EngineOffPresetKey}</b> until you start it.");
                    }
                }
                else {
                    iplayer.Reply($"Selected <b>{key}</b>.It’ll apply when you enter a car and start the engine.");
                }
                return;
            }
            if (sub == "set" && args.Length >= 3) {
                if (car == null) {
                    iplayer.Reply("You must be driving a modular car to use /carhandling set.");
                    return;
                }
                if (!carPreset.TryGetValue(car, out var p)) p = GetDefaultPresetForCar(car).Clone();
                string param = args[1].ToLowerInvariant();
                if (!float.TryParse(args[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var val)) {
                    iplayer.Reply("Value must be a number.");
                    return;
                }
                switch (param) {
                    case "frontspring" : p.FrontSpring = val;
                    break;
                    case "rearspring" : p.RearSpring = val;
                    break;
                    case "frontdamper" : p.FrontDamper = val;
                    break;
                    case "reardamper" : p.RearDamper = val;
                    break;
                    case "susp" : p.SuspDistance = Mathf.Clamp(val, 0.05f, 0.40f);
                    break;
                    case "target" : p.TargetPos = Mathf.Clamp01(val);
                    break;
                    case "fwdfric" : p.ForwardStiffness = Mathf.Max(0.2f, val);
                    break;
                    case "sidefric" : p.SidewaysStiffness = Mathf.Max(0.2f, val);
                    break;
                    default : iplayer.Reply("Unknown param.Use: frontSpring, rearSpring, frontDamper, rearDamper, susp, target, fwdFric, sideFric");
                    return;
                }
                TryApplyToCar(car, p);
                iplayer.Reply($"Updated <b>{param}</b> = {val}.Applied to your current car.");
                return;
            }
            iplayer.Reply("Unknown subcommand.Try /carhandling");
        }
        [ConsoleCommand("carhandling.globalmode")]
        private void CcmdGlobal(ConsoleSystem.Arg arg) {
            if (!arg.IsAdmin) return;
            var key = arg.GetString(0, "sport").ToLowerInvariant();
            if (!config.Presets.ContainsKey(key)) {
                Puts($"Unknown preset '{key}'.");
                return;
            }
            config.DefaultPreset = key;
            SaveConfig();
            Puts($"Default preset set to {key}.New cars will spawn with it.");
        }
#endregion
#region Helpers: Anti-roll + Downforce + Steering Assist
        private class CHP_AntiRollHelper : FacepunchBehaviour {
            private ModularCar car;
            private Preset preset;
            private WheelCollider[] wheels;
            private Rigidbody rb;
            public void Apply(ModularCar c, Preset p) {
                car = c;
                preset = p;
                wheels = car.GetComponentsInChildren<WheelCollider>(true);
                rb = car.GetComponent<Rigidbody>();
                enabled = true;
            }
            private void FixedUpdate() {
                if (car == null || car.IsDestroyed || wheels == null || wheels.Length == 0) {
                    enabled = false;
                    return;
                }
                // Group wheels by axle using local Z
                var midZ = wheels.Average(w => w.transform.localPosition.z);
                var front = wheels.Where(w => w.transform.localPosition.z < midZ).ToArray();
                var rear = wheels.Where(w => w.transform.localPosition.z >= midZ).ToArray();
                ApplyAntiRollAxle(front, preset.AntiRollFront);
                ApplyAntiRollAxle(rear, preset.AntiRollRear);
                // Downforce (v^2 scaled)
                if (preset.DownforceN > 0 && rb != null) {
                    float v = rb.velocity.magnitude;
                    // m/s
                    float scale = Mathf.Clamp01(v * v / (30f * 30f));
                    // ramps by ~30 m/s (~67 mph)
                    rb.AddForce(-car.transform.up * (preset.DownforceN * scale), ForceMode.Force);
                }
                // Speed-sensitive steering “softening”
                if (preset.SpeedSteerAssist && rb != null && preset.SteerAssistFactor > 0f) {
                    float v = rb.velocity.magnitude;
                    if (v > preset.SteerAssistMinSpeed) {
                        // Nudge the car’s yaw towards current velocity vector to stabilize quick oversteer snaps
                        Vector3 vel = rb.velocity;
                        vel.y = 0f;
                        if (vel.sqrMagnitude > 0.01f) {
                            Quaternion velRot = Quaternion.LookRotation(vel.normalized, Vector3.up);
                            Quaternion curRot = Quaternion.Euler(0f, car.transform.eulerAngles.y, 0f);
                            Quaternion tgt = Quaternion.Slerp(curRot, velRot, preset.SteerAssistFactor * Time.fixedDeltaTime);
                            Vector3 e = tgt.eulerAngles;
                            rb.MoveRotation(Quaternion.Euler(car.transform.eulerAngles.x, e.y, car.transform.eulerAngles.z));
                        }
                    }
                }
            }
            private void ApplyAntiRollAxle(WheelCollider[] axle, float strength) {
                if (axle == null || axle.Length < 2 || strength <= 0f) return;
                // Pair left/right by local X
                var ordered = axle.OrderBy(w => w.transform.localPosition.x).ToArray();
                var left = ordered.First();
                var right = ordered.Last();
                float travelL = GetSuspensionTravel(left);
                float travelR = GetSuspensionTravel(right);
                float antiRollForce = (travelL - travelR) * strength;
                if (left.GetGroundHit(out WheelHit hitL)) left.attachedRigidbody.AddForceAtPosition(left.transform.up * -antiRollForce, hitL.point);
                if (right.GetGroundHit(out WheelHit hitR)) right.attachedRigidbody.AddForceAtPosition(right.transform.up * antiRollForce, hitR.point);
            }
            private float GetSuspensionTravel(WheelCollider wc) {
                if (wc.GetGroundHit(out WheelHit hit)) {
                    return (-wc.transform.InverseTransformPoint(hit.point).y - wc.radius) / wc.suspensionDistance;
                }
                // fully extended
                return 1f;
            }
        }
        // Apply preset by key (respects TowAware)
        private void ApplyPresetKey(ModularCar car, string key) {
            if (!car || car.IsDestroyed) return;
            if (!config.Presets.TryGetValue(key, out var p)) p = new Preset();
            TryApplyToCar(car, PrepareTowAwarePreset(car, p));
        }
        // Resolve which preset to use when engine turns ON.// Uses the current driver's saved choice if available; else default.private string ResolveActivePresetForCar(ModularCar car)
        {
            try {
                var driver = car?.GetDriver();
                if (driver != null && playerPresetChoice.TryGetValue(driver.userID, out var key) && config.Presets.ContainsKey(key)) return key;
            }
            catch {
                /* ignore */
            }
            return string.IsNullOrEmpty(config.DefaultPreset) ? "sport" : config.DefaultPreset;
        }
#endregion
    }
}

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху