C#:
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<ulong, Timer> minigunTimersByPlayer = new Dictionary<ulong, Timer>();
private readonly Dictionary<ulong, float> lastMissileFire = new Dictionary<ulong, float>();
private readonly Dictionary<ulong, bool> nextRocketLeftByMini = new Dictionary<ulong, bool>();
private readonly Dictionary<ulong, int> missileCountByMini = new Dictionary<ulong, int>();
private readonly Dictionary<ulong, bool> isMissileReloading = new Dictionary<ulong, bool>();
private readonly Dictionary<ulong, Timer> missileReloadTimers = new Dictionary<ulong, Timer>();
private readonly Dictionary<ulong, int> minigunAmmoByMini = new Dictionary<ulong, int>();
private readonly Dictionary<ulong, bool> isMinigunReloading = new Dictionary<ulong, bool>();
private readonly Dictionary<ulong, Timer> minigunReloadTimers = new Dictionary<ulong, Timer>();
private readonly Dictionary<ulong, Timer> hudUpdateTimers = new Dictionary<ulong, Timer>();
private readonly Dictionary<ulong, LockOnState> lockOnByPlayer = new Dictionary<ulong, LockOnState>();
private readonly Dictionary<ulong, BoostState> boostByMini = new Dictionary<ulong, BoostState>();
private readonly HashSet<ulong> boostedMinisVulnerable = new HashSet<ulong>();
private readonly Dictionary<ulong, int> bombCountByMini = new Dictionary<ulong, int>();
private readonly Dictionary<ulong, bool> isBombReloading = new Dictionary<ulong, bool>();
private readonly Dictionary<ulong, Timer> bombReloadTimers = new Dictionary<ulong, Timer>();
private readonly Dictionary<ulong, float> lastBombDrop = new Dictionary<ulong, float>();
private readonly Dictionary<ulong, float> lastManualReload = new Dictionary<ulong, float>();
private readonly HashSet<BasePlayer> activePilots = new HashSet<BasePlayer>();
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<ulong, IncomingLockState> incomingLockByMini = new Dictionary<ulong, IncomingLockState>();
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<ActiveHomingMissile> activeMissiles = new List<ActiveHomingMissile>();
private readonly List<PlayerHelicopter> activeMinicopters = new List<PlayerHelicopter>();
private readonly List<PendingBullet> pendingBullets = new List<PendingBullet>();
private readonly HashSet<BaseEntity> activeBombs = new HashSet<BaseEntity>();
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<PluginConfig>();
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<PluginConfig>();
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($"<color=#ffaa00>Boost depleted! Recharging... {config.BoostRechargeTime}s</color>");
}
}
}
}
private void ProcessBoostPhysics()
{
float dt = Time.deltaTime;
float currentTime = Time.realtimeSinceStartup;
List<ulong> 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<ulong>();
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($"<color=#00ff00>Boost recharged!</color>");
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($"<color=#ffaa00>Boost depleted! Recharging... {config.BoostRechargeTime}s</color>");
}
}
}
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<ServerProjectile>();
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($"<color=#00ff00>Bombs reloaded! ({config.MaxBombsPerReload} ready)</color>");
}
}
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($"<color=#ffaa00>Manual reload on cooldown ({remaining:F1}s)</color>");
return;
}
}
bool anyReloading = IsMinigunReloading(miniId) || IsReloadingMissiles(miniId) || IsBombReloading(miniId);
if (anyReloading)
{
player.ChatMessage("<color=#ffaa00>Systems are already reloading!</color>");
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("<color=#88ff88>All systems already at full capacity!</color>");
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($"<color=#ffaa00>Reloading depleted systems... {config.ManualReloadCooldown}s</color>");
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("<color=#00ff00>Systems reloaded!</color>");
}
});
}
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 = "<color=#ffaa00>RELOADING</color>";
}
else
{
string bulletColor = bulletsRemaining > 30 ? "#88ff88" : (bulletsRemaining > 10 ? "#ffcc00" : "#ff4444");
bulletValue = $"<color={bulletColor}>{bulletsRemaining}</color>";
}
elements.Add(new CuiLabel
{
Text = {
Text = $"<color=#cccccc>Bullets:</color> {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 = "<color=#ffaa00>RELOADING</color>";
}
else
{
string rocketColor = missilesRemaining > 2 ? "#ff8844" : (missilesRemaining > 0 ? "#ffcc00" : "#ff4444");
rocketValue = $"<color={rocketColor}>{missilesRemaining}</color>";
}
elements.Add(new CuiLabel
{
Text = {
Text = $"<color=#cccccc>Rockets:</color> {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 = "<color=#ffaa00>RELOADING</color>";
}
else
{
string bombColor = bombsRemaining > 2 ? "#aa44ff" : (bombsRemaining > 0 ? "#ffcc00" : "#ff4444");
bombValue = $"<color={bombColor}>{bombsRemaining}</color>";
}
elements.Add(new CuiLabel
{
Text = {
Text = $"<color=#cccccc>Bombs:</color> {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 = "<color=#ffaa00>RECHARGING</color>";
}
else
{
float boostPercent = (boostFuel / config.BoostFuelDuration) * 100f
// ... [Code preview truncated due to length. Full source in attached file] ...