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

FREE HydroRust : 5.0.6 5.0.6

Нет прав для скачивания
C#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("HydroRust", "belisario-afk + copilot", "5.0.6")]
    [Description("Boat racing with automatic queue, battle mode, kits, voting, track editor, time trials.")]
    public class HydroRust : RustPlugin
    {
        #region Permissions & Constants

        private const string PERM_ADMIN = "hydrorust.admin";
        private const string PERM_PLAYER = "hydrorust.player";

        private const string DATA_TRACKS = "HydroRust_Tracks";
        private const string DATA_STATS = "HydroRust_PlayerStats";
        private const string DATA_ROTATION = "HydroRust_RotationIndex";

        private const float EditorRadiusStep = 0.5f;

        #endregion

        #region Config

        private enum TrackSelectionMode { CurrentSelected, Random, Rotation }

        private class KitItem
        {
            [JsonProperty] public string ShortName;
            [JsonProperty] public int Amount;
            [JsonProperty] public ulong SkinId;
        }

        private class KitDefinition
        {
            [JsonProperty] public string Name;
            [JsonProperty] public List<KitItem> Items = new List<KitItem>();
        }

        private class PluginConfig
        {
            [JsonProperty("HudUpdateIntervalSeconds")] public float HudUpdateIntervalSeconds = 0.3f;

            [JsonProperty("RaceCountdownSeconds")] public int RaceCountdownSeconds = 10;
            [JsonProperty("RaceTimeoutSeconds")] public int RaceTimeoutSeconds = 600;

            [JsonProperty("Physics.MaxSpeedMultiplier")] public float MaxSpeedMultiplier = 2.0f;
            [JsonProperty("Physics.BoostForce")] public float BoostForce = 20f;
            [JsonProperty("Physics.BoostDurationSeconds")] public float BoostDurationSeconds = 1.5f;
            [JsonProperty("Physics.BoostCooldownSeconds")] public float BoostCooldownSeconds = 4f;

            [JsonProperty("Boats.AllowedBoatPrefabs")] public List<string> AllowedBoatPrefabs = new List<string>
            {
                "assets/content/vehicles/boats/rowboat/rowboat.prefab",
                "assets/content/vehicles/boats/rhib/rhib.prefab"
            };

            [JsonProperty("Boats.SpawnNewRaceBoats")] public bool SpawnNewRaceBoats = true;

            [JsonProperty("Controls.OverrideBoatControls")] public bool OverrideBoatControls = true;
            [JsonProperty("Controls.ForwardForce")] public float ControlsForwardForce = 6f;
            [JsonProperty("Controls.ReverseForce")] public float ControlsReverseForce = 4f;
            [JsonProperty("Controls.SteerTorque")] public float ControlsSteerTorque = 3f;
            [JsonProperty("Controls.LinearDrag")] public float ControlsLinearDrag = 0.15f;
            [JsonProperty("Controls.BoostKeySprint")] public bool ControlsBoostKeySprint = true;
            [JsonProperty("Controls.BoostForce")] public float ControlsBoostForce = 12f;
            [JsonProperty("Controls.BoostMaxSpeedMultiplier")] public float ControlsBoostMaxSpeedMultiplier = 2.5f;

            [JsonProperty("Visuals.Enabled")] public bool VisualsEnabled = true;
            [JsonProperty("Visuals.CheckpointPrefab")] public string VisualsCheckpointPrefab = "assets/bundled/prefabs/modding/events/twitch/br_sphere_green.prefab";
            [JsonProperty("Visuals.FinishPrefab")] public string VisualsFinishPrefab = "assets/bundled/prefabs/modding/events/twitch/br_sphere_purple.prefab";
            [JsonProperty("Visuals.BoostPrefab")] public string VisualsBoostPrefab = "assets/bundled/prefabs/modding/events/twitch/br_sphere_red.prefab";
            [JsonProperty("Visuals.MarkerBaseScale")] public float VisualsMarkerBaseScale = 1.0f;
            [JsonProperty("Visuals.CheckpointScaleMultiplier")] public float CheckpointScaleMultiplier = 1.0f;
            [JsonProperty("Visuals.FinishScaleMultiplier")] public float FinishScaleMultiplier = 1.2f;
            [JsonProperty("Visuals.BoostScaleMultiplier")] public float BoostScaleMultiplier = 1.0f;
            [JsonProperty("Visuals.ScaleWithRadius")] public bool ScaleWithRadius = true;
            [JsonProperty("Visuals.RadiusToScaleFactor")] public float RadiusToScaleFactor = 0.08f;
            [JsonProperty("Visuals.DefaultCheckpointColor")] public string DefaultCheckpointColor = "0 0.8 1 0.8";
            [JsonProperty("Visuals.DefaultFinishColor")] public string DefaultFinishColor = "0 1 0 0.8";
            [JsonProperty("Visuals.DefaultBoostColor")] public string DefaultBoostColor = "1 0.8 0 0.8";

            [JsonProperty("Lobby.Position")] public Vector3 LobbyPosition = Vector3.zero;
            [JsonProperty("Lobby.RotationYaw")] public float LobbyRotationYaw = 0f;

            [JsonProperty("Boost.MarkerRespawnSeconds")] public float BoostMarkerRespawnSeconds = 10f;

            [JsonProperty("Physics.MaxUpwardVelocity")] public float MaxUpwardVelocity = 4f;
            [JsonProperty("Physics.MaxDownwardVelocity")] public float MaxDownwardVelocity = 35f;
            [JsonProperty("Physics.UprightStrength")] public float UprightStrength = 2.0f;
            [JsonProperty("Physics.AirborneUpwardDamping")] public float AirborneUpwardDamping = 0.6f;
            [JsonProperty("Physics.ExtraGravity")] public float ExtraGravity = 2.0f;

            [JsonProperty("Airtime.MaxAllowedSeconds")] public float MaxAllowedAirtimeSeconds = 3f;

            [JsonProperty("Wipeout.RespawnIfBoatDestroyed")] public bool RespawnIfBoatDestroyed = true;
            [JsonProperty("Wipeout.RespawnIfPlayerDrowned")] public bool RespawnIfPlayerDrowned = true;
            [JsonProperty("Wipeout.RespawnHeightBelowBoat")] public float RespawnHeightBelowBoat = 10f;

            [JsonProperty("AutoRace.Enabled")] public bool AutoRaceEnabled = true;
            [JsonProperty("AutoRace.SlotSize")] public int AutoRaceSlotSize = 10;
            [JsonProperty("AutoRace.QueueStartDelaySeconds")] public float AutoRaceQueueStartDelaySeconds = 15f;
            [JsonProperty("AutoRace.TrackSelectionMode")] public TrackSelectionMode AutoRaceTrackSelectionMode = TrackSelectionMode.CurrentSelected;
            [JsonProperty("AutoRace.TrackRotationList")] public List<string> AutoRaceRotationList = new List<string>();

            [JsonProperty("BattleRace.Enabled")] public bool BattleRaceEnabled = true;
            [JsonProperty("BattleRace.VoteDurationSeconds")] public int BattleRaceVoteDurationSeconds = 10;
            [JsonProperty("BattleRace.DefaultKit")] public string BattleRaceDefaultKit = "Standard";
            [JsonProperty("BattleRace.ClearKitItemsAfterRace")] public bool BattleRaceClearKitItemsAfterRace = true;
            [JsonProperty("BattleRace.EnableMountedWeaponUse")] public bool BattleRaceEnableMountedWeaponUse = true;

            [JsonProperty("BattleRace.Kits")] public List<KitDefinition> BattleRaceKits = new List<KitDefinition>
            {
                new KitDefinition
                {
                    Name = "Standard",
                    Items = new List<KitItem>
                    {
                        new KitItem{ ShortName="rifle.ak", Amount=1, SkinId=0 },
                        new KitItem{ ShortName="pistol.python", Amount=1, SkinId=0 },
                        new KitItem{ ShortName="ammo.rifle", Amount=120, SkinId=0 },
                        new KitItem{ ShortName="ammo.pistol", Amount=60, SkinId=0 },
                        new KitItem{ ShortName="medical.syringe", Amount=8, SkinId=0 }
                    }
                }
            };
        }

        private PluginConfig config;
        private Quaternion LobbyRotation => Quaternion.Euler(0f, (config?.LobbyRotationYaw ?? 0f), 0f);

        #endregion

        #region Data Structures

        [JsonObject(MemberSerialization.OptIn)]
        private class Checkpoint
        {
            [JsonProperty] public Vector3 Position;
            [JsonProperty] public float Radius;
            [JsonProperty] public string Color;
        }

        [JsonObject(MemberSerialization.OptIn)]
        private class BoostZone
        {
            [JsonProperty] public Vector3 Position;
            [JsonProperty] public float Radius;
            [JsonProperty] public float StrengthMultiplier;
            [JsonProperty] public float DurationSeconds;
            [JsonProperty] public string Color;
        }

        [JsonObject(MemberSerialization.OptIn)]
        private class TrackDefinition
        {
            [JsonProperty] public string Name;
            [JsonProperty] public Vector3 StartPosition;
            [JsonProperty] public float StartYaw;
            [JsonProperty] public float StartRadius = 12f;
            [JsonProperty] public Vector3 FinishPosition;
            [JsonProperty] public float FinishRadius = 12f;
            [JsonProperty] public string FinishColor;
            [JsonProperty] public List<Checkpoint> Checkpoints = new List<Checkpoint>();
            [JsonProperty] public List<BoostZone> BoostZones = new List<BoostZone>();
            [JsonProperty] public int Laps = 1;
            [JsonProperty] public string Designer = "Unknown";
            [JsonProperty] public string ImageKey = "";
            [JsonIgnore] public Quaternion StartRotation
            {
                get => Quaternion.Euler(0f, StartYaw, 0f);
                set => StartYaw = value.eulerAngles.y;
            }
        }

        [JsonObject(MemberSerialization.OptIn)]
        private class TrackStats
        {
            [JsonProperty] public float BestTimeSeconds = -1f;
            [JsonProperty] public int Wins;
            [JsonProperty] public int CompletedRaces;
        }

        [JsonObject(MemberSerialization.OptIn)]
        private class PlayerStats
        {
            [JsonProperty] public ulong PlayerId;
            [JsonProperty] public Dictionary<string, TrackStats> TrackStats =
                new Dictionary<string, TrackStats>(StringComparer.OrdinalIgnoreCase);
        }

        private class TrackBuilder
        {
            public string Name;
            public TrackDefinition Track = new TrackDefinition();
        }

        private enum RaceState { None, Staging, Voting, Countdown, Running, Finished }
        private enum RaceMode { Normal, Battle }

        private class RaceParticipant
        {
            public BasePlayer Player;
            public BaseBoat Boat;
            public int CurrentCheckpointIndex;
            public int CurrentLap;
            public float TimeElapsed;
            public bool Finished;
            public float FinishTime;
            public Dictionary<int, float> ActiveBoosts = new Dictionary<int, float>();
            public Dictionary<int, float> BoostCooldowns = new Dictionary<int, float>();
            public float AirborneTime;
            public List<ItemId> GivenBattleItemIds = new List<ItemId>();
        }

        private class RaceSession
        {
            public TrackDefinition Track;
            public RaceState State = RaceState.None;
            public float CountdownRemaining;
            public float TimeSinceStart;
            public Dictionary<ulong, RaceParticipant> Participants = new Dictionary<ulong, RaceParticipant>();
            public List<BaseBoat> SpawnedBoats = new List<BaseBoat>();
            public Timer CountdownTimer;
            public Timer VoteTimer;
            public RaceMode Mode = RaceMode.Normal;
            public Dictionary<ulong, RaceMode> Votes = new Dictionary<ulong, RaceMode>();
            public string BattleKitUsed = "";
        }

        private class TimeTrialSession
        {
            public TrackDefinition Track;
            public BasePlayer Player;
            public BaseBoat Boat;
            public int CurrentCheckpointIndex;
            public int CurrentLap;
            public float TimeElapsed;
            public bool Finished;
            public Dictionary<int, float> ActiveBoosts = new Dictionary<int, float>();
            public Dictionary<int, float> BoostCooldowns = new Dictionary<int, float>();
            public float AirborneTime;
        }

        private class MarkerInfo
        {
            public BaseEntity Entity;
            public TrackDefinition Track;
            public int CheckpointIndex = -1;
            public int BoostIndex = -1;
            public bool IsFinish;
        }

        private enum EditorSelectType { None, Start, Finish, Checkpoint, Boost }

        private class EditorState
        {
            public bool Active;
            public TrackDefinition SelectedTrack;
            public EditorSelectType SelectedType = EditorSelectType.None;
            public int SelectedCheckpointIndex = -1;
            public int SelectedBoostIndex = -1;
            public bool MovingMarker;
        }

        #endregion

        #region State

        private readonly Dictionary<string, TrackDefinition> tracks = new Dictionary<string, TrackDefinition>(StringComparer.OrdinalIgnoreCase);
        private readonly Dictionary<ulong, PlayerStats> playerStats = new Dictionary<ulong, PlayerStats>();

        private TrackBuilder currentTrackBuilder;
        private RaceSession currentRace;
        private readonly Dictionary<ulong, TimeTrialSession> activeTimeTrials = new Dictionary<ulong, TimeTrialSession>();

        private Timer raceTickTimer;
        private Timer timeTrialTickTimer;
        private Timer controlTickTimer;
        private Timer editorTickTimer;
        private Timer autoRaceMonitor;

        private readonly List<MarkerInfo> activeTrackMarkers = new List<MarkerInfo>();
        private readonly Dictionary<ulong, EditorState> editorStates = new Dictionary<ulong, EditorState>();
        private readonly Dictionary<string, HashSet<int>> disabledBoostMarkers = new Dictionary<string, HashSet<int>>(StringComparer.OrdinalIgnoreCase);

        private readonly LinkedList<ulong> raceQueue = new LinkedList<ulong>();
        private int rotationIndex = 0;

        #endregion

        #region Init / Unload

        [PluginReference] private Plugin HydroLobby;

        private void EnsureConfig()
        {
            if (config == null)
            {
                PrintWarning("Config null; creating defaults.");
                config = new PluginConfig();
            }
            if (config.AllowedBoatPrefabs == null || config.AllowedBoatPrefabs.Count == 0)
            {
                config.AllowedBoatPrefabs = new List<string>
                {
                    "assets/content/vehicles/boats/rowboat/rowboat.prefab",
                    "assets/content/vehicles/boats/rhib/rhib.prefab"
                };
            }
        }

        private void LoadConfigSafe()
        {
            try
            {
                if (!Config.Exists())
                {
                    LoadDefaultConfig();
                    SaveConfigSafe();
                }
                else
                {
                    config = Config.ReadObject<PluginConfig>();
                }
            }
            catch (Exception e)
            {
                PrintWarning($"LoadConfig failed: {e.Message}");
                config = null;
            }
            EnsureConfig();
            SaveConfigSafe();
        }

        protected override void LoadDefaultConfig()
        {
            config = new PluginConfig();
            PrintWarning("Created default HydroRust config.");
        }

        private void SaveConfigSafe()
        {
            try
            {
                if (config != null)
                    Config.WriteObject(config, true);
            }
            catch (Exception e)
            {
                PrintWarning($"SaveConfig failed: {e.Message}");
            }
        }

        private void Init()
        {
            permission.RegisterPermission(PERM_ADMIN, this);
            permission.RegisterPermission(PERM_PLAYER, this);
            LoadConfigSafe();
            LoadDataFiles();
            LoadRotationIndex();
        }

        private void OnServerInitialized()
        {
            EnsureConfig();
            StartTickTimers();
            StartAutoRaceMonitor();
            var players = BasePlayer.activePlayerList;
            if (players != null)
            {
                foreach (var player in players)
                {
                    if (player == null) continue;
                    EnsurePlayerPermission(player);
                    TryAddToQueueFromLobby(player);
                }
            }
        }

        private void Unload()
        {
            StopTickTimers();
            autoRaceMonitor?.Destroy();
            SaveDataFiles();
            SaveRotationIndex();
            SaveConfigSafe();
            DestroyTrackMarkers();
            currentRace = null;
            activeTimeTrials.Clear();
        }

        private void EnsurePlayerPermission(BasePlayer player)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PERM_PLAYER))
                permission.GrantUserPermission(player.UserIDString, PERM_PLAYER, null);
        }

        private void OnPlayerConnected(BasePlayer player)
        {
            EnsureConfig();
            EnsurePlayerPermission(player);
            timer.Once(2f, () => TryAddToQueueFromLobby(player));
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            editorStates.Remove(player.userID);
            currentRace?.Participants.Remove(player.userID);
            activeTimeTrials.Remove(player.userID);
            RemoveFromQueue(player.userID);
        }

        private void OnPlayerRespawn(BasePlayer player)
        {
            timer.Once(0.5f, () => TryAddToQueueFromLobby(player));
        }

        private void TryAddToQueueFromLobby(BasePlayer player)
        {
            EnsureConfig();
            if (config == null) return;
            if (player == null || !player.IsConnected) return;
            if (!config.AutoRaceEnabled) return;

            Vector3 lobbyPos = GetLobbyPosition();
            if (lobbyPos == Vector3.zero) return;

            if (Vector3.Distance(player.transform.position, lobbyPos) <= 20f)
                EnqueuePlayer(player.userID);
        }

        #endregion

        #region Data Load/Save

        private void LoadDataFiles()
        {
            try
            {
                var tracksData = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, TrackDefinition>>(DATA_TRACKS);
                if (tracksData != null)
                {
                    foreach (var kvp in tracksData)
                    {
                        var t = kvp.Value;
                        if (t == null) continue;
                        if (string.IsNullOrEmpty(t.FinishColor))
                            t.FinishColor = config?.DefaultFinishColor ?? "0 1 0 0.8";
                        for (int i = 0; i < t.Checkpoints.Count; i++)
                            if (string.IsNullOrEmpty(t.Checkpoints[i].Color))
                                t.Checkpoints[i].Color = config?.DefaultCheckpointColor ?? "0 0.8 1 0.8";
                        for (int i = 0; i < t.BoostZones.Count; i++)
                            if (string.IsNullOrEmpty(t.BoostZones[i].Color))
                                t.BoostZones[i].Color = config?.DefaultBoostColor ?? "1 0.8 0 0.8";
                        tracks[kvp.Key] = t;
                    }
                }
            }
            catch (Exception e) { PrintWarning($"Failed to load tracks data: {e.Message}"); }

            try
            {
                var statsData = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, PlayerStats>>(DATA_STATS);
                if (statsData != null)
                    foreach (var kvp in statsData)
                        if (kvp.Value != null)
                            playerStats[kvp.Key] = kvp.Value;
            }
            catch (Exception e) { PrintWarning($"Failed to load stats data: {e.Message}"); }
        }

        private void SaveDataFiles()
        {
            try
            {
                Interface.Oxide.DataFileSystem.WriteObject(DATA_TRACKS, new Dictionary<string, TrackDefinition>(tracks, StringComparer.OrdinalIgnoreCase), true);
                Interface.Oxide.DataFileSystem.WriteObject(DATA_STATS, new Dictionary<ulong, PlayerStats>(playerStats), true);
            }
            catch (Exception e)
            {
                PrintWarning($"SaveDataFiles error: {e.Message}");
            }
        }

        private void LoadRotationIndex()
        {
            try
            {
                rotationIndex = Interface.Oxide.DataFileSystem.ReadObject<int>(DATA_ROTATION);
            }
            catch { rotationIndex = 0; }
        }

        private void SaveRotationIndex()
        {
            try
            {
                Interface.Oxide.DataFileSystem.WriteObject(DATA_ROTATION, rotationIndex);
            }
            catch (Exception e)
            {
                PrintWarning($"SaveRotationIndex error: {e.Message}");
            }
        }

        private PlayerStats GetOrCreatePlayerStats(ulong id)
        {
            if (!playerStats.TryGetValue(id, out var ps))
            {
                ps = new PlayerStats { PlayerId = id };
                playerStats[id] = ps;
            }
            return ps;
        }

        private TrackStats GetOrCreateTrackStats(PlayerStats ps, string trackName)
        {
            if (ps == null) return null;
            if (!ps.TrackStats.TryGetValue(trackName, out var ts))
            {
                ts = new TrackStats();
                ps.TrackStats[trackName] = ts;
            }
            return ts;
        }

        private void UpdatePlayerStatsOnFinish(ulong id, string trackName, float time)
        {
            var ps = GetOrCreatePlayerStats(id);
            var ts = GetOrCreateTrackStats(ps, trackName);
            if (ts == null) return;
            if (ts.BestTimeSeconds < 0f || time < ts.BestTimeSeconds)
                ts.BestTimeSeconds = time;
            ts.CompletedRaces++;
            SaveDataFiles();
        }

        #endregion

        #region Timers

        private void StartTickTimers()
        {
            StopTickTimers();
            raceTickTimer = timer.Every(0.1f, RaceTick);
            timeTrialTickTimer = timer.Every(0.1f, TimeTrialTick);
            controlTickTimer = timer.Every(0.05f, ControlTick);
            editorTickTimer = timer.Every(0.05f, EditorTick);
        }

        private void StopTickTimers()
        {
            raceTickTimer?.Destroy();
            timeTrialTickTimer?.Destroy();
            controlTickTimer?.Destroy();
            editorTickTimer?.Destroy();
            raceTickTimer = timeTrialTickTimer = controlTickTimer = editorTickTimer = null;
        }

        private void StartAutoRaceMonitor()
        {
            autoRaceMonitor?.Destroy();
            autoRaceMonitor = timer.Every(3f, AutoRaceCheck);
        }

        #endregion

        #region Queue & Auto Race

        private void EnqueuePlayer(ulong id)
        {
            if (currentRace != null && currentRace.State != RaceState.Finished)
            {
                if (currentRace.Participants.ContainsKey(id)) return;
            }
            if (raceQueue.Contains(id)) return;
            raceQueue.AddLast(id);
        }

        private void RemoveFromQueue(ulong id)
        {
            var node = raceQueue.Find(id);
            if (node != null) raceQueue.Remove(node);
        }

        private TrackDefinition ResolveAutoRaceTrack()
        {
            EnsureConfig();
            if (tracks.Count == 0) return null;
            var mode = config.AutoRaceTrackSelectionMode;

            switch (mode)
            {
                case TrackSelectionMode.CurrentSelected:
                    var selected = editorStates.Values.FirstOrDefault(s => s.SelectedTrack != null)?.SelectedTrack;
                    return selected ?? tracks.Values.First();

                case TrackSelectionMode.Random:
                    return tracks.Values.ElementAt(UnityEngine.Random.Range(0, tracks.Count));

                case TrackSelectionMode.Rotation:
                    if (config.AutoRaceRotationList == null || config.AutoRaceRotationList.Count == 0)
                        return tracks.Values.ElementAt(rotationIndex % tracks.Count);
                    var valid = config.AutoRaceRotationList.Where(n => tracks.ContainsKey(n)).ToList();
                    if (valid.Count == 0) return tracks.Values.First();
                    if (rotationIndex >= valid.Count) rotationIndex = 0;
                    var name = valid[rotationIndex];
                    rotationIndex = (rotationIndex + 1) % valid.Count;
                    SaveRotationIndex();
                    return tracks[name];
            }
            return tracks.Values.First();
        }

        private void AutoRaceCheck()
        {
            EnsureConfig();
            if (config == null) return;
            if (!config.AutoRaceEnabled) return;
            if (currentRace != null && currentRace.State != RaceState.Finished) return;

            int slotSize = Mathf.Max(1, config.AutoRaceSlotSize);
            if (raceQueue.Count == 0) return;

            if (raceQueue.Count >= slotSize)
            {
                BeginAutoStage(slotSize);
            }
            else
            {
                if (config.AutoRaceQueueStartDelaySeconds > 0f)
                {
                    if (_pendingDelayedStart == null)
                    {
                        _pendingDelayedStart = timer.Once(config.AutoRaceQueueStartDelaySeconds, () =>
                        {
                            _pendingDelayedStart = null;
                            if (currentRace == null || currentRace.State == RaceState.Finished)
                            {
                                if (raceQueue.Count > 0 && raceQueue.Count < slotSize)
                                    BeginAutoStage(Math.Min(slotSize, raceQueue.Count));
                            }
                        });
                    }
                }
            }
        }

        private Timer _pendingDelayedStart;

        private void BeginAutoStage(int takeCount)
        {
            var track = ResolveAutoRaceTrack();
            if (track == null)
            {
                PrintWarning("AutoRace: No track available.");
                return;
            }

            currentRace = new RaceSession
            {
                Track = track,
                State = RaceState.Staging,
                CountdownRemaining = config?.RaceCountdownSeconds ?? 10
            };

            int added = 0;
            foreach (var id in raceQueue.ToList())
            {
                var player = BasePlayer.FindByID(id);
                if (player != null && player.IsConnected)
                {
                    currentRace.Participants[id] = new RaceParticipant { Player = player };
                    added++;
                    raceQueue.Remove(id);
                    if (added >= takeCount) break;
                }
                else raceQueue.Remove(id);
            }

            BroadcastToRace($"Auto race staged on {track.Name}. Voting for mode begins!");
            if ((config?.VisualsEnabled ?? false))
            {
                DestroyTrackMarkers();
                SpawnTrackMarkers(track);
            }

            StartBattleVote();
        }

        private void StartBattleVote()
        {
            EnsureConfig();
            if (currentRace == null) return;

            if (!(config?.BattleRaceEnabled ?? false))
            {
                currentRace.Mode = RaceMode.Normal;
                StartCountdownPhase();
                return;
            }

            currentRace.State = RaceState.Voting;
            currentRace.Votes.Clear();
            BroadcastToRace($"Vote race mode: /hydro vote normal OR /hydro vote battle ({config?.BattleRaceVoteDurationSeconds ?? 10}s).");

            currentRace.VoteTimer?.Destroy();
            currentRace.VoteTimer = timer.Once(config?.BattleRaceVoteDurationSeconds ?? 10, FinishVoteAndProceed);
        }

        private void FinishVoteAndProceed()
        {
            if (currentRace == null || currentRace.State != RaceState.Voting) return;
            int battle = currentRace.Votes.Values.Count(v => v == RaceMode.Battle);
            int normal = currentRace.Votes.Values.Count(v => v == RaceMode.Normal);

            currentRace.Mode = (battle > normal) ? RaceMode.Battle : RaceMode.Normal;
            BroadcastToRace($"Race mode chosen: {currentRace.Mode}");
            StartCountdownPhase();
        }

        private void StartCountdownPhase()
        {
            if (currentRace == null) return;
            currentRace.State = RaceState.Countdown;
            currentRace.CountdownRemaining = config?.RaceCountdownSeconds ?? 10;

            BroadcastToRace($"Race ({currentRace.Mode}) starting in {currentRace.CountdownRemaining} seconds...");
            currentRace.CountdownTimer?.Destroy();
            currentRace.CountdownTimer = timer.Every(1f, () =>
            {
                if (currentRace == null || currentRace.State != RaceState.Countdown) return;
                currentRace.CountdownRemaining--;
                if (currentRace.CountdownRemaining <= 0)
                {
                    currentRace.CountdownTimer?.Destroy();
                    currentRace.CountdownTimer = null;
                    BeginRace();
                }
                else BroadcastToRace($"{currentRace.Mode} race starting in {currentRace.CountdownRemaining}...");
            });
        }

        #endregion

        #region Airtime / Wipeout

        private void HandleAirtimeAndWipeout(TrackDefinition track, RaceParticipant part, BaseBoat boat, float delta)
        {
            if (part == null || part.Player == null) return;

            if (boat == null || boat.IsDestroyed)
            {
                if (config?.RespawnIfBoatDestroyed ?? true)
                    RespawnParticipantAtLastCheckpoint(track, part);
                return;
            }

            bool airborne = IsBoatAirborne(boat);
            if (airborne)
            {
                part.AirborneTime += delta;
                if (part.AirborneTime > (config?.MaxAllowedAirtimeSeconds ?? 3f))
                {
                    RespawnParticipantAtLastCheckpoint(track, part);
                    part.AirborneTime = 0f;
                }
            }
            else part.AirborneTime = 0f;

            if ((config?.RespawnIfPlayerDrowned ?? true) && part.Player.GetMounted() == null)
            {
                float diff = boat.transform.position.y - part.Player.transform.position.y;
                if (diff > (config?.RespawnHeightBelowBoat ?? 10f))
                    RespawnParticipantAtLastCheckpoint(track, part);
            }
        }

        private void HandleAirtimeAndWipeout(TrackDefinition track, TimeTrialSession session, BaseBoat boat, float delta)
        {
            if (session == null || session.Player == null) return;

            if (boat == null || boat.IsDestroyed)
            {
                if (config?.RespawnIfBoatDestroyed ?? true)
                    RespawnTimeTrialAtLastCheckpoint(track, session);
                return;
            }

            bool airborne = IsBoatAirborne(boat);
            if (airborne)
            {
                session.AirborneTime += delta;
                if (session.AirborneTime > (config?.MaxAllowedAirtimeSeconds ?? 3f))
                {
                    RespawnTimeTrialAtLastCheckpoint(track, session);
                    session.AirborneTime = 0f;
                }
            }
            else session.AirborneTime = 0f;

            if ((config?.RespawnIfPlayerDrowned ?? true) && session.Player.GetMounted() == null)
            {
                float diff = boat.transform.position.y - session.Player.transform.position.y;
                if (diff > (config?.RespawnHeightBelowBoat ?? 10f))
                    RespawnTimeTrialAtLastCheckpoint(track, session);
            }
        }

        private Vector3 GetCheckpointPosition(TrackDefinition track, int checkpointIndex)
        {
            if (track == null) return Vector3.zero;
            if (checkpointIndex <= 0 || track.Checkpoints.Count == 0) return track.StartPosition;
            int idx = Math.Min(checkpointIndex - 1, track.Checkpoints.Count - 1);
            return track.Checkpoints[idx].Position;
        }

        private Quaternion GetCheckpointRotation(TrackDefinition track, int checkpointIndex)
        {
            if (track == null) return Quaternion.identity;
            if (checkpointIndex <= 0 || track.Checkpoints.Count == 0) return track.StartRotation;
            int idx = Math.Min(checkpointIndex - 1, track.Checkpoints.Count - 1);
            Vector3 from = track.Checkpoints[idx].Position;
            Vector3 to = (idx + 1 < track.Checkpoints.Count) ? track.Checkpoints[idx + 1].Position : track.FinishPosition;
            Vector3 dir = to - from; dir.y = 0f;
            if (dir.sqrMagnitude < 0.01f) return track.StartRotation;
            return Quaternion.LookRotation(dir.normalized, Vector3.up);
        }

        private void RespawnParticipantAtLastCheckpoint(TrackDefinition track, RaceParticipant part)
        {
            if (track == null || part == null || part.Player == null) return;
            Vector3 cpPos = GetCheckpointPosition(track, part.CurrentCheckpointIndex);
            Quaternion cpRot = GetCheckpointRotation(track, part.CurrentCheckpointIndex);
            part.Boat?.Kill();
            var boat = SpawnBoatForPlayer(part.Player, cpPos, cpRot);
            StabilizeBoat(boat);
            part.Boat = boat;
            part.AirborneTime = 0f;
            part.Player.ChatMessage("<color=#ff8080>Respawned.</color>");
        }

        private void RespawnTimeTrialAtLastCheckpoint(TrackDefinition track, TimeTrialSession session)
        {
            if (track == null || session == null || session.Player == null) return;
            Vector3 cpPos = GetCheckpointPosition(track, session.CurrentCheckpointIndex);
            Quaternion cpRot = GetCheckpointRotation(track, session.CurrentCheckpointIndex);
            session.Boat?.Kill();
            var boat = SpawnBoatForPlayer(session.Player, cpPos, cpRot);
            StabilizeBoat(boat);
            session.Boat = boat;
            session.AirborneTime = 0f;
            session.Player.ChatMessage("<color=#ff8080>Respawned.</color>");
        }

        #endregion

        #region Boost & Controls

        private void TickBoosts(TrackDefinition track, BaseBoat boat,
            Dictionary<int, float> activeBoosts, Dictionary<int, float> boostCooldowns,
            float delta, BasePlayer player)
        {
            if (boat == null || boat.IsDestroyed || track == null) return;

            foreach (var idx in activeBoosts.Keys.ToList())
            {
                activeBoosts[idx] -= delta;
                if (activeBoosts[idx] <= 0f) activeBoosts.Remove(idx);
            }

            foreach (var idx in boostCooldowns.Keys.ToList())
            {
                boostCooldowns[idx] -= delta;
                if (boostCooldowns[idx] <= 0f) boostCooldowns.Remove(idx);
            }

            var rb = GetBoatRigidbody(boat);
            if (rb == null) return;

            Vector3 boatPos = boat.transform.position;

            for (int i = 0; i < track.BoostZones.Count; i++)
            {
                var zone = track.BoostZones[i];
                if (Vector3.Distance(boatPos, zone.Position) <= zone.Radius)
                {
                    if (boostCooldowns.ContainsKey(i) || activeBoosts.ContainsKey(i)) continue;

                    float duration = zone.DurationSeconds > 0 ? zone.DurationSeconds : (config?.BoostDurationSeconds ?? 1.5f);
                    float strength = zone.StrengthMultiplier > 0 ? zone.StrengthMultiplier : 1f;

                    activeBoosts[i] = duration;
                    boostCooldowns[i] = (config?.BoostCooldownSeconds ?? 4f);

                    Vector3 forward = boat.transform.forward.normalized;
                    rb.AddForce(forward * (config?.BoostForce ?? 20f) * strength, ForceMode.VelocityChange);

                    float maxSpeed = GetBaseBoatSpeed(boat) * (config?.MaxSpeedMultiplier ?? 2f) * strength;
                    if (rb.velocity.magnitude > maxSpeed)
                        rb.velocity = rb.velocity.normalized * maxSpeed;

                    player?.ChatMessage("<color=#00ffff>BOOST!</color>");
                    DisableBoostMarkerTemporarily(track, i, config?.BoostMarkerRespawnSeconds ?? 10f);
                }
            }

            if (rb.velocity.sqrMagnitude > 0.01f)
            {
                float baseMax = GetBaseBoatSpeed(boat) * (config?.MaxSpeedMultiplier ?? 2f);
                if (rb.velocity.magnitude > baseMax)
                    rb.velocity = rb.velocity.normalized * baseMax;
            }
        }

        private Rigidbody GetBoatRigidbody(BaseBoat boat)
        {
            return boat?.GetComponent<Rigidbody>() ?? boat?.GetComponentInChildren<Rigidbody>();
        }

        private float GetBaseBoatSpeed(BaseBoat boat)
        {
            if (boat == null) return 10f;
            string prefab = boat.ShortPrefabName?.ToLowerInvariant() ?? "";
            if (prefab.Contains("rowboat")) return 8f;
            if (prefab.Contains("rhib")) return 14f;
            return 10f;
        }

        private bool IsBoatAirborne(BaseBoat boat)
        {
            if (boat == null) return false;
            return !Physics.Raycast(boat.transform.position + Vector3.up * 0.5f, Vector3.down, out _, 3f,
                LayerMask.GetMask("Terrain", "World", "Water"));
        }

        private void ApplyArcadeControls(BasePlayer player, BaseBoat boat, float deltaTime)
        {
            if (player?.serverInput == null || boat == null || boat.IsDestroyed) return;
            if (!(config?.OverrideBoatControls ?? true)) return;

            var rb = GetBoatRigidbody(boat);
            if (rb == null) return;

            var input = player.serverInput;
            bool forward = input.IsDown(BUTTON.FORWARD);
            bool backward = input.IsDown(BUTTON.BACKWARD);
            bool left = input.IsDown(BUTTON.LEFT);
            bool right = input.IsDown(BUTTON.RIGHT);
            bool boostKey = (config?.ControlsBoostKeySprint ?? true) ? input.IsDown(BUTTON.SPRINT) : input.IsDown(BUTTON.RELOAD);

            Vector3 forwardDir = boat.transform.forward;
            rb.velocity *= (1f - (config?.ControlsLinearDrag ?? 0.15f) * deltaTime);

            float baseSpeed = GetBaseBoatSpeed(boat);
            float maxSpeed = baseSpeed * (config?.MaxSpeedMultiplier ?? 2f);

            if (forward) rb.AddForce(forwardDir * (config?.ControlsForwardForce ?? 6f), ForceMode.Acceleration);
            if (backward) rb.AddForce(-forwardDir * (config?.ControlsReverseForce ?? 4f), ForceMode.Acceleration);

            float steer = 0f;
            if (left) steer -= 1f;
            if (right) steer += 1f;
            if (Mathf.Abs(steer) > 0.01f)
                rb.AddTorque(Vector3.up * steer * (config?.ControlsSteerTorque ?? 3f), ForceMode.Acceleration);

            if (boostKey)
            {
                float boostMax = baseSpeed * (config?.ControlsBoostMaxSpeedMultiplier ?? 2.5f);
                rb.AddForce(forwardDir * (config?.ControlsBoostForce ?? 12f), ForceMode.Acceleration);
                if (rb.velocity.magnitude > boostMax)
                    rb.velocity = rb.velocity.normalized * boostMax;
            }
            else if (rb.velocity.magnitude > maxSpeed)
                rb.velocity = rb.velocity.normalized * maxSpeed;

            var vel = rb.velocity;
            Vector3 up = boat.transform.up;
            float tilt = Vector3.Angle(up, Vector3.up);

            if (tilt > 10f)
            {
                Vector3 axis = Vector3.Cross(up, Vector3.up);
                float strength = Mathf.Min(1.0f, (config?.UprightStrength ?? 2f)) * (tilt / 90f);
                rb.AddTorque(axis.normalized * (-strength), ForceMode.Acceleration);
                if (tilt > 25f) rb.AddForce(Vector3.down * (config?.ExtraGravity ?? 2f), ForceMode.Acceleration);
            }

            if (IsBoatAirborne(boat) && vel.y > 0f)
                vel.y *= Mathf.Clamp01(1f - (config?.AirborneUpwardDamping ?? 0.6f));

            vel.y = Mathf.Clamp(vel.y, -(config?.MaxDownwardVelocity ?? 35f), (config?.MaxUpwardVelocity ?? 4f));
            rb.velocity = vel;
        }

        #endregion

        #region Track Logic

        private void HandleCheckpointLogic(TrackDefinition track, RaceParticipant part)
        {
            if (part?.Boat == null || track == null || track.Checkpoints.Count == 0) return;

            int cpIndex = part.CurrentCheckpointIndex;
            if (cpIndex >= track.Checkpoints.Count) return;

            var cp = track.Checkpoints[cpIndex];
            if (Vector3.Distance(part.Boat.transform.position, cp.Position) <= cp.Radius)
            {
                part.CurrentCheckpointIndex++;
                if (part.CurrentCheckpointIndex >= track.Checkpoints.Count)
                {
                    part.CurrentCheckpointIndex = 0;
                    part.CurrentLap++;
                    part.Player?.ChatMessage($"<color=#ffff00>Lap {part.CurrentLap}/{track.Laps}</color>");
                }
                else part.Player?.ChatMessage($"Checkpoint {part.CurrentCheckpointIndex}/{track.Checkpoints.Count}");
            }
        }

        private void HandleCheckpointLogic(TrackDefinition track, TimeTrialSession session)
        {
            if (session?.Boat == null || track == null || track.Checkpoints.Count == 0) return;

            int cpIndex = session.CurrentCheckpointIndex;
            if (cpIndex >= track.Checkpoints.Count) return;

            var cp = track.Checkpoints[cpIndex];
            if (Vector3.Distance(session.Boat.transform.position, cp.Position) <= cp.Radius)
            {
                session.CurrentCheckpointIndex++;
                if (session.CurrentCheckpointIndex >= track.Checkpoints.Count)
                {
                    session.CurrentCheckpointIndex = 0;
                    session.CurrentLap++;
                    session.Player?.ChatMessage($"<color=#ffff00>Lap {session.CurrentLap}/{track.Laps}</color>");
                }
                else session.Player?.ChatMessage($"Checkpoint {session.CurrentCheckpointIndex}/{track.Checkpoints.Count}");
            }
        }

        private bool HasFinishedTrack(TrackDefinition track, RaceParticipant part)
        {
            if (part?.Boat == null || track == null || part.CurrentLap < track.Laps) return false;
            return Vector3.Distance(part.Boat.transform.position, track.FinishPosition) <= track.FinishRadius;
        }

        private bool HasFinishedTrack(TrackDefinition track, TimeTrialSession session)
        {
            if (session?.Boat == null || track == null || session.CurrentLap < track.Laps) return false;
            return Vector3.Distance(session.Boat.transform.position, track.FinishPosition) <= track.FinishRadius;
        }

        #endregion

        #region Race Flow

        private void BeginRace()
        {
            if (currentRace == null) return;

            int count = Math.Min(currentRace.Participants.Count, config?.AutoRaceSlotSize ?? 10);
            int index = 0;

            foreach (var kvp in currentRace.Participants.ToList())
            {
                var part = kvp.Value;
                var player = part.Player;
                if (player == null || !player.IsConnected) continue;

                Vector3 startPos = GetStartSlotPosition(currentRace.Track, index, count);
                Quaternion startRot = currentRace.Track.StartRotation;
                TeleportTo(startPos, startRot, player);

                if (config?.SpawnNewRaceBoats ?? true)
                {
                    var boat = SpawnBoatForPlayer(player, startPos, startRot);
                    if (boat != null)
                    {
                        part.Boat = boat;
                        currentRace.SpawnedBoats.Add(boat);
                        EnableMountedCombatIfBattle(boat);
                    }
                }
                else
                {
                    part.Boat = GetPlayerBoat(player);
                    MountPlayerInBoatDriverSeat(player, part.Boat);
                    EnableMountedCombatIfBattle(part.Boat);
                }

                index++;
                if (index >= count) break;
            }

            DistributeBattleKitsIfNeeded();
            currentRace.State = RaceState.Running;
            BroadcastToRace(currentRace.Mode == RaceMode.Battle
                ? "<color=#ff4444>BATTLE GO!</color>"
                : "<color=#00ff00>GO!</color>");
        }

        private void EnableMountedCombatIfBattle(BaseBoat boat)
        {
            if (currentRace == null || currentRace.Mode != RaceMode.Battle) return;
            if (!(config?.BattleRaceEnableMountedWeaponUse ?? true) || boat == null) return;

            var mounts = boat.GetComponentsInChildren<BaseMountable>();
            foreach (var m in mounts)
                TryEnableItemsOnMount(m);
        }

        private void TryEnableItemsOnMount(BaseMountable m)
        {
            if (m == null) return;
            SetBoolFieldOrProperty(m, "disableItemUsage", false);
            SetBoolFieldOrProperty(m, "canWieldItems", true);
            SetBoolFieldOrProperty(m, "canHideHeldEntity", false);
        }

        private void SetBoolFieldOrProperty(object obj, string name, bool value)
        {
            if (obj == null) return;
            var type = obj.GetType();

            var fi = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
            if (fi != null && fi.FieldType == typeof(bool))
            {
                try { fi.SetValue(obj, value); } catch { }
                return;
            }

            var pi = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
            if (pi != null && pi.CanWrite && pi.PropertyType == typeof(bool))
            {
                try { pi.SetValue(obj, value, null); } catch { }
            }
        }

        private void DistributeBattleKitsIfNeeded()
        {
            if (currentRace == null || currentRace.Mode != RaceMode.Battle) return;
            var kits = config?.BattleRaceKits;
            if (kits == null || kits.Count == 0) return;
            var kit = kits.FirstOrDefault(k => k.Name.Equals(config.BattleRaceDefaultKit, StringComparison.OrdinalIgnoreCase))
                      ?? kits.FirstOrDefault();
            if (kit == null) return;
            currentRace.BattleKitUsed = kit.Name;

            foreach (var part in currentRace.Participants.Values)
            {
                var player = part.Player;
                if (player == null || !player.IsConnected) continue;
                foreach (var itemDef in kit.Items)
                {
                    var item = ItemManager.CreateByName(itemDef.ShortName, itemDef.Amount, itemDef.SkinId);
                    if (item == null) continue;
                    if (!player.inventory.GiveItem(item))
                    {
                        item.Remove();
                        
// ... [Code preview truncated due to length. Full source in attached file] ...

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