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

FREE DriveBySedanGangs : 2.9.0 2.9.0

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

namespace Oxide.Plugins
{
    [Info("DriveBySedanGangs", "belisario-afk + Gemini + Copilot", "2.9.0")]
    [Description("Spawn sedan gangs via command; sedans stalk players with 3 gang scientists that shoot from the car and on foot, then despawn when too far or dead.")]
    public class DriveBySedanGangs : RustPlugin
    {
        #region Plugin References

        // HoodWars plugin reference for gang territory integration
        [PluginReference]
        private Plugin HoodWars;

        #endregion

        #region HoodWars Data Types (for reading HoodWars_CoreData.json directly)

        // These mirror the data structures in HoodWars.cs
        private class HoodWarsStoredData
        {
            public Dictionary<ulong, HoodWarsPlayerInfo> Players = new Dictionary<ulong, HoodWarsPlayerInfo>();
        }

        private class HoodWarsPlayerInfo
        {
            public int HomeHood = 4; // 0=West, 1=North, 2=South, 3=East, 4=Neutral
        }

        // Gang names corresponding to HomeHood enum values
        private static readonly string[] GangNames = { "Westside Pirus", "Northside Vagos", "Southside Sureños", "Eastside Disciples", "Neutral" };

        #endregion

        #region Data Types

        private class GangVisuals
        {
            public List<string> Clothing;
            public Dictionary<string, ulong> Skins;
            public string Weapon = "pistol.semiauto";
            public ulong WeaponSkin = 0;
        }

        private class DriveByState
        {
            public ulong TargetID;
            public List<ScientistNPC> Shooters = new List<ScientistNPC>();
            public float LastShootTime;
            public int LastShooterIndex = -1;
            public string GangName;
        }

        #endregion

        #region Fields / Constants

        private readonly Dictionary<string, GangVisuals> _gangKits = new Dictionary<string, GangVisuals>();
        private readonly Dictionary<string, string> _borderSpawns = new Dictionary<string, string>();

        // Combat-oriented scientist prefab
        private const string PrefabScientist =
            "assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_cargo.prefab";

        private readonly HashSet<ulong> _driveByNPCs = new HashSet<ulong>();

        private const string DefaultGangName = "Westside Pirus";
        private const string NeutralTerritory = "Neutral";
        private const string NeutralGround = "Neutral Ground";

        // Debug mode - set to true in config to see debug messages in console
        private bool _debugMode = false;

        private const string SedanPrefab = "assets/content/vehicles/sedan_a/sedantest.entity.prefab";

        private const int DefaultSedansPerPlayer = 1;
        private const float SpawnRadius = 35f;
        private const float FollowUpdateInterval = 0.1f;
        private const float MaxSpeed = 11f;
        private const float Acceleration = 45f;
        private const float BrakeForce = 50f;
        private const float TurnTorque = 14f;
        private const float MaxSteerAngleDeg = 55f;
        private const float MinDistanceToPlayer = 8f;
        private const float TeleportDistance = 300f;    // if car > this, we retire it
        private const float SpawnHeightCheck = 30f;
        private const float SpawnAboveGround = 1.0f;
        private const int GroundLayerMask = -1;

        private const float AttackDistance = 18f;       // car stops & deploys here
        private const float DeployDelaySeconds = 1f;  // slight delay before deploy

        private const float ScientistHealth = 50f;
        private const float ScientistMoveSpeed = 4.5f;

        // Manual drive-by shooting tuning
        private const float MinShootDistance = 10f;
        private const float MaxShootDistance = 60f;
        private const float ShootInterval = 0.4f;

        // playerID -> list of sedans
        private readonly Dictionary<ulong, List<BaseEntity>> _playerSedans =
            new Dictionary<ulong, List<BaseEntity>>();

        // sedan -> scientists owned by that sedan
        private readonly Dictionary<BaseEntity, List<ScientistNPC>> _sedanScientists =
            new Dictionary<BaseEntity, List<ScientistNPC>>();

        // scientist -> seat they are mounted in (for proper, player-like dismount)
        private readonly Dictionary<ScientistNPC, BaseMountable> _scientistSeats =
            new Dictionary<ScientistNPC, BaseMountable>();

        // sedan -> fully deployed (scientists have been dismounted)
        private readonly HashSet<BaseEntity> _deployedSedans =
            new HashSet<BaseEntity>();

        // sedan -> already scheduled deploy timer
        private readonly HashSet<BaseEntity> _deployScheduled =
            new HashSet<BaseEntity>();

        // sedan -> manual shooting state
        private readonly Dictionary<BaseEntity, DriveByState> _driveByStates =
            new Dictionary<BaseEntity, DriveByState>();

        // sedan -> flagged for safe retire (so timers/logic ignore it)
        private readonly HashSet<BaseEntity> _retiringSedans =
            new HashSet<BaseEntity>();

        // Territory tracking for automatic drive-by spawning
        private readonly Dictionary<ulong, string> _playerLastTerritory =
            new Dictionary<ulong, string>();
        
