using System; using System.Collections.Generic; using Oxide.Core; using Oxide.Core.Plugins; using Oxide.Game.Rust.Cui; using UnityEngine; using Facepunch; using Network; // ____ _ _ _ // / ___(_) __ _(_) | ___ // \___ \| |/ _` | | |/ _ \ // ___) | | (_| | | | (_) | // |____/|_|\__, |_|_|\___/ // |___/ // ☽✧✵✧✵✧✵✧✵✧✵✧✵✧☾ // ✦ RUST PLUGINS ✦ // ✦ https://sigilo.dev ✦ // ☽✧✵✧✵✧✵✧✵✧✵✧✵✧☾ namespace Oxide.Plugins { [Info("MinicopterCombat", "Sigilo", "2.6.0")] [Description("Adds realistic frontal minigun with bullet travel time and side missiles to the Minicopter.")] public class MinicopterCombat : RustPlugin { #region Configuration private class PluginConfig { public bool RequirePermission = true; public float MinigunDamage = 20.0f; public float MinigunRange = 400.0f; public float MinigunBulletVelocity = 500.0f; public float MinigunBulletGravity = 0.6f; public float MinigunAimconeDegrees = 0.4f; public float MinigunRoundsPerSecond = 10.0f; public float MinigunDamageFalloffStart = 250.0f; public float MinigunDamageFalloffEnd = 400.0f; public float MinigunMinDamageMultiplier = 0.6f; public float MinigunMuzzleForward = 1.5f; public float MinigunMuzzleUp = 0.3f; public float MinigunMuzzleSide = 0.0f; public int MinigunMagazineSize = 100; public float MinigunReloadTime = 30.0f; public bool MinigunEnableMuzzleFlash = true; public bool MinigunEnableTracers = false; public bool MinigunEnableSound = true; public bool MinigunEnableImpactEffects = true; public float MinigunTracerChance = 0.2f; public string MinigunSoundEffect = "assets/prefabs/weapons/ak47u/effects/attack.prefab"; public string MinigunMuzzleFlashEffect = "assets/prefabs/weapons/ak47u/effects/attack.prefab"; public string MinigunTracerEffect = ""; public string MinigunImpactEffect = "assets/bundled/prefabs/fx/impacts/additive/explosion.prefab"; public string MinigunFleshImpactEffect = "assets/bundled/prefabs/fx/impacts/additive/fire.prefab"; public bool ExplosiveBulletsEnabled = true; public float ExplosiveBulletChance = 0.01f; public float ExplosiveBulletRadius = 1.5f; public float ExplosiveBulletDamage = 30.0f; public string ExplosiveBulletEffect = "assets/bundled/prefabs/fx/explosions/explosion_01.prefab"; public float MissileCooldown = 1.5f; public float MissileSpeed = 100.0f; public string RocketPrefab = "assets/prefabs/ammo/rocket/rocket_hv.prefab"; public float RocketOffsetForward = 1.5f; public float RocketOffsetSide = 1.0f; public float RocketOffsetUp = -0.3f; public int MaxMissilesPerReload = 6; public float MissileReloadCooldown = 30.0f; public string MissileReloadStartSound = "assets/prefabs/deployable/repair bench/effects/skinchange_spraypaint.prefab"; public string MissileReloadCompleteSound = "assets/bundled/prefabs/fx/invite_notice.prefab"; public bool InheritHelicopterVelocity = false; public bool HeadshotMultiplier = true; public float HeadshotDamageMultiplier = 1.5f; public bool DebugMode = false; public bool EnableHUD = true; public bool EnableLockOn = true; public float LockOnTime = 0.35f; public float LockOnMaxRange = 400.0f; public float LockOnMaxAngle = 10.0f; public string HomingRocketPrefab = "assets/prefabs/ammo/rocket/rocket_hv.prefab"; public bool EnableRocketBoost = true; public float BoostThrustMultiplier = 1.5f; public float BoostFuelDuration = 1.0f; public float BoostRechargeTime = 15.0f; public float BoostCrashSpeedThreshold = 15.0f; public string BoostActivateSound = "assets/prefabs/npc/patrol helicopter/effects/rocket_fire.prefab"; public string BoostLoopEffect = "assets/prefabs/ammo/rocket/rocket_basic.prefab"; public string BoostCrashExplosionEffect = "assets/bundled/prefabs/fx/explosions/explosion_01.prefab"; public string BoostCrashFireEffect = "assets/bundled/prefabs/fx/fire/fire_v3_2x2.prefab"; public bool EnableBombs = true; public int MaxBombsPerReload = 4; public float BombReloadTime = 20.0f; public float BombCooldown = 1.0f; public float BombDropForwardOffset = 0.0f; public float BombDropDownOffset = 1.5f; public float BombDamage = 150.0f; public float BombRadius = 6.0f; public string BombPrefab = "assets/prefabs/ammo/rocket/rocket_basic.prefab"; public string BombDropSound = "assets/prefabs/locks/keypad/effects/lock.code.lock.prefab"; public string BombExplosionEffect = "assets/bundled/prefabs/fx/gas_explosion_small.prefab"; public float ManualReloadCooldown = 45.0f; } private PluginConfig config; #endregion #region Constants private const string HUD_PANEL = "MinicopterCombat_HUD"; private const string PermissionUse = "minicoptercombat.use"; private static readonly int BulletLayerMask = ~( (1 << 2) | (1 << 29) ); #endregion #region Data Storage private readonly Dictionary minigunTimersByPlayer = new Dictionary(); private readonly Dictionary lastMissileFire = new Dictionary(); private readonly Dictionary nextRocketLeftByMini = new Dictionary(); private readonly Dictionary missileCountByMini = new Dictionary(); private readonly Dictionary isMissileReloading = new Dictionary(); private readonly Dictionary missileReloadTimers = new Dictionary(); private readonly Dictionary minigunAmmoByMini = new Dictionary(); private readonly Dictionary isMinigunReloading = new Dictionary(); private readonly Dictionary minigunReloadTimers = new Dictionary(); private readonly Dictionary hudUpdateTimers = new Dictionary(); private readonly Dictionary lockOnByPlayer = new Dictionary(); private readonly Dictionary boostByMini = new Dictionary(); private readonly HashSet boostedMinisVulnerable = new HashSet(); private readonly Dictionary bombCountByMini = new Dictionary(); private readonly Dictionary isBombReloading = new Dictionary(); private readonly Dictionary bombReloadTimers = new Dictionary(); private readonly Dictionary lastBombDrop = new Dictionary(); private readonly Dictionary lastManualReload = new Dictionary(); private readonly HashSet activePilots = new HashSet(); private float nextLockOnProcessTime = 0f; private const float LOCK_ON_PROCESS_INTERVAL = 0.1f; private class LockOnState { public PlayerHelicopter TargetHelicopter; public float LockProgress; public bool IsLocked; public float NextBeepTime; } private class IncomingLockState { public float LockProgress; public bool IsLocked; public float NextWarningTime; } private readonly Dictionary incomingLockByMini = new Dictionary(); private class BoostState { public PlayerHelicopter Helicopter; public bool IsActive; public float FuelRemaining; public float RechargeTimer; public float LastEffectTime; } private class ActiveHomingMissile { public ServerProjectile Missile; public BaseEntity Target; public float LaunchTime; } private readonly List activeMissiles = new List(); private readonly List activeMinicopters = new List(); private readonly List pendingBullets = new List(); private readonly HashSet activeBombs = new HashSet(); private class PendingBullet { public BasePlayer Owner; public PlayerHelicopter Helicopter; public Vector3 Origin; public Vector3 Velocity; public Vector3 CurrentPosition; public float SpawnTime; public float BaseDamage; public float MaxLifetime; public Vector3 LastPosition; public bool ShowTracer; public float TracerUpdateTime; } #endregion #region Oxide Hooks private void Init() { permission.RegisterPermission(PermissionUse, this); } protected override void LoadDefaultConfig() { config = new PluginConfig(); SaveConfig(); } protected override void LoadConfig() { base.LoadConfig(); try { config = Config.ReadObject(); if (config == null) throw new Exception("Configuration file is null"); } catch { PrintWarning("Configuration file is corrupt or missing, creating new one..."); LoadDefaultConfig(); } } protected override void SaveConfig() { Config.WriteObject(config, true); } private void OnServerInitialized() { try { config = Config.ReadObject(); if (config.LockOnMaxAngle > 20.0f) { config.LockOnMaxAngle = 10.0f; } SaveConfig(); activeMinicopters.Clear(); foreach (var entity in BaseNetworkable.serverEntities) { if (entity is PlayerHelicopter mini) activeMinicopters.Add(mini); } Puts($"MinicopterCombat v2.5.0 loaded - Bullet velocity: {config.MinigunBulletVelocity} m/s, Magazine: {config.MinigunMagazineSize}"); Puts($"Tracers: {(config.MinigunEnableTracers ? "Enabled" : "Disabled")}, Explosive: {(config.ExplosiveBulletsEnabled ? "Enabled" : "Disabled")}, Boost: {(config.EnableRocketBoost ? "Enabled" : "Disabled")}"); } catch (Exception e) { PrintError($"OnServerInitialized error: {e}"); } } private void OnTick() { ProcessPendingBullets(); if (activePilots.Count > 0 && config.EnableLockOn) { float currentTime = Time.realtimeSinceStartup; if (currentTime >= nextLockOnProcessTime) { nextLockOnProcessTime = currentTime + LOCK_ON_PROCESS_INTERVAL; ProcessLockOnLogic(); } } if (activeMissiles.Count > 0) { ProcessHomingMissiles(); } if (boostByMini.Count > 0) { ProcessBoostPhysics(); } } private void ProcessLockOnLogic() { activePilots.RemoveWhere(p => p == null || !p.IsConnected || p.IsDead()); foreach (var player in activePilots) { if (!IsInMiniCopter(player, out PlayerHelicopter mini, out BaseMountable seat)) { if (lockOnByPlayer.ContainsKey(player.userID)) { lockOnByPlayer.Remove(player.userID); } continue; } if (!lockOnByPlayer.TryGetValue(player.userID, out LockOnState state)) { state = new LockOnState(); lockOnByPlayer[player.userID] = state; } PlayerHelicopter target = null; PlayerHelicopter bestTarget = null; float bestAngle = config.LockOnMaxAngle; Vector3 eyePos = player.eyes.position; Vector3 lookDir = player.eyes.HeadForward(); for (int i = activeMinicopters.Count - 1; i >= 0; i--) { var targetMini = activeMinicopters[i]; if (targetMini == null || targetMini.IsDestroyed) { activeMinicopters.RemoveAt(i); continue; } if (targetMini == mini) continue; Vector3 toTarget = targetMini.transform.position - eyePos; float distSqr = toTarget.sqrMagnitude; if (distSqr > config.LockOnMaxRange * config.LockOnMaxRange) continue; float angle = Vector3.Angle(lookDir, toTarget); if (angle > config.LockOnMaxAngle) continue; if (Physics.Linecast(eyePos, targetMini.transform.position + Vector3.up, out RaycastHit hit, BulletLayerMask)) { var hitEntity = hit.GetEntity(); if (hitEntity != targetMini && hitEntity?.GetParentEntity() != targetMini) { continue; } } if (angle < bestAngle) { bestAngle = angle; bestTarget = targetMini; } } if (bestTarget != null) { target = bestTarget; if (config.DebugMode && Time.realtimeSinceStartup % 1.0f < 0.1f) { } } if (target != null) { ulong targetMiniId = target.net.ID.Value; if (state.TargetHelicopter != target) { state.TargetHelicopter = target; state.LockProgress = 0f; state.IsLocked = false; } if (!incomingLockByMini.TryGetValue(targetMiniId, out IncomingLockState incomingState)) { incomingState = new IncomingLockState { LockProgress = 0f, IsLocked = false, NextWarningTime = 0f }; incomingLockByMini[targetMiniId] = incomingState; } if (!state.IsLocked) { state.LockProgress += Time.deltaTime / config.LockOnTime; incomingState.LockProgress = state.LockProgress; if (state.LockProgress >= 1.0f) { state.LockProgress = 1.0f; state.IsLocked = true; incomingState.LockProgress = 1.0f; incomingState.IsLocked = true; Effect.server.Run("assets/prefabs/npc/auto_turret/effects/target_acquired.prefab", player.transform.position); var targetDriver = target.GetDriver(); if (targetDriver != null) { Effect.server.Run("assets/prefabs/locks/keypad/effects/lock.denied.prefab", targetDriver.transform.position); } } else if (Time.realtimeSinceStartup >= state.NextBeepTime) { float beepDelay = Mathf.Lerp(0.8f, 0.1f, state.LockProgress); state.NextBeepTime = Time.realtimeSinceStartup + beepDelay; Effect.server.Run("assets/prefabs/locks/keypad/effects/lock.code.updated.prefab", player.transform.position); if (Time.realtimeSinceStartup >= incomingState.NextWarningTime) { incomingState.NextWarningTime = Time.realtimeSinceStartup + beepDelay; var targetDriver = target.GetDriver(); if (targetDriver != null) { Effect.server.Run("assets/prefabs/locks/keypad/effects/lock.code.lock.prefab", targetDriver.transform.position); } } } } else { incomingState.IsLocked = true; incomingState.LockProgress = 1.0f; } } else { if (state.TargetHelicopter != null) { ulong oldTargetId = state.TargetHelicopter.net.ID.Value; if (incomingLockByMini.TryGetValue(oldTargetId, out IncomingLockState oldIncoming)) { oldIncoming.LockProgress = Mathf.Max(0f, oldIncoming.LockProgress - Time.deltaTime / config.LockOnTime); if (oldIncoming.LockProgress <= 0f) { incomingLockByMini.Remove(oldTargetId); } oldIncoming.IsLocked = false; } } if (state.LockProgress > 0) { state.LockProgress -= Time.deltaTime / config.LockOnTime; if (state.LockProgress <= 0f) { state.LockProgress = 0f; state.TargetHelicopter = null; state.IsLocked = false; } } else { if (state.TargetHelicopter != null) { state.TargetHelicopter = null; state.IsLocked = false; } } } } } private void Unload() { foreach (var player in BasePlayer.activePlayerList) { DestroyHUD(player); } foreach (var t in minigunTimersByPlayer.Values) { try { t?.Destroy(); } catch { } } minigunTimersByPlayer.Clear(); foreach (var t in missileReloadTimers.Values) { try { t?.Destroy(); } catch { } } missileReloadTimers.Clear(); foreach (var t in minigunReloadTimers.Values) { try { t?.Destroy(); } catch { } } minigunReloadTimers.Clear(); foreach (var t in hudUpdateTimers.Values) { try { t?.Destroy(); } catch { } } hudUpdateTimers.Clear(); foreach (var t in bombReloadTimers.Values) { try { t?.Destroy(); } catch { } } bombReloadTimers.Clear(); pendingBullets.Clear(); } private void OnEntitySpawned(BaseNetworkable entity) { if (entity is PlayerHelicopter mini) { if (!activeMinicopters.Contains(mini)) activeMinicopters.Add(mini); } } private void OnEntityKill(BaseNetworkable entity) { if (entity is PlayerHelicopter mini) { if (activeMinicopters.Contains(mini)) activeMinicopters.Remove(mini); ulong miniId = mini.net.ID.Value; CleanupMinicopter(miniId); boostByMini.Remove(miniId); boostedMinisVulnerable.Remove(miniId); incomingLockByMini.Remove(miniId); } if (entity is BaseEntity bombEntity && activeBombs.Contains(bombEntity)) { activeBombs.Remove(bombEntity); Vector3 pos = bombEntity.transform.position; Effect.server.Run(config.BombExplosionEffect, pos, Vector3.up); Effect.server.Run(config.BombExplosionEffect, pos + Vector3.up * 0.5f, Vector3.up); } } private void OnEntityMounted(BaseMountable mountable, BasePlayer player) { try { if (player == null || mountable == null) return; if (!IsInMiniCopter(player, out PlayerHelicopter mini, out BaseMountable seat)) return; var driver = mini.GetDriver(); if (driver == null || driver != player) return; if (HasPermission(player)) { activePilots.Add(player); } } catch (Exception e) { PrintError($"OnEntityMounted error: {e}"); } } private void OnPlayerInput(BasePlayer player, InputState input) { try { if (player == null || input == null || !player.IsConnected) return; if (!IsInMiniCopter(player, out PlayerHelicopter mini, out BaseMountable seat)) return; var driver = mini.GetDriver(); if (driver == null || driver != player) return; if (mini.engineController == null || !mini.engineController.IsOn) return; if (!HasPermission(player)) return; ulong miniId = mini.net.ID.Value; if (config.EnableHUD) { StartHUDUpdates(player, mini); } if (input.IsDown(BUTTON.FIRE_PRIMARY)) { if (IsMinigunReloading(miniId)) { return; } StartMinigunTimer(player, mini); } else { StopMinigunTimer(player.userID); } if (input.WasJustPressed(BUTTON.FIRE_SECONDARY)) { TryFireMissiles(player, mini); } if (config.EnableRocketBoost) { HandleBoostInput(player, mini, input.IsDown(BUTTON.SPRINT)); } if (config.EnableBombs && input.WasJustPressed(BUTTON.USE)) { TryDropBomb(player, mini); } if (input.WasJustPressed(BUTTON.RELOAD)) { TryManualReloadAll(player, mini); } } catch (Exception e) { PrintError($"OnPlayerInput error: {e}"); } } private void HandleBoostInput(BasePlayer player, PlayerHelicopter mini, bool isPressed) { ulong miniId = mini.net.ID.Value; if (!boostByMini.TryGetValue(miniId, out BoostState state)) { state = new BoostState { Helicopter = mini, IsActive = false, FuelRemaining = config.BoostFuelDuration, RechargeTimer = 0f, LastEffectTime = 0f }; boostByMini[miniId] = state; } if (isPressed && state.FuelRemaining > 0f && state.RechargeTimer <= 0f) { if (!state.IsActive) { state.IsActive = true; boostedMinisVulnerable.Add(miniId); Effect.server.Run(config.BoostActivateSound, mini.transform.position); } } else { if (state.IsActive) { state.IsActive = false; if (state.FuelRemaining <= 0f) { state.RechargeTimer = config.BoostRechargeTime; player.ChatMessage($"Boost depleted! Recharging... {config.BoostRechargeTime}s"); } } } } private void ProcessBoostPhysics() { float dt = Time.deltaTime; float currentTime = Time.realtimeSinceStartup; List toRemove = null; foreach (var kvp in boostByMini) { ulong miniId = kvp.Key; BoostState state = kvp.Value; var mini = state.Helicopter; if (mini == null || mini.IsDestroyed) { if (toRemove == null) toRemove = new List(); toRemove.Add(miniId); continue; } if (state.RechargeTimer > 0f) { state.RechargeTimer -= dt; if (state.RechargeTimer <= 0f) { state.FuelRemaining = config.BoostFuelDuration; state.RechargeTimer = 0f; var driver = mini.GetDriver(); if (driver != null) { driver.ChatMessage($"Boost recharged!"); Effect.server.Run(config.MissileReloadCompleteSound, mini.transform.position); } } } if (state.IsActive && state.FuelRemaining > 0f) { state.FuelRemaining -= dt; if (mini.rigidBody != null) { Vector3 boostForce = mini.transform.forward * config.BoostThrustMultiplier * 50f; mini.rigidBody.AddForce(boostForce, ForceMode.Acceleration); } if (currentTime - state.LastEffectTime > 0.15f) { Vector3 exhaustPos = mini.transform.position - mini.transform.forward * 1.5f; Effect.server.Run(config.BoostLoopEffect, exhaustPos, -mini.transform.forward); state.LastEffectTime = currentTime; } if (state.FuelRemaining <= 0f) { state.IsActive = false; state.FuelRemaining = 0f; state.RechargeTimer = config.BoostRechargeTime; var driver = mini.GetDriver(); if (driver != null) { driver.ChatMessage($"Boost depleted! Recharging... {config.BoostRechargeTime}s"); } } } else if (!state.IsActive && state.RechargeTimer <= 0f && state.FuelRemaining < config.BoostFuelDuration) { boostedMinisVulnerable.Remove(miniId); } } if (toRemove != null) { foreach (var id in toRemove) { boostByMini.Remove(id); boostedMinisVulnerable.Remove(id); } } } private void OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info) { if (entity == null || info == null) return; var mini = entity as PlayerHelicopter; if (mini == null) return; ulong miniId = mini.net.ID.Value; if (!boostedMinisVulnerable.Contains(miniId)) return; if (!boostByMini.TryGetValue(miniId, out BoostState state)) return; if (!state.IsActive) return; float speed = mini.rigidBody != null ? mini.rigidBody.velocity.magnitude : 0f; if (speed < config.BoostCrashSpeedThreshold) return; bool isCollision = info.damageTypes.Has(Rust.DamageType.Collision) || info.damageTypes.Has(Rust.DamageType.Blunt) || (info.Initiator == null && info.damageTypes.Total() > 0); if (!isCollision) return; TriggerBoostCrashExplosion(mini, info.HitPositionWorld); } private void TriggerBoostCrashExplosion(PlayerHelicopter mini, Vector3 crashPoint) { if (mini == null || mini.IsDestroyed) return; ulong miniId = mini.net.ID.Value; boostByMini.Remove(miniId); boostedMinisVulnerable.Remove(miniId); Vector3 pos = crashPoint != Vector3.zero ? crashPoint : mini.transform.position; for (int i = 0; i < 3; i++) { Vector3 offset = UnityEngine.Random.insideUnitSphere * 2f; Effect.server.Run(config.BoostCrashExplosionEffect, pos + offset, Vector3.up); } Effect.server.Run(config.BoostCrashFireEffect, pos, Vector3.up); Effect.server.Run(config.BoostCrashFireEffect, pos + Vector3.up * 1f, Vector3.up); Effect.server.Run("assets/bundled/prefabs/fx/explosions/explosion_03.prefab", pos, Vector3.up); Effect.server.Run("assets/bundled/prefabs/fx/fire/fire_v3_1x1.prefab", pos + UnityEngine.Random.insideUnitSphere * 1.5f, Vector3.up); var driver = mini.GetDriver(); if (driver != null) { activePilots.Remove(driver); lockOnByPlayer.Remove(driver.userID); } timer.Once(0.1f, () => { if (mini != null && !mini.IsDestroyed) { mini.Kill(BaseNetworkable.DestroyMode.Gib); } }); } private void TryDropBomb(BasePlayer player, PlayerHelicopter mini) { try { float now = Time.realtimeSinceStartup; ulong miniId = mini.net.ID.Value; if (IsBombReloading(miniId)) { return; } if (lastBombDrop.TryGetValue(miniId, out float lastDrop)) { if (now - lastDrop < config.BombCooldown) { return; } } int bombsUsed = 0; if (bombCountByMini.TryGetValue(miniId, out int used)) { bombsUsed = used; } if (bombsUsed >= config.MaxBombsPerReload) { StartBombReload(player, mini, miniId); return; } lastBombDrop[miniId] = now; bombCountByMini[miniId] = bombsUsed + 1; Vector3 dropPos = mini.transform.position + mini.transform.forward * config.BombDropForwardOffset + Vector3.down * config.BombDropDownOffset; BaseEntity bomb = GameManager.server.CreateEntity(config.BombPrefab, dropPos, Quaternion.identity); if (bomb == null) return; ServerProjectile proj = bomb.GetComponent(); if (proj != null) { Vector3 heliVelocity = mini.rigidBody != null ? mini.rigidBody.velocity : Vector3.zero; Vector3 velocity = heliVelocity * 0.3f; velocity += Vector3.down * 20f; proj.InitializeVelocity(velocity); } bomb.Spawn(); activeBombs.Add(bomb); Effect.server.Run(config.BombDropSound, mini.transform.position); int remaining = config.MaxBombsPerReload - (bombsUsed + 1); if (remaining <= 0) { StartBombReload(player, mini, miniId); } } catch (Exception e) { PrintError($"TryDropBomb error: {e}"); } } private void StartBombReload(BasePlayer player, PlayerHelicopter mini, ulong miniId) { if (IsBombReloading(miniId)) return; isBombReloading[miniId] = true; Effect.server.Run(config.MissileReloadStartSound, mini.transform.position, Vector3.up); if (bombReloadTimers.TryGetValue(miniId, out var existingTimer)) { existingTimer?.Destroy(); } bombReloadTimers[miniId] = timer.Once(config.BombReloadTime, () => { CompleteBombReload(miniId); }); } private void CompleteBombReload(ulong miniId) { if (!isBombReloading.ContainsKey(miniId) || !isBombReloading[miniId]) return; var mini = BaseNetworkable.serverEntities.Find(new NetworkableId(miniId)) as PlayerHelicopter; bombCountByMini[miniId] = 0; isBombReloading[miniId] = false; if (mini != null) { Effect.server.Run(config.MissileReloadCompleteSound, mini.transform.position, Vector3.up); var driver = mini.GetDriver(); if (driver != null) { driver.ChatMessage($"Bombs reloaded! ({config.MaxBombsPerReload} ready)"); } } bombReloadTimers.Remove(miniId); } private int GetBombsRemaining(ulong miniId) { if (!bombCountByMini.TryGetValue(miniId, out int used)) used = 0; return config.MaxBombsPerReload - used; } private bool IsBombReloading(ulong miniId) { return isBombReloading.TryGetValue(miniId, out bool reloading) && reloading; } private void TryManualReloadAll(BasePlayer player, PlayerHelicopter mini) { try { float now = Time.realtimeSinceStartup; ulong miniId = mini.net.ID.Value; if (lastManualReload.TryGetValue(miniId, out float lastReload)) { float remaining = config.ManualReloadCooldown - (now - lastReload); if (remaining > 0) { player.ChatMessage($"Manual reload on cooldown ({remaining:F1}s)"); return; } } bool anyReloading = IsMinigunReloading(miniId) || IsReloadingMissiles(miniId) || IsBombReloading(miniId); if (anyReloading) { player.ChatMessage("Systems are already reloading!"); return; } int currentAmmo = GetMinigunAmmo(miniId); int currentMissiles = GetMissilesRemaining(miniId); int currentBombs = GetBombsRemaining(miniId); float currentBoost = config.BoostFuelDuration; if (boostByMini.TryGetValue(miniId, out BoostState bState)) { currentBoost = bState.FuelRemaining; } bool needsMinigun = currentAmmo < config.MinigunMagazineSize; bool needsMissiles = currentMissiles < config.MaxMissilesPerReload; bool needsBombs = currentBombs < config.MaxBombsPerReload; bool needsBoost = currentBoost < config.BoostFuelDuration; if (!needsMinigun && !needsMissiles && !needsBombs && !needsBoost) { player.ChatMessage("All systems already at full capacity!"); return; } lastManualReload[miniId] = now; if (minigunReloadTimers.TryGetValue(miniId, out var t1)) { t1?.Destroy(); minigunReloadTimers.Remove(miniId); } if (missileReloadTimers.TryGetValue(miniId, out var t2)) { t2?.Destroy(); missileReloadTimers.Remove(miniId); } if (bombReloadTimers.TryGetValue(miniId, out var t3)) { t3?.Destroy(); bombReloadTimers.Remove(miniId); } if (needsMinigun) isMinigunReloading[miniId] = true; if (needsMissiles) isMissileReloading[miniId] = true; if (needsBombs) isBombReloading[miniId] = true; if (needsBoost && boostByMini.TryGetValue(miniId, out BoostState boostState)) { boostState.IsActive = false; boostState.RechargeTimer = config.ManualReloadCooldown; boostedMinisVulnerable.Remove(miniId); } Effect.server.Run(config.MissileReloadStartSound, mini.transform.position, Vector3.up); player.ChatMessage($"Reloading depleted systems... {config.ManualReloadCooldown}s"); timer.Once(config.ManualReloadCooldown, () => { if (mini == null || mini.IsDestroyed) return; if (needsMinigun) { minigunAmmoByMini[miniId] = config.MinigunMagazineSize; isMinigunReloading[miniId] = false; } if (needsMissiles) { missileCountByMini[miniId] = 0; isMissileReloading[miniId] = false; } if (needsBombs) { bombCountByMini[miniId] = 0; isBombReloading[miniId] = false; } if (needsBoost && boostByMini.TryGetValue(miniId, out BoostState bs)) { bs.FuelRemaining = config.BoostFuelDuration; bs.RechargeTimer = 0f; } Effect.server.Run(config.MissileReloadCompleteSound, mini.transform.position, Vector3.up); var driver = mini.GetDriver(); if (driver != null) { driver.ChatMessage("Systems reloaded!"); } }); } catch (Exception e) { PrintError($"TryManualReloadAll error: {e}"); } } private void OnEntityDismounted(BaseMountable mountable, BasePlayer player) { try { if (player == null) return; activePilots.Remove(player); lockOnByPlayer.Remove(player.userID); StopMinigunTimer(player.userID); StopHUDUpdates(player); DestroyHUD(player); } catch { } } private void OnPlayerDisconnected(BasePlayer player, string reason) { try { if (player == null) return; activePilots.Remove(player); lockOnByPlayer.Remove(player.userID); StopMinigunTimer(player.userID); StopHUDUpdates(player); DestroyHUD(player); lastMissileFire.Remove(player.userID); } catch { } } private void OnPlayerDeath(BasePlayer player, HitInfo info) { try { if (player == null) return; activePilots.Remove(player); lockOnByPlayer.Remove(player.userID); StopMinigunTimer(player.userID); StopHUDUpdates(player); DestroyHUD(player); } catch { } } #endregion #region HUD System private void StartHUDUpdates(BasePlayer player, PlayerHelicopter mini) { if (hudUpdateTimers.ContainsKey(player.userID)) return; UpdateHUD(player, mini); hudUpdateTimers[player.userID] = timer.Every(0.5f, () => { if (player == null || !player.IsConnected) { StopHUDUpdates(player); return; } if (!IsInMiniCopter(player, out PlayerHelicopter m, out BaseMountable s)) { StopHUDUpdates(player); DestroyHUD(player); return; } UpdateHUD(player, m); }); } private void StopHUDUpdates(BasePlayer player) { if (player == null) return; if (hudUpdateTimers.TryGetValue(player.userID, out var t)) { try { t?.Destroy(); } catch { } hudUpdateTimers.Remove(player.userID); } } private void UpdateHUD(BasePlayer player, PlayerHelicopter mini) { if (player == null || mini == null) return; ulong miniId = mini.net.ID.Value; int bulletsRemaining = GetMinigunAmmo(miniId); int missilesRemaining = GetMissilesRemaining(miniId); bool minigunReloading = IsMinigunReloading(miniId); bool missileReloading = IsReloadingMissiles(miniId); float boostFuel = config.BoostFuelDuration; bool boostRecharging = false; if (config.EnableRocketBoost && boostByMini.TryGetValue(miniId, out BoostState boostState)) { boostFuel = boostState.FuelRemaining; boostRecharging = boostState.RechargeTimer > 0f; } int bombsRemaining = GetBombsRemaining(miniId); bool bombReloading = IsBombReloading(miniId); CuiHelper.DestroyUi(player, HUD_PANEL); var elements = new CuiElementContainer(); string panelAnchorMin = "0.35 0.095"; string panelAnchorMax = "0.65 0.125"; elements.Add(new CuiPanel { Image = { Color = "0 0 0 0" }, RectTransform = { AnchorMin = panelAnchorMin, AnchorMax = panelAnchorMax }, CursorEnabled = false }, "Hud", HUD_PANEL); elements.Add(new CuiPanel { Image = { Color = "0.1 0.1 0.1 0.7" }, RectTransform = { AnchorMin = "0.02 0.1", AnchorMax = "0.26 0.9" } }, HUD_PANEL, HUD_PANEL + "_BulletBG"); string bulletValue; if (minigunReloading) { bulletValue = "RELOADING"; } else { string bulletColor = bulletsRemaining > 30 ? "#88ff88" : (bulletsRemaining > 10 ? "#ffcc00" : "#ff4444"); bulletValue = $"{bulletsRemaining}"; } elements.Add(new CuiLabel { Text = { Text = $"Bullets: {bulletValue}", FontSize = 11, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1", Font = "robotocondensed-bold.ttf" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, HUD_PANEL + "_BulletBG"); elements.Add(new CuiPanel { Image = { Color = "0.1 0.1 0.1 0.7" }, RectTransform = { AnchorMin = "0.27 0.1", AnchorMax = "0.51 0.9" } }, HUD_PANEL, HUD_PANEL + "_RocketBG"); string rocketValue; if (missileReloading) { rocketValue = "RELOADING"; } else { string rocketColor = missilesRemaining > 2 ? "#ff8844" : (missilesRemaining > 0 ? "#ffcc00" : "#ff4444"); rocketValue = $"{missilesRemaining}"; } elements.Add(new CuiLabel { Text = { Text = $"Rockets: {rocketValue}", FontSize = 11, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1", Font = "robotocondensed-bold.ttf" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, HUD_PANEL + "_RocketBG"); if (config.EnableBombs) { elements.Add(new CuiPanel { Image = { Color = "0.1 0.1 0.1 0.7" }, RectTransform = { AnchorMin = "0.52 0.1", AnchorMax = "0.76 0.9" } }, HUD_PANEL, HUD_PANEL + "_BombBG"); string bombValue; if (bombReloading) { bombValue = "RELOADING"; } else { string bombColor = bombsRemaining > 2 ? "#aa44ff" : (bombsRemaining > 0 ? "#ffcc00" : "#ff4444"); bombValue = $"{bombsRemaining}"; } elements.Add(new CuiLabel { Text = { Text = $"Bombs: {bombValue}", FontSize = 11, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1", Font = "robotocondensed-bold.ttf" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, HUD_PANEL + "_BombBG"); } if (config.EnableRocketBoost) { elements.Add(new CuiPanel { Image = { Color = "0.1 0.1 0.1 0.7" }, RectTransform = { AnchorMin = "0.77 0.1", AnchorMax = "0.98 0.9" } }, HUD_PANEL, HUD_PANEL + "_BoostBG"); string boostValue; if (boostRecharging) { boostValue = "RECHARGING"; } else { float boostPercent = (boostFuel / config.BoostFuelDuration) * 100f; string boostColor = boostPercent > 50 ? "#44aaff" : (boostPercent > 25 ? "#ffcc00" : "#ff4444"); boostValue = $"{boostPercent:F0}%"; } elements.Add(new CuiLabel { Text = { Text = $"Boost: {boostValue}", FontSize = 10, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1", Font = "robotocondensed-bold.ttf" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" } }, HUD_PANEL + "_BoostBG"); } if (config.EnableLockOn && lockOnByPlayer.TryGetValue(player.userID, out LockOnState state) && state.TargetHelicopter != null) { elements.Add(new CuiPanel { Image = { Color = "0.0 0.0 0.0 0.6" }, RectTransform = { AnchorMin = "0.43 1.4", AnchorMax = "0.57 2.0" } }, HUD_PANEL, HUD_PANEL + "_LockBox"); if (state.LockProgress > 0) { elements.Add(new CuiPanel { Image = { Color = "0.15 0.15 0.15 0.8" }, RectTransform = { AnchorMin = "0.05 0.15", AnchorMax = "0.95 0.45" } }, HUD_PANEL + "_LockBox"); float fillMax = 0.05f + (0.90f * state.LockProgress); string barColor = state.IsLocked ? "0.9 0.2 0.2 1.0" : "1.0 0.75 0.0 1.0"; elements.Add(new CuiPanel { Image = { Color = barColor }, RectTransform = { AnchorMin = "0.05 0.15", AnchorMax = $"{fillMax} 0.45" } }, HUD_PANEL + "_LockBox"); } string statusSymbol = state.IsLocked ? "◉" : "◎"; string symbolColor = state.IsLocked ? "0.9 0.2 0.2 1.0" : "1.0 0.75 0.0 1.0"; elements.Add(new CuiLabel { Text = { Text = statusSymbol, FontSize = 16, Align = TextAnchor.MiddleCenter, Color = symbolColor, Font = "robotocondensed-bold.ttf" }, RectTransform = { AnchorMin = "0 0.5", AnchorMax = "1 0.95" } }, HUD_PANEL + "_LockBox"); } CuiHelper.AddUi(player, elements); } private void DestroyHUD(BasePlayer player) { if (player == null) return; CuiHelper.DestroyUi(player, HUD_PANEL); } #endregion #region Minigun System private void StartMinigunTimer(BasePlayer player, PlayerHelicopter mini) { if (player == null || mini == null) return; if (minigunTimersByPlayer.ContainsKey(player.userID)) return; var interval = Mathf.Max(0.01f, 1f / Mathf.Max(1f, config.MinigunRoundsPerSecond)); var timerHandle = timer.Every(interval, () => { if (player == null || !player.IsConnected) { StopMinigunTimer(player?.userID ?? 0); return; } if (!IsInMiniCopter(player, out PlayerHelicopter m, out BaseMountable s)) { StopMinigunTimer(player.userID); return; } var d = m.GetDriver(); if (d == null || d != player) { StopMinigunTimer(player.userID); return; } if (m.engineController == null || !m.engineController.IsOn) { StopMinigunTimer(player.userID); return; } var input = player.serverInput; if (input == null || !input.IsDown(BUTTON.FIRE_PRIMARY)) { StopMinigunTimer(player.userID); return; } ulong miniId = m.net.ID.Value; if (IsMinigunReloading(miniId)) { StopMinigunTimer(player.userID); return; } int currentAmmo = GetMinigunAmmo(miniId); if (currentAmmo <= 0) { StartMinigunReload(player, m, miniId); StopMinigunTimer(player.userID); return; } FireMinigunShot(player, m); minigunAmmoByMini[miniId] = currentAmmo - 1; }); minigunTimersByPlayer[player.userID] = timerHandle; } private void StopMinigunTimer(ulong userId) { if (userId == 0) return; if (minigunTimersByPlayer.TryGetValue(userId, out var t)) { try { t?.Destroy(); } catch { } minigunTimersByPlayer.Remove(userId); } } private void StartMinigunReload(BasePlayer player, PlayerHelicopter mini, ulong miniId) { if (IsMinigunReloading(miniId)) return; isMinigunReloading[miniId] = true; Effect.server.Run(config.MissileReloadStartSound, mini.transform.position, Vector3.up); player.ChatMessage($"Minigun reloading... {config.MinigunReloadTime}s"); if (minigunReloadTimers.TryGetValue(miniId, out var existingTimer)) { existingTimer?.Destroy(); } minigunReloadTimers[miniId] = timer.Once(config.MinigunReloadTime, () => { CompleteMinigunReload(miniId); }); } private void CompleteMinigunReload(ulong miniId) { if (!isMinigunReloading.ContainsKey(miniId) || !isMinigunReloading[miniId]) return; var mini = BaseNetworkable.serverEntities.Find(new NetworkableId(miniId)) as PlayerHelicopter; minigunAmmoByMini[miniId] = config.MinigunMagazineSize; isMinigunReloading[miniId] = false; if (mini != null) { Effect.server.Run(config.MissileReloadCompleteSound, mini.transform.position, Vector3.up); var driver = mini.GetDriver(); if (driver != null) { driver.ChatMessage($"Minigun reloaded! ({config.MinigunMagazineSize} rounds)"); } } minigunReloadTimers.Remove(miniId); } private int GetMinigunAmmo(ulong miniId) { if (!minigunAmmoByMini.TryGetValue(miniId, out int ammo)) { minigunAmmoByMini[miniId] = config.MinigunMagazineSize; return config.MinigunMagazineSize; } return ammo; } private bool IsMinigunReloading(ulong miniId) { return isMinigunReloading.TryGetValue(miniId, out bool reloading) && reloading; } private void FireMinigunShot(BasePlayer player, PlayerHelicopter mini) { try { Vector3 muzzlePos = GetMuzzlePosition(mini); Vector3 aimDir = player.eyes.HeadForward(); Vector3 aimTarget; if (Physics.Raycast(player.eyes.position, aimDir, out RaycastHit aimHit, config.MinigunRange, BulletLayerMask, QueryTriggerInteraction.Ignore)) { aimTarget = aimHit.point; } else { aimTarget = player.eyes.position + aimDir * config.MinigunRange; } Vector3 fireDirection = (aimTarget - muzzlePos).normalized; fireDirection = ApplyAimcone(fireDirection, config.MinigunAimconeDegrees); Vector3 bulletVelocity = fireDirection * config.MinigunBulletVelocity; if (config.InheritHelicopterVelocity && mini.rigidBody != null) { bulletVelocity += mini.rigidBody.velocity; } bool showTracer = config.MinigunEnableTracers && UnityEngine.Random.value < config.MinigunTracerChance; var bullet = new PendingBullet { Owner = player, Helicopter = mini, Origin = muzzlePos, Velocity = bulletVelocity, CurrentPosition = muzzlePos, LastPosition = muzzlePos, SpawnTime = Time.realtimeSinceStartup, BaseDamage = config.MinigunDamage, MaxLifetime = config.MinigunRange / config.MinigunBulletVelocity + 0.5f, ShowTracer = showTracer, TracerUpdateTime = 0f }; pendingBullets.Add(bullet); if (config.MinigunEnableMuzzleFlash) { Effect.server.Run(config.MinigunMuzzleFlashEffect, muzzlePos, fireDirection); } if (config.MinigunEnableSound) { Effect.server.Run(config.MinigunSoundEffect, muzzlePos, fireDirection); } if (showTracer) { ShowTracerEffect(muzzlePos, muzzlePos + fireDirection * 5f); } } catch (Exception e) { PrintError($"FireMinigunShot error: {e}"); } } private void ShowTracerEffect(Vector3 start, Vector3 end) { if (string.IsNullOrEmpty(config.MinigunTracerEffect)) return; try { Vector3 direction = (end - start).normalized; float distance = Vector3.Distance(start, end); Effect.server.Run(config.MinigunTracerEffect, start, direction); if (distance > 5f) { Vector3 midPoint = Vector3.Lerp(start, end, 0.5f); Effect.server.Run(config.MinigunTracerEffect, midPoint, direction); } } catch (Exception e) { if (config.DebugMode) { PrintWarning($"ShowTracerEffect error: {e.Message}"); } } } private void ProcessPendingBullets() { if (pendingBullets.Count == 0) return; float deltaTime = Time.deltaTime; float currentTime = Time.realtimeSinceStartup; for (int i = pendingBullets.Count - 1; i >= 0; i--) { var bullet = pendingBullets[i]; float age = currentTime - bullet.SpawnTime; if (age > bullet.MaxLifetime) { pendingBullets.RemoveAt(i); continue; } if (bullet.Owner == null || !bullet.Owner.IsConnected) { pendingBullets.RemoveAt(i); continue; } bullet.LastPosition = bullet.CurrentPosition; bullet.Velocity += Vector3.down * (9.81f * config.MinigunBulletGravity * deltaTime); bullet.CurrentPosition += bullet.Velocity * deltaTime; if (bullet.ShowTracer && currentTime - bullet.TracerUpdateTime > 0.05f) { ShowTracerEffect(bullet.LastPosition, bullet.CurrentPosition); bullet.TracerUpdateTime = currentTime; } float frameDistance = Vector3.Distance(bullet.LastPosition, bullet.CurrentPosition); if (frameDistance < 0.01f) continue; Vector3 direction = (bullet.CurrentPosition - bullet.LastPosition).normalized; if (Physics.Raycast(bullet.LastPosition, direction, out RaycastHit hit, frameDistance + 0.1f, BulletLayerMask, QueryTriggerInteraction.Ignore)) { BaseEntity hitEntity = hit.GetEntity(); if (hitEntity != null && (IsPartOfEntity(hitEntity, bullet.Helicopter) || hitEntity == bullet.Owner)) { continue; } if (config.DebugMode && hitEntity != null) { Puts($"[Debug] Bullet hit: {hitEntity.ShortPrefabName} at {hit.point}, Layer: {hit.collider.gameObject.layer}, Tag: {hit.collider.tag}"); } else if (config.DebugMode && hitEntity == null) { Puts($"[Debug] Bullet hit world collider at {hit.point}, Layer: {hit.collider.gameObject.layer}, Name: {hit.collider.name}"); } float totalDistance = Vector3.Distance(bullet.Origin, hit.point); float damage = CalculateDamageWithFalloff(bullet.BaseDamage, totalDistance); if (hitEntity != null) { var combatEntity = hitEntity as BaseCombatEntity; if (combatEntity != null && !combatEntity.IsDestroyed) { var hitInfo = new HitInfo(bullet.Owner, combatEntity, Rust.DamageType.Bullet, damage, hit.point); hitInfo.HitPositionWorld = hit.point; hitInfo.HitNormalWorld = hit.normal; hitInfo.PointStart = bullet.Origin; hitInfo.PointEnd = hit.point; if (config.HeadshotMultiplier && combatEntity is BasePlayer targetPlayer) { if (IsHeadshot(targetPlayer, hit.point)) { hitInfo.damageTypes.ScaleAll(config.HeadshotDamageMultiplier); } } combatEntity.OnAttacked(hitInfo); } } if (config.ExplosiveBulletsEnabled && HasPermission(bullet.Owner)) { if (UnityEngine.Random.value < config.ExplosiveBulletChance) { CreateExplosion(bullet.Owner, hit.point, hit.normal); } } if (config.MinigunEnableImpactEffects) { string effectPath = config.MinigunImpactEffect; if (hitEntity is BasePlayer || hitEntity is BaseNpc) { effectPath = config.MinigunFleshImpactEffect; } Effect.server.Run(effectPath, hit.point, hit.normal); } pendingBullets.RemoveAt(i); } else { float totalDistance = Vector3.Distance(bullet.Origin, bullet.CurrentPosition); if (totalDistance > config.MinigunRange) { pendingBullets.RemoveAt(i); } } } } #endregion #region Missile System private void TryFireMissiles(BasePlayer player, PlayerHelicopter mini) { try { float now = Time.realtimeSinceStartup; ulong miniId = mini.net.ID.Value; if (IsReloadingMissiles(miniId)) { return; } if (lastMissileFire.TryGetValue(player.userID, out float last)) { if (now - last < config.MissileCooldown) { return; } } lastMissileFire[player.userID] = now; FireMissile(player, mini); if (!missileCountByMini.ContainsKey(miniId)) missileCountByMini[miniId] = 0; missileCountByMini[miniId]++; int remaining = config.MaxMissilesPerReload - missileCountByMini[miniId]; if (remaining <= 0) { StartMissileReload(player, mini, miniId); } } catch (Exception e) { PrintError($"TryFireMissiles error: {e}"); } } private void FireMissile(BasePlayer player, PlayerHelicopter mini) { try { ulong miniId = mini.net.ID.Value; Vector3 forward = mini.transform.forward; Vector3 right = mini.transform.right; Vector3 up = mini.transform.up; LockOnState state = null; bool isLocked = false; if (config.EnableLockOn && lockOnByPlayer.TryGetValue(player.userID, out state)) { isLocked = state.IsLocked && state.TargetHelicopter != null && !state.TargetHelicopter.IsDestroyed; } string prefabToUse = isLocked ? config.HomingRocketPrefab : config.RocketPrefab; float speed = config.MissileSpeed; bool isLeft = true; if (nextRocketLeftByMini.ContainsKey(miniId)) { isLeft = !nextRocketLeftByMini[miniId]; } nextRocketLeftByMini[miniId] = isLeft; float sideOffset = isLocked ? config.RocketOffsetSide * 1.5f : config.RocketOffsetSide; Vector3 launchPos = mini.transform.position + (forward * config.RocketOffsetForward) + (right * (isLeft ? -sideOffset : sideOffset)) + (up * config.RocketOffsetUp); BaseEntity rocketEntity = GameManager.server.CreateEntity(prefabToUse, launchPos, Quaternion.LookRotation(forward)); if (rocketEntity == null) return; ServerProjectile projectile = rocketEntity.GetComponent(); if (projectile != null) { projectile.InitializeVelocity(forward * speed); if (isLocked) { activeMissiles.Add(new ActiveHomingMissile { Missile = projectile, Target = state.TargetHelicopter, LaunchTime = Time.realtimeSinceStartup }); } } rocketEntity.Spawn(); } catch (Exception e) { PrintError($"FireMissile error: {e}"); } } private void ProcessHomingMissiles() { if (activeMissiles.Count == 0) return; for (int i = activeMissiles.Count - 1; i >= 0; i--) { var homing = activeMissiles[i]; if (homing.Missile == null || homing.Target == null || homing.Target.IsDestroyed || homing.Missile.gameObject == null) { activeMissiles.RemoveAt(i); continue; } Vector3 targetPos = homing.Target.transform.position; Vector3 missilePos = homing.Missile.transform.position; float distToTarget = Vector3.Distance(targetPos, missilePos); if (distToTarget < 3.0f) { var explosive = homing.Missile.GetComponent(); if (explosive != null) { explosive.Explode(); } else { var entity = homing.Missile.GetComponent(); if (entity != null) entity.Kill(); } activeMissiles.RemoveAt(i); continue; } Vector3 targetVel = Vector3.zero; if (homing.Target is BaseCombatEntity combatEnt) targetVel = combatEnt.GetLocalVelocity(); float leadTime = distToTarget / config.MissileSpeed; Vector3 predictedPos = targetPos + (targetVel * leadTime * 0.8f); Vector3 directionToTarget = (predictedPos - missilePos).normalized; Vector3 currentVelocity = homing.Missile.CurrentVelocity; float currentSpeed = config.MissileSpeed; homing.Missile.gravityModifier = 0f; homing.Missile.drag = 0f; float turnRate = 8.0f * Time.deltaTime; Vector3 newDirection = Vector3.Slerp(currentVelocity.normalized, directionToTarget, turnRate); homing.Missile.CurrentVelocity = newDirection * currentSpeed; homing.Missile.transform.rotation = Quaternion.LookRotation(newDirection); if (Time.realtimeSinceStartup - homing.LaunchTime > 10f) { activeMissiles.RemoveAt(i); } } } private void StartMissileReload(BasePlayer player, PlayerHelicopter mini, ulong miniId) { isMissileReloading[miniId] = true; Effect.server.Run(config.MissileReloadStartSound, mini.transform.position, Vector3.up); if (missileReloadTimers.TryGetValue(miniId, out var existingTimer)) { existingTimer?.Destroy(); } missileReloadTimers[miniId] = timer.Once(config.MissileReloadCooldown, () => { CompleteMissileReload(miniId); }); } private void CompleteMissileReload(ulong miniId) { try { if (!isMissileReloading.ContainsKey(miniId) || !isMissileReloading[miniId]) return; var mini = BaseNetworkable.serverEntities.Find(new NetworkableId(miniId)) as PlayerHelicopter; if (mini == null) { CleanupMinicopter(miniId); return; } missileCountByMini[miniId] = 0; isMissileReloading[miniId] = false; Effect.server.Run(config.MissileReloadCompleteSound, mini.transform.position, Vector3.up); var driver = mini.GetDriver(); if (driver != null) { } missileReloadTimers.Remove(miniId); } catch (Exception e) { PrintError($"CompleteMissileReload error: {e}"); } } private int GetMissilesRemaining(ulong miniId) { if (!missileCountByMini.TryGetValue(miniId, out int fired)) fired = 0; return config.MaxMissilesPerReload - fired; } private bool IsReloadingMissiles(ulong miniId) { return isMissileReloading.TryGetValue(miniId, out bool reloading) && reloading; } #endregion #region Physics & Ballistics Helpers private Vector3 ApplyAimcone(Vector3 direction, float aimconeDegrees) { if (aimconeDegrees <= 0f) return direction.normalized; float aimconeRad = aimconeDegrees * Mathf.Deg2Rad; float z = UnityEngine.Random.Range(Mathf.Cos(aimconeRad), 1f); float theta = UnityEngine.Random.Range(0f, 2f * Mathf.PI); float sqrtPart = Mathf.Sqrt(1f - z * z); Vector3 randomDir = new Vector3( sqrtPart * Mathf.Cos(theta), sqrtPart * Mathf.Sin(theta), z ); return Quaternion.LookRotation(direction) * randomDir; } private float CalculateDamageWithFalloff(float baseDamage, float distance) { if (distance <= config.MinigunDamageFalloffStart) return baseDamage; if (distance >= config.MinigunDamageFalloffEnd) return baseDamage * config.MinigunMinDamageMultiplier; float t = (distance - config.MinigunDamageFalloffStart) / (config.MinigunDamageFalloffEnd - config.MinigunDamageFalloffStart); return Mathf.Lerp(baseDamage, baseDamage * config.MinigunMinDamageMultiplier, t); } private bool IsHeadshot(BasePlayer target, Vector3 hitPoint) { if (target == null) return false; Vector3 headPos = target.eyes.position; float headRadius = 0.2f; return Vector3.Distance(hitPoint, headPos) <= headRadius; } private Vector3 GetMuzzlePosition(PlayerHelicopter mini) { return mini.transform.position + mini.transform.forward * config.MinigunMuzzleForward + mini.transform.up * config.MinigunMuzzleUp + mini.transform.right * config.MinigunMuzzleSide; } #endregion #region Permission Helpers private bool HasPermission(BasePlayer player) { if (!config.RequirePermission) return true; return permission.UserHasPermission(player.UserIDString, PermissionUse); } private void CreateExplosion(BasePlayer owner, Vector3 position, Vector3 normal) { try { Effect.server.Run(config.ExplosiveBulletEffect, position, normal); var colliders = Pool.Get>(); Vis.Colliders(position, config.ExplosiveBulletRadius, colliders, BulletLayerMask, QueryTriggerInteraction.Ignore); foreach (var collider in colliders) { if (collider == null) continue; var entity = collider.gameObject.ToBaseEntity(); if (entity == null || entity == owner) continue; if (IsInMiniCopter(owner, out PlayerHelicopter mini, out BaseMountable seat)) { if (IsPartOfEntity(entity, mini)) continue; } var combatEntity = entity as BaseCombatEntity; if (combatEntity == null || combatEntity.IsDestroyed) continue; float distance = Vector3.Distance(position, combatEntity.transform.position); float damageMultiplier = 1f - (distance / config.ExplosiveBulletRadius); damageMultiplier = Mathf.Clamp01(damageMultiplier); float damage = config.ExplosiveBulletDamage * damageMultiplier; var hitInfo = new HitInfo(owner, combatEntity, Rust.DamageType.Explosion, damage, position); hitInfo.HitPositionWorld = position; combatEntity.OnAttacked(hitInfo); } Pool.FreeUnmanaged(ref colliders); } catch (Exception e) { if (config.DebugMode) { PrintWarning($"CreateExplosion error: {e.Message}"); } } } #endregion #region Utility Methods private bool IsInMiniCopter(BasePlayer player, out PlayerHelicopter mini, out BaseMountable seat) { mini = null; seat = player.GetMounted() as BaseMountable; if (seat == null) return false; var vehicle = seat.VehicleParent() as BaseVehicle; var heli = vehicle as PlayerHelicopter; if (heli != null && !heli.IsDestroyed && IsMiniCopterEntity(heli)) { mini = heli; return true; } var mounted = player.GetMountedVehicle(); heli = mounted as PlayerHelicopter; if (heli != null && !heli.IsDestroyed && IsMiniCopterEntity(heli)) { mini = heli; return true; } return false; } private bool IsMiniCopterEntity(BaseEntity entity) { if (entity == null) return false; var shortName = entity.ShortPrefabName; return shortName.Contains("minicopter"); } private bool IsPartOfEntity(BaseEntity candidate, BaseEntity root) { if (root == null) return false; var current = candidate; while (current != null) { if (current == root) return true; current = current.GetParentEntity(); } return false; } private void CleanupMinicopter(ulong miniId) { nextRocketLeftByMini.Remove(miniId); missileCountByMini.Remove(miniId); isMissileReloading.Remove(miniId); minigunAmmoByMini.Remove(miniId); isMinigunReloading.Remove(miniId); if (missileReloadTimers.TryGetValue(miniId, out var t)) { t?.Destroy(); missileReloadTimers.Remove(miniId); } if (minigunReloadTimers.TryGetValue(miniId, out var t2)) { t2?.Destroy(); minigunReloadTimers.Remove(miniId); } bombCountByMini.Remove(miniId); isBombReloading.Remove(miniId); lastBombDrop.Remove(miniId); lastManualReload.Remove(miniId); if (bombReloadTimers.TryGetValue(miniId, out var t3)) { t3?.Destroy(); bombReloadTimers.Remove(miniId); } } #endregion } }