        // Cooldown tracking for territory-based spawns (prevents spam)
        private readonly Dictionary<ulong, float> _playerTerritorySpawnCooldown =
            new Dictionary<ulong, float>();
        
        private const float TerritoryCheckInterval = 2f;  // Check player territories every 2 seconds
        private const float TerritorySpawnCooldown = 300f; // 5 minute cooldown between territory spawns

        #endregion

        #region Config & Gang Loading

        protected override void LoadDefaultConfig()
        {
            Config["Visuals"] = new Dictionary<string, object>
            {
                ["Westside Pirus"] = new Dictionary<string, object>
                {
                    ["Clothing"] = new List<string> { "hoodie", "pants", "mask.balaclava" },
                    ["Skins"] = new Dictionary<string, object>
                    {
                        ["hoodie"] = 3637124708,
                        ["pants"] = 3637161289,
                        ["mask.balaclava"] = 3637136628
                    },
                    ["Weapon"] = "pistol.semiauto",
                    ["WeaponSkin"] = 3639125341
                },
                ["Northside Vagos"] = new Dictionary<string, object>
                {
                    ["Clothing"] = new List<string> { "hoodie", "pants", "mask.bandana" },
                    ["Skins"] = new Dictionary<string, object>
                    {
                        ["hoodie"] = 3637132959,
                        ["pants"] = 3637162032,
                        ["mask.bandana"] = 3637144551
                    },
                    ["Weapon"] = "pistol.semiauto",
                    ["WeaponSkin"] = 3639147460
                },
                ["Southside Sureños"] = new Dictionary<string, object>
                {
                    ["Clothing"] = new List<string> { "hoodie", "pants", "mask.balaclava" },
                    ["Skins"] = new Dictionary<string, object>
                    {
                        ["hoodie"] = 3637133781,
                        ["pants"] = 3637162360,
                        ["mask.balaclava"] = 3637136303
                    },
                    ["Weapon"] = "pistol.semiauto",
                    ["WeaponSkin"] = 3639138385
                },
                ["Eastside Disciples"] = new Dictionary<string, object>
                {
                    ["Clothing"] = new List<string> { "hoodie", "pants", "mask.bandana" },
                    ["Skins"] = new Dictionary<string, object>
                    {
                        ["hoodie"] = 3637126631,
                        ["pants"] = 3637163268,
                        ["mask.bandana"] = 3637149926
                    },
                    ["Weapon"] = "pistol.semiauto",
                    ["WeaponSkin"] = 3639144230
                }
            };

            Config["Border Spawns"] = new Dictionary<string, object>
            {
                ["Westside Pirus"] = "west",
                ["Northside Vagos"] = "north",
                ["Southside Sureños"] = "south",
                ["Eastside Disciples"] = "east"
            };

            Config["DebugMode"] = false;

            SaveConfig();
        }

        private void LoadGangConfig()
        {
            _gangKits.Clear();

            var visualData = Config["Visuals"] as Dictionary<string, object>;
            if (visualData != null)
            {
                foreach (var kvp in visualData)
                {
                    var data = kvp.Value as Dictionary<string, object>;
                    if (data == null) continue;

                    var clothingList = data["Clothing"] as List<object>;
                    var skinDict = data["Skins"] as Dictionary<string, object>;

                    if (clothingList == null || skinDict == null)
                        continue;

                    // Read weapon config with defaults
                    string weapon = "pistol.semiauto";
                    ulong weaponSkin = 0;
                    
                    object weaponValue;
                    if (data.TryGetValue("Weapon", out weaponValue) && weaponValue != null)
                        weapon = weaponValue.ToString();
                    
                    object weaponSkinValue;
                    if (data.TryGetValue("WeaponSkin", out weaponSkinValue) && weaponSkinValue != null)
                        ulong.TryParse(weaponSkinValue.ToString(), out weaponSkin);

                    _gangKits[kvp.Key] = new GangVisuals
                    {
                        Clothing = clothingList.Select(x => x.ToString()).ToList(),
                        Skins = skinDict.ToDictionary(
                            x => x.Key,
                            x => ulong.Parse(x.Value.ToString())
                        ),
                        Weapon = weapon,
                        WeaponSkin = weaponSkin
                    };
                    
                    Puts($"[DriveBySedanGangs] Loaded gang kit: {kvp.Key} - {clothingList.Count} clothing items, weapon: {weapon}");
                }
            }
            else
            {
                Puts("[DriveBySedanGangs] WARNING: No 'Visuals' config found. Gang kits will not be loaded.");
            }
            
            Puts($"[DriveBySedanGangs] Loaded {_gangKits.Count} gang kits.");

            _borderSpawns.Clear();
            var borderCfg = Config["Border Spawns"] as Dictionary<string, object>;
            if (borderCfg != null)
            {
                foreach (var kvp in borderCfg)
                    _borderSpawns[kvp.Key] = kvp.Value.ToString().ToLower();
            }
        }

        #endregion

        #region Helpers

        /// <summary>
        /// Helper method to log debug messages only when debug mode is enabled
        /// </summary>
        private void DebugLog(string message)
        {
            if (_debugMode)
            {
                Puts($"[DriveBySedanGangs] DEBUG: {message}");
            }
        }

        private bool FindGroundPosition(Vector3 desired, out Vector3 groundPos)
        {
            groundPos = desired + Vector3.up * SpawnAboveGround;

            Vector3 rayStart = desired + Vector3.up * SpawnHeightCheck;
            RaycastHit hit;
            if (Physics.Raycast(
                    rayStart,
                    Vector3.down,
                    out hit,
                    SpawnHeightCheck * 2f,
                    GroundLayerMask,
                    QueryTriggerInteraction.Ignore))
            {
                groundPos = hit.point + Vector3.up * SpawnAboveGround;
                return true;
            }

            return false;
        }

        /// <summary>
        /// Safely retire a sedan: stop plugin logic, dismount/kill NPCs, then call Kill() once.
        /// </summary>
        private void RetireSedan(BaseEntity car)
        {
            if (car == null) return;
            if (_retiringSedans.Contains(car)) return;
            _retiringSedans.Add(car);

            NextFrame(() =>
            {
                if (car == null || car.IsDestroyed) return;

                if (_sedanScientists.TryGetValue(car, out var sciList) && sciList != null)
                {
                    foreach (var npc in sciList.ToArray())
                    {
                        if (npc == null) continue;

                        if (npc.isMounted)
                        {
                            BaseMountable seat;
                            if (_scientistSeats.TryGetValue(npc, out seat) && seat != null && !seat.IsDestroyed)
                            {
                                seat.DismountAllPlayers();
                            }
                            else
                            {
                                var mountable = npc.GetMounted() as BaseMountable;
                                if (mountable != null && !mountable.IsDestroyed)
                                    mountable.DismountAllPlayers();
                            }
                        }

                        if (!npc.IsDestroyed)
                            npc.Kill();

                        _scientistSeats.Remove(npc);
                    }

                    _sedanScientists.Remove(car);
                }

                _deployedSedans.Remove(car);
                _deployScheduled.Remove(car);
                _driveByStates.Remove(car);
                _retiringSedans.Remove(car);

                if (!car.IsDestroyed)
                    car.Kill();
            });
        }

        /// <summary>
        /// Check if a player is wearing clothing with skin IDs that match a specific gang's kit.
        /// Returns true if the player has any item with a skin ID matching the gang's clothing skins.
        /// </summary>
        private bool IsPlayerWearingGangClothing(BasePlayer player, string gangName)
        {
            if (player == null || string.IsNullOrEmpty(gangName))
                return false;

            if (!_gangKits.TryGetValue(gangName, out var kit) || kit.Skins == null)
                return false;

            // Get all skin IDs used by this gang
            var gangSkinIds = new HashSet<ulong>(kit.Skins.Values);
            if (gangSkinIds.Count == 0)
                return false;

            // Check player's worn items for matching skin IDs
            if (player.inventory?.containerWear?.itemList == null)
                return false;

            foreach (var item in player.inventory.containerWear.itemList)
            {
                if (item != null && item.skin != 0 && gangSkinIds.Contains(item.skin))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Get a player's gang name by reading directly from HoodWars_CoreData.json.
        /// This bypasses Plugin.Call() which doesn't work with private methods.
        /// </summary>
        private string GetPlayerGangFromHoodWars(BasePlayer player)
        {
            if (player == null)
            {
                DebugLog("GetPlayerGangFromHoodWars - player is null");
                return null;
            }

            DebugLog($"Attempting to get gang for player {player.userID} ({player.displayName})");

            // Read directly from HoodWars data file
            try
            {
                var dataFile = Interface.Oxide.DataFileSystem.GetFile("HoodWars_CoreData");
                if (dataFile == null)
                {
                    DebugLog("HoodWars_CoreData.json not found");
                    return null;
                }

                var hoodWarsData = dataFile.ReadObject<HoodWarsStoredData>();
                if (hoodWarsData == null || hoodWarsData.Players == null)
                {
                    DebugLog("HoodWars data is null or has no Players dictionary");
                    return null;
                }

                DebugLog($"HoodWars data loaded, {hoodWarsData.Players.Count} players in database");

                if (!hoodWarsData.Players.TryGetValue(player.userID, out var playerInfo))
                {
                    DebugLog($"Player {player.userID} not found in HoodWars data");
                    return null;
                }

                DebugLog($"Player {player.userID} HomeHood = {playerInfo.HomeHood}");

                // HomeHood: 0=West, 1=North, 2=South, 3=East, 4=Neutral
                if (playerInfo.HomeHood < 0 || playerInfo.HomeHood >= GangNames.Length)
                {
                    DebugLog($"Invalid HomeHood value: {playerInfo.HomeHood}");
                    return null;
                }

                var gangName = GangNames[playerInfo.HomeHood];
                DebugLog($"Resolved gang name: '{gangName}'");

                // Check if it's a valid gang (not Neutral)
                if (gangName == NeutralTerritory || gangName == NeutralGround)
                {
                    DebugLog("Player is Neutral, returning null");
                    return null;
                }

                DebugLog($"SUCCESS! Player {player.userID} is in gang '{gangName}'");
                return gangName;
            }
            catch (Exception ex)
            {
                DebugLog($"Exception reading HoodWars data: {ex.Message}");
                return null;
            }
        }

        #endregion

        #region Scientist Creation & Death Handling

        private ScientistNPC CreateDressedGangScientist(Vector3 position, Quaternion rotation, string gangName)
        {
            var npcEntity = GameManager.server.CreateEntity(PrefabScientist, position, rotation);
            if (npcEntity == null)
            {
                Puts("[DriveBySedanGangs] ERROR: Failed to create scientist entity.");
                return null;
            }

            var npc = npcEntity as ScientistNPC;
            if (npc == null)
            {
                Puts($"[DriveBySedanGangs] ERROR: Scientist cast failed. Entity type: {npcEntity?.GetType().Name ?? "NULL"}");
                npcEntity.Kill();
                return null;
            }

            npc.Spawn();

            if (npc.net != null)
                _driveByNPCs.Add(npc.net.ID.Value);

            npc.InitializeHealth(ScientistHealth, ScientistHealth);
            npc.startHealth = ScientistHealth;
            npc.SetMaxHealth(ScientistHealth);
            npc.SetHealth(ScientistHealth);

            npc.inventory.Strip();

            if (_gangKits.TryGetValue(gangName, out var kit))
            {
                Puts($"[DriveBySedanGangs] Dressing NPC with gang kit: {gangName}");
                foreach (var itemShort in kit.Clothing)
                {
                    ulong skin = kit.Skins.ContainsKey(itemShort) ? kit.Skins[itemShort] : 0;
                    var item = ItemManager.CreateByName(itemShort, 1, skin);
                    if (item != null)
                        npc.inventory.GiveItem(item, npc.inventory.containerWear);
                }

                var weapon = ItemManager.CreateByName(kit.Weapon, 1, kit.WeaponSkin);
                if (weapon != null)
                {
                    npc.inventory.GiveItem(weapon, npc.inventory.containerBelt);
                    npc.UpdateActiveItem(weapon.uid);
                    Puts($"[DriveBySedanGangs] NPC equipped with weapon: {kit.Weapon}");
                }
                else
                {
                    Puts($"[DriveBySedanGangs] WARNING: Failed to create weapon '{kit.Weapon}' for NPC");
                }
            }
            else
            {
                Puts($"[DriveBySedanGangs] WARNING: No gang kit found for '{gangName}'. Available kits: {string.Join(", ", _gangKits.Keys)}");
            }

            npc.SetPlayerFlag(BasePlayer.PlayerFlags.Relaxed, false);
            npc.SetPlayerFlag(BasePlayer.PlayerFlags.DisplaySash, false);

            var agent = npc.GetComponent<NavMeshAgent>();
            if (agent != null)
            {
                agent.enabled = true;
                agent.speed = 0.1f;
                agent.acceleration = 1f;
                agent.stoppingDistance = 0f;
                agent.autoBraking = true;
            }

            if (npc.Brain != null)
            {
                npc.Brain.SetEnabled(true);
                if (npc.Brain.Navigator != null)
                {
                    npc.Brain.Navigator.CanUseNavMesh = true;
                    npc.Brain.Navigator.CanUseAStar = true;
                    npc.Brain.Navigator.MaxRoamDistanceFromHome = 500f;
                }
            }

            return npc;
        }

        private void OnEntityDeath(BaseCombatEntity entity, HitInfo info)
        {
            var npc = entity as ScientistNPC;
            if (npc != null)
            {
                if (npc.net != null)
                    _driveByNPCs.Remove(npc.net.ID.Value);

                HandleScientistDeath(npc);
                return;
            }
        }

        private void HandleScientistDeath(ScientistNPC npc)
        {
            if (npc == null) return;

            _scientistSeats.Remove(npc);

            BaseEntity ownerCar = null;

            foreach (var kvp in _sedanScientists)
            {
                var car = kvp.Key;
                var list = kvp.Value;
                if (list == null) continue;

                if (list.Remove(npc))
                {
                    ownerCar = car;
                    break;
                }
            }

            if (ownerCar == null) return;

            if (_sedanScientists.TryGetValue(ownerCar, out var remaining))
            {
                if (remaining == null || remaining.Count == 0)
                {
                    _sedanScientists.Remove(ownerCar);
                    _deployedSedans.Remove(ownerCar);
                    _deployScheduled.Remove(ownerCar);
                    _driveByStates.Remove(ownerCar);
                    _retiringSedans.Remove(ownerCar);

                    if (ownerCar != null && !ownerCar.IsDestroyed)
                        ownerCar.Kill();
                }
            }
        }

        #endregion

        #region Lifecycle

        private void OnServerInitialized()
        {
            LoadGangConfig();
            
            // Load debug mode from config
            if (Config["DebugMode"] != null)
            {
                _debugMode = Convert.ToBoolean(Config["DebugMode"]);
            }
            
            timer.Every(FollowUpdateInterval, UpdateAllSedans);
            timer.Every(TerritoryCheckInterval, CheckPlayerTerritories);
        }

        private void Unload()
        {
            foreach (var list in _playerSedans.Values.ToArray())
            {
                foreach (var car in list.ToArray())
                {
                    if (car != null && !car.IsDestroyed)
                        car.Kill();
                }
            }

            _playerSedans.Clear();

            foreach (var kvp in _sedanScientists.ToArray())
            {
                var sciList = kvp.Value;
                if (sciList == null) continue;

                foreach (var npc in sciList.ToArray())
                {
                    if (npc != null && !npc.IsDestroyed)
                        npc.Kill();

                    if (npc != null)
                        _scientistSeats.Remove(npc);
                }
            }

            _sedanScientists.Clear();
            _deployedSedans.Clear();
            _deployScheduled.Clear();
            _driveByStates.Clear();
            _retiringSedans.Clear();
            _scientistSeats.Clear();
            _playerLastTerritory.Clear();
            _playerTerritorySpawnCooldown.Clear();
        }

        /// <summary>
        /// Check all players' territories and spawn drive-by gangs when they cross into enemy territory.
        /// </summary>
        private void CheckPlayerTerritories()
        {
            if (HoodWars == null || !HoodWars.IsLoaded)
                return;

            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player == null || player.IsNpc || !player.IsConnected || player.IsDead())
                    continue;

                CheckPlayerTerritory(player);
            }
        }

        /// <summary>
        /// Check if a player has crossed into enemy territory and spawn a drive-by gang if so.
        /// </summary>
        private void CheckPlayerTerritory(BasePlayer player)
        {
            if (player == null || HoodWars == null || !HoodWars.IsLoaded)
                return;

            // Get player's gang from HoodWars using multiple fallback methods
            var playerGang = GetPlayerGangFromHoodWars(player);
            if (string.IsNullOrEmpty(playerGang))
                return; // Neutral players don't trigger territory spawns

            // Get the territory the player is currently in based on their world position (X, Z coordinates)
            // HoodWars divides the map into 4 quadrants based on X and Z coordinates
            var currentTerritory = HoodWars.Call("GetNeighborhoodNameAt", player.transform.position) as string;
            if (string.IsNullOrEmpty(currentTerritory) || currentTerritory == NeutralGround)
            {
                // Update last territory but don't spawn
                _playerLastTerritory[player.userID] = currentTerritory ?? NeutralGround;
                return;
            }

            // Get player's last known territory
            _playerLastTerritory.TryGetValue(player.userID, out var lastTerritory);

            // Debug logging to help test territory detection
            if (lastTerritory != currentTerritory)
            {
                DebugLog($"Player {player.userID} moved from '{lastTerritory ?? "null"}' to '{currentTerritory}' at position X:{player.transform.position.x:F0} Z:{player.transform.position.z:F0}");
                DebugLog($"Player's gang: {playerGang}, Current territory: {currentTerritory}, Is enemy territory: {currentTerritory != playerGang}");
            }

            // Update current territory
            _playerLastTerritory[player.userID] = currentTerritory;

            // Check if player crossed into a new enemy territory
            if (lastTerritory != currentTerritory && currentTerritory != playerGang)
            {
                // Player entered enemy territory
                // Check cooldown
                if (_playerTerritorySpawnCooldown.TryGetValue(player.userID, out var lastSpawnTime))
                {
                    var cooldownRemaining = TerritorySpawnCooldown - (Time.realtimeSinceStartup - lastSpawnTime);
                    if (cooldownRemaining > 0)
                    {
                        DebugLog($"Player {player.userID} on cooldown for {cooldownRemaining:F0} more seconds");
                        return; // Still on cooldown
                    }
                }

                // Check if player already has an active drive-by gang
                if (_playerSedans.TryGetValue(player.userID, out var existingGangs) && existingGangs.Count > 0)
                {
                    // Clean up destroyed sedans
                    existingGangs.RemoveAll(c => c == null || c.IsDestroyed);
                    if (existingGangs.Count > 0)
                    {
                        DebugLog($"Player {player.userID} already has {existingGangs.Count} active drive-by gang(s)");
                        return; // Already has active gang
                    }
                }

                // Spawn a drive-by gang from the territory they entered
                DebugLog($"TERRITORY SPAWN: Player {player.userID} ({playerGang}) crossed into {currentTerritory} territory at X:{player.transform.position.x:F0} Z:{player.transform.position.z:F0} - spawning drive-by!");
                
                // Set cooldown
                _playerTerritorySpawnCooldown[player.userID] = Time.realtimeSinceStartup;

                // Spawn the gang with the territory's gang name
                EnsureGangForPlayerWithGang(player, 1, currentTerritory);

                // Notify the player
                player.ChatMessage($"<color=#ff4444>WARNING:</color> You've entered {currentTerritory} territory! A drive-by gang has been dispatched!");
            }
        }

        #endregion

        #region Hooks

        // NOTE: no auto spawn on init/respawn/disconnect anymore.
        // Only /stalksedan and /destroysedan control gangs.

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            DestroyGangForPlayer(player);
        }

        private void OnEntityKill(BaseNetworkable entity)
        {
            var baseEntity = entity as BaseEntity;
            if (baseEntity == null)
                return;

            if (!IsSedan(baseEntity))
                return;

            ulong playerToClean = 0;
            bool needsCleanup = false;

            foreach (var kvp in _playerSedans.ToArray())
            {
                var list = kvp.Value;
                if (list == null)
                    continue;

                if (list.Remove(baseEntity))
                {
                    playerToClean = kvp.Key;
                    needsCleanup = true;
                    break;
                }
            }

            if (needsCleanup)
            {
                if (_playerSedans.TryGetValue(playerToClean, out var list) && (list == null || list.Count == 0))
                    _playerSedans.Remove(playerToClean);
            }

            if (_sedanScientists.TryGetValue(baseEntity, out var sciList))
            {
                foreach (var npc in sciList.ToArray())
                {
                    if (npc != null && !npc.IsDestroyed)
                        npc.Kill();

                    if (npc != null)
                        _scientistSeats.Remove(npc);
                }

                _sedanScientists.Remove(baseEntity);
            }

            _deployedSedans.Remove(baseEntity);
            _deployScheduled.Remove(baseEntity);
            _driveByStates.Remove(baseEntity);
            _retiringSedans.Remove(baseEntity);
        }

        #endregion

        #region Sedan / Gang Management

        private bool IsSedan(BaseEntity ent)
        {
            if (ent == null) return false;
            return ent.ShortPrefabName.Equals("sedantest.entity", StringComparison.OrdinalIgnoreCase)
                   || ent.PrefabName == SedanPrefab;
        }

        private void EnsureGangForPlayer(BasePlayer player, int desiredCount)
        {
            if (player == null || !player.IsConnected)
                return;

            if (!_playerSedans.TryGetValue(player.userID, out var list))
            {
                list = new List<BaseEntity>();
                _playerSedans[player.userID] = list;
            }

            // Clean invalid cars
            for (int i = list.Count - 1; i >= 0; i--)
            {
                if (list[i] == null || list[i].IsDestroyed)
                    list.RemoveAt(i);
            }

            int missing = desiredCount - list.Count;
            if (missing <= 0)
                return;

            for (int i = 0; i < missing; i++)
            {
                var car = SpawnSedanNearPlayer(player, i, desiredCount);
                if (car != null)
                    list.Add(car);
            }
        }

        private BaseEntity SpawnSedanNearPlayer(BasePlayer player, int indexInGang, int gangSize)
        {
            Vector3 playerPos = player.transform.position;

            float angle = (360f / Mathf.Max(gangSize, 1)) * indexInGang;
            float rad = angle * Mathf.Deg2Rad;

            Vector3 offset = new Vector3(
                Mathf.Cos(rad) * SpawnRadius,
                0f,
                Mathf.Sin(rad) * SpawnRadius
            );

            Vector3 samplePos = playerPos + offset;

            if (!FindGroundPosition(samplePos, out var finalPos))
            {
                PrintWarning($"[DriveBySedanGangs] Failed to find ground for sedan spawn near {player.displayName}.");
                return null;
            }

            Vector3 toPlayer = (playerPos - finalPos);
            toPlayer.y = 0f;
            if (toPlayer.sqrMagnitude < 0.01f)
                toPlayer = -player.transform.forward;

            toPlayer.Normalize();
            Quaternion spawnRot = Quaternion.LookRotation(toPlayer, Vector3.up);

            BaseEntity car = GameManager.server.CreateEntity(SedanPrefab, finalPos, spawnRot, true);
            if (car == null)
            {
                PrintError("Failed to create sedan entity from prefab: " + SedanPrefab);
                return null;
            }

            car.enableSaving = false;
            car.Spawn();

            var rb = car.GetComponent<Rigidbody>();
            if (rb != null)
            {
                rb.velocity = Vector3.zero;
                rb.angularVelocity = Vector3.zero;
                rb.isKinematic = false;
            }

            car.SendNetworkUpdateImmediate();

            _deployedSedans.Remove(car);
            _deployScheduled.Remove(car);
            _retiringSedans.Remove(car);

            SeatGangScientistsInSedan(car, DefaultGangName, player);

            return car;
        }

        private void SeatGangScientistsInSedan(BaseEntity car, string gangName, BasePlayer target)
        {
            if (car == null || car.IsDestroyed) return;

            var seats = car.GetComponentsInChildren<BaseMountable>(true);
            if (seats == null || seats.Length == 0)
            {
                Puts("[DriveBySedanGangs] No seats (BaseMountable) found on sedan; cannot seat scientists.");
                return;
            }

            int needed = 3;
            var seated = new List<ScientistNPC>();

            foreach (var seat in seats)
            {
                if (needed <= 0)
                    break;

                if (seat == null || seat.IsDestroyed) continue;
                if (seat.AnyMounted()) continue;

                Vector3 spawnPos = seat.transform.position + Vector3.up * 0.1f;
                var npc = CreateDressedGangScientist(spawnPos, seat.transform.rotation, gangName);
                if (npc == null) continue;

                seat.AttemptMount(npc);

                _scientistSeats[npc] = seat;

                if (npc.Brain != null && target != null)
                {
                    if (npc.Brain.Senses?.Memory != null)
                        npc.Brain.Senses.Memory.SetKnown(target, npc, npc.Brain.Senses);

                    if (npc.Brain.Events?.Memory?.Entity != null)
                        npc.Brain.Events.Memory.Entity.Set(target, 0);
                }

                seated.Add(npc);
                needed--;
            }

            if (seated.Count > 0)
            {
                _sedanScientists[car] = seated;

                _driveByStates[car] = new DriveByState
                {
                    TargetID = target.userID,
                    Shooters = new List<ScientistNPC>(seated),
                    LastShootTime = 0f,
                    LastShooterIndex = -1,
                    GangName = gangName
                };
            }
        }

        private void DestroyGangForPlayer(BasePlayer player)
        {
            if (player == null)
                return;

            if (!_playerSedans.TryGetValue(player.userID, out var list))
                return;

            foreach (var car in list.ToArray())
            {
                if (car != null && !car.IsDestroyed)
                    car.Kill();

                if (car != null && _sedanScientists.TryGetValue(car, out var sciList))
                {
                    foreach (var npc in sciList.ToArray())
                    {
                        if (npc != null && !npc.IsDestroyed)
                            npc.Kill();

                        if (npc != null)
                            _scientistSeats.Remove(npc);
                    }

                    _sedanScientists.Remove(car);
                }

                _deployedSedans.Remove(car);
                _deployScheduled.Remove(car);
                _driveByStates.Remove(car);
                _retiringSedans.Remove(car);
            }

            _playerSedans.Remove(player.userID);
        }

        #endregion

        #region Driving + Deployment + Shooting

        private void UpdateAllSedans()
        {
            if (_playerSedans.Count == 0)
                return;

            var playerIds = new List<ulong>(_playerSedans.Keys);

            foreach (var playerId in playerIds)
            {
                var player = BasePlayer.FindByID(playerId) ?? BasePlayer.FindSleeping(playerId);
                if (player == null || !player.IsConnected || player.IsDead())
                {
                    if (_playerSedans.TryGetValue(playerId, out var listToClean))
                    {
                        foreach (var car in listToClean.ToArray())
                        {
                            if (car != null && !car.IsDestroyed)
                                car.Kill();

                            if (car != null && _sedanScientists.TryGetValue(car, out var sciList))
                            {
                                foreach (var npc in sciList.ToArray())
                                {
                                    if (npc != null && !npc.IsDestroyed)
                                        npc.Kill();

                                    if (npc != null)
                                        _scientistSeats.Remove(npc);
                                }

                                _sedanScientists.Remove(car);
                            }

                            _deployedSedans.Remove(car);
                            _deployScheduled.Remove(car);
                            _driveByStates.Remove(car);
                            _retiringSedans.Remove(car);
                        }
                    }

                    _playerSedans.Remove(playerId);
                    continue;
                }

                if (!_playerSedans.TryGetValue(playerId, out var sedans) || sedans == null)
                    continue;

                var sedanSnapshot = sedans.ToArray();
                var toRemoveFromPlayerList = new List<BaseEntity>();

                foreach (var car in sedanSnapshot)
                {
                    if (car == null || car.IsDestroyed)
                    {
                        toRemoveFromPlayerList.Add(car);
                        continue;
                    }

                    if (_retiringSedans.Contains(car))
                        continue;

                    // Manual shooting both mounted and on-foot
                    if (_driveByStates.TryGetValue(car, out var state))
                    {
                        MakeNPCsShootAtPlayer(car, state);
                    }

                    // If not yet deployed, drive car
                    if (!_deployedSedans.Contains(car))
                    {
                        DriveSedanTowardsPlayer(car, player);
                    }
                    else
                    {
                        // Deployed: keep chase nav updated
                        if (_sedanScientists.TryGetValue(car, out var sciList) && sciList != null)
                        {
                            foreach (var sci in sciList.ToArray())
                            {
                                if (sci == null || sci.IsDestroyed) continue;
                                if (sci.Brain == null || sci.Brain.Navigator == null) continue;

                                sci.Brain.Navigator.SetDestination(player.transform.position, BaseNavigator.NavigationSpeed.Normal);
                            }
                        }
                    }
                }

                foreach (var car in toRemoveFromPlayerList)
                {
                    sedans.Remove(car);
                }

                if (sedans.Count == 0)
                    _playerSedans.Remove(playerId);
            }
        }

        /// <summary>
        /// Manual shooting logic, used both while mounted and on foot.
        /// </summary>
        private void MakeNPCsShootAtPlayer(BaseEntity car, DriveByState ev)
        {
            if (car == null || car.IsDestroyed) return;
            if (_retiringSedans.Contains(car)) return;

            if (Time.realtimeSinceStartup - ev.LastShootTime < ShootInterval) return;

            BasePlayer target = BasePlayer.FindByID(ev.TargetID);
            if (target == null || !target.IsAlive()) return;

            // Don't shoot gang members wearing the same gang's clothing
            if (!string.IsNullOrEmpty(ev.GangName) && IsPlayerWearingGangClothing(target, ev.GangName))
                return;

            var validShooters = new List<ScientistNPC>();
            for (int i = 0; i < ev.Shooters.Count; i++)
            {
                var npc = ev.Shooters[i];
                if (npc == null || npc.IsDestroyed) continue;
                // Still skip index 0 as "driver" when mounted,
                // but on foot it doesn't hurt – can adjust if needed.
                if (i == 0 && npc.isMounted) continue;
                validShooters.Add(npc);
            }
            if (validShooters.Count == 0) return;

            ev.LastShooterIndex = (ev.LastShooterIndex + 1) % validShooters.Count;
            var shooter = validShooters[ev.LastShooterIndex];
            if (shooter == null || shooter.IsDestroyed) return;

            float distToTarget = Vector3.Distance(shooter.transform.position, target.transform.position);
            if (distToTarget < MinShootDistance || distToTarget > MaxShootDistance) return;

            Vector3 npcEyes = shooter.eyes?.position ?? (shooter.transform.position + Vector3.up * 1.5f);
            Vector3 targetPos = target.transform.position + Vector3.up * 1.2f;
            int losMask = LayerMask.GetMask("World", "Construction", "Terrain");

            if (Physics.Linecast(npcEyes, targetPos, losMask))
                return;

            Vector3 lookDir = (targetPos - npcEyes).normalized;
            shooter.SetAimDirection(lookDir);

            var heldEntity = shooter.GetHeldEntity() as BaseProjectile;
            if (heldEntity != null)
            {
                if (heldEntity.primaryMagazine.contents <= 0)
                    heldEntity.primaryMagazine.contents = heldEntity.primaryMagazine.capacity;

                shooter.SignalBroadcast(BaseEntity.Signal.Attack, string.Empty);
                heldEntity.ServerUse();

                ev.LastShootTime = Time.realtimeSinceStartup;
            }
        }

        private void ScheduleDeploy(BaseEntity car, BasePlayer target)
        {
            if (car == null || car.IsDestroyed || target == null) return;
            if (_retiringSedans.Contains(car)) return;
            if (_deployScheduled.Contains(car)) return;

            _deployScheduled.Add(car);

            var rb = car.GetComponent<Rigidbody>();
            if (rb != null)
            {
                rb.velocity = Vector3.zero;
                rb.angularVelocity = Vector3.zero;
                rb.isKinematic = true;
            }

            timer.Once(DeployDelaySeconds, () =>
            {
                if (car == null || car.IsDestroyed) return;
                if (_retiringSedans.Contains(car)) return;
                if (target == null || target.IsDead()) return;

                DeployScientistsFromSedan(car, target);
            });
        }

        private void DeployScientistsFromSedan(BaseEntity car, BasePlayer target)
        {
            if (car == null || car.IsDestroyed || target == null) return;
            if (_retiringSedans.Contains(car)) return;
            if (_deployedSedans.Contains(car)) return;

            _deployedSedans.Add(car);

            if (!_sedanScientists.TryGetValue(car, out var sciList) || sciList == null || sciList.Count == 0)
                return;

            foreach (var sci in sciList.ToArray())
            {
                if (sci == null || sci.IsDestroyed) continue;

                if (sci.isMounted)
                {
                    BaseMountable seat;
                    if (_scientistSeats.TryGetValue(sci, out seat) && seat != null && !seat.IsDestroyed)
                    {
                        seat.DismountAllPlayers();
                    }
                    else
                    {
                        var mountable = sci.GetMounted() as BaseMountable;
                        if (mountable != null && !mountable.IsDestroyed)
                            mountable.DismountAllPlayers();
                    }
                }

                _scientistSeats.Remove(sci);

                var agent = sci.GetComponent<NavMeshAgent>();
                if (agent != null)
                {
                    agent.enabled = true;
                    agent.stoppingDistance = 5f;
                    agent.speed = ScientistMoveSpeed;
                    agent.acceleration = 8f;
                    agent.autoBraking = true;
                }

                if (sci.Brain != null)
                {
                    sci.Brain.SetEnabled(true);

                    if (sci.Brain.Navigator != null)
                    {
                        sci.Brain.Navigator.CanUseNavMesh = true;
                        sci.Brain.Navigator.CanUseAStar = true;
                        sci.Brain.Navigator.MaxRoamDistanceFromHome = 500f;
                        sci.Brain.Navigator.SetDestination(target.transform.position, BaseNavigator.NavigationSpeed.Normal);
                    }

                    if (sci.Brain.Senses?.Memory != null)
                        sci.Brai
// ... [Code preview truncated due to length. Full source in attached file] ...

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