using System; using System.Collections; using System.Collections.Generic; using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Plugins; using UnityEngine; using System.Linq; // ____ _ _ _ // / ___|(_) __ _(_) | ___ // \___ \| |/ _` | | |/ _ \ // ___) | | (_| | | | (_) | // |____/|_|\__, |_|_|\___/ // |___/ // ☽✧✵✧✵✧✵✧✵✧✵✧✵✧☾ // ✦ RUST PLUGINS ✦ // ✦ Sigilo.dev ✦ // ☽✧✵✧✵✧✵✧✵✧✵✧✵✧☾ namespace Oxide.Plugins { [Info("Comet", "Sigilo", "1.1.2")] class Comet : RustPlugin { const bool en = true; static Comet ins; private List activeComets = new List(); private List activeCometComponents = new List(); private static readonly WaitForSeconds Wait0_05 = new WaitForSeconds(0.05f); private static readonly WaitForSeconds Wait0_1 = new WaitForSeconds(0.1f); private static readonly Dictionary WaitDictionary = new Dictionary(); private static WaitForSeconds GetWait(float time) { if (time <= 0) return null; WaitForSeconds wait; if (WaitDictionary.TryGetValue(time, out wait)) return wait; wait = new WaitForSeconds(time); WaitDictionary[time] = wait; return wait; } #region Permissions const string PERMISSION_ADMIN = "comet.admin"; #endregion #region Signal System private const string SUPPLY_SIGNAL_PREFAB = "assets/prefabs/tools/supply signal/supplysignal.prefab"; private const ulong COMET_SIGNAL_SKIN_ID = 2791026167; private StoredData storedData; private class StoredData { public Dictionary Cooldowns { get; set; } = new Dictionary(); } void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(Name, storedData); void LoadData() { try { storedData = Interface.Oxide.DataFileSystem.ReadObject(Name) ?? new StoredData(); CleanupExpiredCooldowns(); } catch { storedData = new StoredData(); } } void OnServerSave() => SaveData(); private void CleanupExpiredCooldowns() { if (storedData?.Cooldowns == null) return; double currentTime = GetCurrentTime(); foreach (var userId in storedData.Cooldowns .Where(kvp => (currentTime - kvp.Value) >= _config.signalConfig.CooldownSeconds) .Select(kvp => kvp.Key)) { storedData.Cooldowns.Remove(userId); } SaveData(); } private bool IsOnCooldown(BasePlayer player) { if (storedData?.Cooldowns == null || !storedData.Cooldowns.TryGetValue(player.userID, out double lastUsage)) return false; double timeSinceLastUse = GetCurrentTime() - lastUsage; float cooldownTime = GetPlayerCooldownTime(player); return timeSinceLastUse < cooldownTime; } private double GetRemainingCooldown(BasePlayer player) { if (storedData?.Cooldowns == null || !storedData.Cooldowns.TryGetValue(player.userID, out double lastUsage)) return 0; double timeSinceLastUse = GetCurrentTime() - lastUsage; float cooldownTime = GetPlayerCooldownTime(player); double remainingTime = cooldownTime - timeSinceLastUse; return remainingTime > 0 ? remainingTime : 0; } private float GetPlayerCooldownTime(BasePlayer player) { return _config.signalConfig.CooldownSeconds; } private void SetCooldown(BasePlayer player) { if (storedData?.Cooldowns == null) storedData = new StoredData(); storedData.Cooldowns[player.userID] = GetCurrentTime(); SaveData(); } private double GetCurrentTime() => DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds; private void ResetCooldown(BasePlayer player) { if (storedData?.Cooldowns == null) return; storedData.Cooldowns.Remove(player.userID); SaveData(); SendReply(player, "Your Comet Signal cooldown has been reset."); } private void OnExplosiveThrown(BasePlayer player, BaseEntity entity) { SupplySignal signal = entity as SupplySignal; if (signal == null || signal.skinID != COMET_SIGNAL_SKIN_ID) return; Puts($"COMET: Player {player.displayName} threw a comet signal"); signal.CancelInvoke(signal.Explode); if (IsOnCooldown(player)) { double remainingMinutes = Math.Ceiling(GetRemainingCooldown(player) / 60.0); string cooldownMessage = $"You must wait {remainingMinutes} minutes before using another Comet Signal."; SendReply(player, cooldownMessage); ReturnCometSignal(player, signal); return; } SetCooldown(player); if (signal != null && !signal.IsDestroyed) { signal.CancelInvoke(signal.KillMessage); signal.Invoke(signal.KillMessage, 60f); timer.Once(3f, () => { if (signal == null || signal.IsDestroyed) { Puts("COMET: Signal was destroyed before it could land"); return; } Vector3 signalPosition = signal.transform.position; Puts($"COMET: Signal has landed at position: {signalPosition}"); SpawnFlareMarker(signalPosition); timer.Repeat(1.5f, (int)Math.Ceiling(_config.signalConfig.WarmupTime / 1.5f), () => { if (signal != null && !signal.IsDestroyed) { Effect.server.Run("assets/bundled/prefabs/fx/fire/fire_v2.prefab", signalPosition); } }); foreach (var nearbyPlayer in BasePlayer.activePlayerList.Where(p => Vector3.Distance(p.transform.position, signalPosition) <= 100f)) { SendReply(nearbyPlayer, $"A Comet has been called! Impact in {_config.signalConfig.WarmupTime} seconds. Take cover!"); } timer.Once(_config.signalConfig.WarmupTime, () => { Puts($"COMET: Spawning comet at exact position: {signalPosition}"); foreach (var nearbyPlayer in BasePlayer.activePlayerList.Where(p => Vector3.Distance(p.transform.position, signalPosition) <= 150f)) { SendReply(nearbyPlayer, "Comet incoming! TAKE COVER NOW!"); } SpawnComet(signalPosition); if (signal != null && !signal.IsDestroyed) signal.Kill(); }); }); } } private void SpawnFlareMarker(Vector3 position) { BaseEntity flareEntity = GameManager.server.CreateEntity("assets/prefabs/tools/flareold/flare.deployed.prefab", position, Quaternion.identity); if (flareEntity != null) { flareEntity.Spawn(); timer.Once(_config.signalConfig.WarmupTime + 10f, () => { if (flareEntity != null && !flareEntity.IsDestroyed) flareEntity.Kill(); }); } Effect.server.Run("assets/bundled/prefabs/fx/fire/fire_v3.prefab", position, Vector3.up, null, true); } private void ReturnCometSignal(BasePlayer player, SupplySignal signal) { Item item = ItemManager.CreateByName("supply.signal", 1, COMET_SIGNAL_SKIN_ID); if (item != null) { item.name = _config.signalConfig.DisplayName; player.GiveItem(item); } if (signal != null && !signal.IsDestroyed) signal.Kill(); } [ChatCommand("givecometcaller")] void GiveCometSignalCommand(BasePlayer player, string command, string[] args) { if (!permission.UserHasPermission(player.UserIDString, PERMISSION_ADMIN)) { SendReply(player, "You do not have permission to use this command."); return; } BasePlayer targetPlayer = player; if (args.Length > 0 && args[0].ToLower() != "me") { if (args[0].ToLower() == "reset") { if (args.Length > 1) { var target = BasePlayer.Find(args[1]); if (target == null) { SendReply(player, "Player not found."); return; } ResetCooldown(target); SendReply(player, $"You've reset the cooldown for {target.displayName}."); } else { ResetCooldown(player); } return; } targetPlayer = BasePlayer.Find(args[0]); if (targetPlayer == null) { SendReply(player, "Player not found."); return; } args = args.Skip(1).ToArray(); } int amount = 1; if (args.Length > 0) { if (!int.TryParse(args[0], out amount) || amount <= 0) { SendReply(player, "Invalid amount. Please specify a positive number."); return; } } GivePlayerCometSignal(targetPlayer, amount); if (targetPlayer == player) SendReply(player, $"You've received {amount} Comet Signal{(amount > 1 ? "s" : "")}."); else { SendReply(player, $"You've given {amount} Comet Signal{(amount > 1 ? "s" : "")} to {targetPlayer.displayName}."); SendReply(targetPlayer, $"You've received {amount} Comet Signal{(amount > 1 ? "s" : "")} from an admin."); } } void GivePlayerCometSignal(BasePlayer player, int amount = 1) { Item item = ItemManager.CreateByName("supply.signal", amount, COMET_SIGNAL_SKIN_ID); if (item != null) { item.name = _config.signalConfig.DisplayName; item.MarkDirty(); player.GiveItem(item); } } #endregion #region Hooks void Init() { ins = this; try { Puts("Initializing Comet plugin..."); permission.RegisterPermission(PERMISSION_ADMIN, this); LoadConfig(); LoadData(); _config = Config.ReadObject(); if (_config == null) { Puts("Config is null, loading default config..."); LoadDefaultConfig(); _config = Config.ReadObject(); } if (_config.cometConfig == null) { Puts("Comet config is null, loading default config..."); _config = PluginConfig.DefaultConfig(); SaveConfig(); } if (_config.signalConfig == null) { Puts("Signal config is null, updating config..."); _config.signalConfig = PluginConfig.DefaultConfig().signalConfig; SaveConfig(); } Puts("Comet plugin initialized successfully!"); } catch (Exception ex) { PrintError("Error initializing Comet plugin: " + ex.Message); LoadDefaultConfig(); _config = Config.ReadObject(); permission.RegisterPermission(PERMISSION_ADMIN, this); } } void OnServerInitialized() { if (ins == null) ins = this; if (storedData == null) LoadData(); CleanupExpiredCooldowns(); } void Unload() { try { if (activeComets != null && activeComets.Count > 0) { Puts($"Cleaning up {activeComets.Count} tracked comet objects on plugin unload"); for (int i = activeComets.Count - 1; i >= 0; i--) { var cometObj = activeComets[i]; if (cometObj != null) { UnityEngine.Object.Destroy(cometObj); } activeComets.RemoveAt(i); } } if (activeCometComponents != null && activeCometComponents.Count > 0) { Puts($"Cleaning up {activeCometComponents.Count} comet components on plugin unload"); for (int i = activeCometComponents.Count - 1; i >= 0; i--) { var component = activeCometComponents[i]; if (component != null) { component.CleanupOnPluginUnload(); UnityEngine.Object.Destroy(component); } activeCometComponents.RemoveAt(i); } } } catch (Exception ex) { Debug.LogError($"Error during Comet plugin unload: {ex}"); } finally { ins = null; } } #endregion #region Commands [ChatCommand("comet")] void SummonCometCommand(BasePlayer player, string command, string[] args) { if (!permission.UserHasPermission(player.UserIDString, PERMISSION_ADMIN)) { SendReply(player, "You do not have permission to use this command."); return; } Vector3 position; if (args.Length > 0 && args[0].ToLower() == "here") { RaycastHit hit; if (Physics.Raycast(player.eyes.HeadRay(), out hit, 1000f, LayerMask.GetMask("Terrain", "World", "Construction"))) { position = hit.point; } else { return; } } else { position = player.transform.position; } SpawnComet(position); SendReply(player, "A comet is falling from the sky"); } [ConsoleCommand("comet.summon")] void ConsoleSummonComet(ConsoleSystem.Arg arg) { BasePlayer player = arg.Player(); if (player != null && !permission.UserHasPermission(player.UserIDString, PERMISSION_ADMIN)) { SendReply(player, "You do not have permission to use this command."); return; } Vector3 position; if (arg.Args != null && arg.Args.Length >= 3) { float x, y, z; if (float.TryParse(arg.Args[0], out x) && float.TryParse(arg.Args[1], out y) && float.TryParse(arg.Args[2], out z)) { position = new Vector3(x, y, z); } else { if (player != null) SendReply(player, "Invalid position coordinates. Format: comet.summon "); else Puts("Invalid position coordinates. Format: comet.summon "); return; } } else if (player != null) { position = player.transform.position; } else { Puts("When using from server console, you must specify coordinates: comet.summon "); return; } SpawnComet(position); if (player != null) SendReply(player, "A comet is falling from the sky"); else Puts("A comet is falling from the sky"); } [ConsoleCommand("comet.give")] void ConsoleGiveCometSignal(ConsoleSystem.Arg arg) { BasePlayer caller = arg.Player(); if (caller != null && !permission.UserHasPermission(caller.UserIDString, PERMISSION_ADMIN)) { SendReply(caller, "You do not have permission to use this command."); return; } if (arg.Args == null || arg.Args.Length < 1) { string message = "Usage: comet.give [amount=1]"; if (caller != null) SendReply(caller, message); else Puts(message); return; } BasePlayer targetPlayer = null; string playerNameOrId = arg.Args[0]; if (playerNameOrId.All(char.IsDigit)) { targetPlayer = BasePlayer.FindByID(ulong.Parse(playerNameOrId)); } if (targetPlayer == null) { targetPlayer = BasePlayer.Find(playerNameOrId); } if (targetPlayer == null) { string message = $"Player '{playerNameOrId}' not found."; if (caller != null) SendReply(caller, message); else Puts(message); return; } int amount = 1; if (arg.Args.Length > 1 && (!int.TryParse(arg.Args[1], out amount) || amount <= 0)) { string message = "Invalid amount. Please specify a positive number."; if (caller != null) SendReply(caller, message); else Puts(message); return; } GivePlayerCometSignal(targetPlayer, amount); string successMessage = $"Given {amount} Comet Signal{(amount > 1 ? "s" : "")} to {targetPlayer.displayName}."; if (caller != null) SendReply(caller, successMessage); else Puts(successMessage); SendReply(targetPlayer, $"You've received {amount} Comet Signal{(amount > 1 ? "s" : "")}."); } [ConsoleCommand("comet.giveall")] void ConsoleGiveAllCometSignal(ConsoleSystem.Arg arg) { BasePlayer caller = arg.Player(); if (caller != null && !permission.UserHasPermission(caller.UserIDString, PERMISSION_ADMIN)) { SendReply(caller, "You do not have permission to use this command."); return; } int amount = 1; if (arg.Args != null && arg.Args.Length > 0) { if (!int.TryParse(arg.Args[0], out amount) || amount <= 0) { string message = "Invalid amount. Please specify a positive number."; if (caller != null) SendReply(caller, message); else Puts(message); return; } } int playerCount = 0; foreach (var player in BasePlayer.activePlayerList) { GivePlayerCometSignal(player, amount); playerCount++; SendReply(player, $"You've received {amount} Comet Signal{(amount > 1 ? "s" : "")}."); } string successMessage = $"Given {amount} Comet Signal{(amount > 1 ? "s" : "")} to {playerCount} online players."; if (caller != null) SendReply(caller, successMessage); else Puts(successMessage); } #endregion #region Comet Logic void SpawnComet(Vector3 impactPosition) { try { if (ins == null) ins = this; GameObject cometObj = new GameObject("Comet"); activeComets.Add(cometObj); CometFall cometFall = cometObj.AddComponent(); activeCometComponents.Add(cometFall); Vector3 startPosition = FindStartFallPosition(impactPosition); float horizontalDistance = Mathf.Sqrt( (startPosition.x - impactPosition.x) * (startPosition.x - impactPosition.x) + (startPosition.z - impactPosition.z) * (startPosition.z - impactPosition.z) ); float verticalDistance = startPosition.y - impactPosition.y; float trajectoryAngle = Mathf.Atan2(verticalDistance, horizontalDistance) * Mathf.Rad2Deg; Puts($"Comet spawn info:"); Puts($" - Target position: {impactPosition.x}, {impactPosition.y}, {impactPosition.z}"); Puts($" - Start position: {startPosition.x}, {startPosition.y}, {startPosition.z}"); Puts($" - Horizontal offset: {_config.cometConfig.horizontalOffset}"); Puts($" - Horizontal distance: {horizontalDistance}"); Puts($" - Vertical distance: {verticalDistance}"); Puts($" - Trajectory angle: {trajectoryAngle}° from horizontal"); Vector3 direction = (impactPosition - startPosition).normalized; Puts($" - Direction: {direction.x}, {direction.y}, {direction.z}"); cometFall.Init(impactPosition, startPosition); } catch (Exception ex) { PrintError("Error spawning comet: " + ex.Message); } } Vector3 FindStartFallPosition(Vector3 impactPosition) { float horizontalOffset = _config.cometConfig.horizontalOffset; float fallHeight = UnityEngine.Random.Range(_config.cometConfig.minFallHeight, _config.cometConfig.maxFallHeight); Vector3 startPos = new Vector3( impactPosition.x + horizontalOffset, impactPosition.y + fallHeight, impactPosition.z); return startPos; } #endregion #region Comet Class class CometFall : FacepunchBehaviour { Vector3 startPosition; Vector3 impactPosition; Vector3 fallDirection; Coroutine cometFallCoroutine; List cometEntities = new List(); List impactEntities = new List(); float impactRadius; float explosionDamage; float currentFallTime = 0f; float totalFallTime = 0f; bool impactOccurred = false; internal void CleanupOnPluginUnload() { try { if (cometFallCoroutine != null && ServerMgr.Instance != null) { ServerMgr.Instance.StopCoroutine(cometFallCoroutine); cometFallCoroutine = null; } CancelInvoke(); foreach (BaseEntity entity in cometEntities) { if (entity != null && !entity.IsDestroyed) { entity.Kill(); } } cometEntities.Clear(); foreach (BaseEntity entity in impactEntities) { if (entity != null && !entity.IsDestroyed) { entity.Kill(); } } impactEntities.Clear(); } catch (Exception ex) { Debug.LogError($"Error cleaning up CometFall component: {ex}"); } } internal void Init(Vector3 impactPosition, Vector3 startPosition) { try { this.impactPosition = impactPosition; this.startPosition = startPosition; if (ins == null) { throw new Exception("Plugin instance is null"); } if (ins._config == null) { ins._config = PluginConfig.DefaultConfig(); throw new Exception("Plugin config is null, created default config"); } if (ins._config.cometConfig == null) { ins._config.cometConfig = PluginConfig.DefaultConfig().cometConfig; throw new Exception("Comet config is null, created default values"); } this.impactRadius = ins._config.cometConfig.impactRadius; this.explosionDamage = ins._config.cometConfig.explosionDamage; DefineFallDirection(); CreateCometEntities(); float distance = Vector3.Distance(startPosition, impactPosition); totalFallTime = distance / (0.5f * ins._config.cometConfig.fallingSpeedScale); if (ServerMgr.Instance != null) cometFallCoroutine = ServerMgr.Instance.StartCoroutine(CometFallCoroutine()); else throw new Exception("ServerMgr.Instance is null"); InvokeRepeating(CreatePeriodicEffects, 0.1f, 0.1f); } catch (Exception ex) { Debug.LogError("Error in CometFall.Init: " + ex.Message); throw; } } void DefineFallDirection() { fallDirection = (impactPosition - startPosition).normalized; } void CreateCometEntities() { ins.Puts($"Creating comet entities at position: {startPosition}"); cometEntities.Clear(); BaseEntity mainEntity = GameManager.server.CreateEntity("assets/bundled/prefabs/oilfireballsmall.prefab", startPosition, Quaternion.identity); if (mainEntity != null) { mainEntity.Spawn(); mainEntity.transform.position = startPosition; cometEntities.Add(mainEntity); ins.Puts($"Main comet entity created at {mainEntity.transform.position}"); } else { ins.Puts("Failed to create main comet entity!"); } int flareCount = Mathf.RoundToInt(ins._config.cometConfig.initialExplosionCount * 0.2f); for (int i = 0; i < flareCount; i++) { Vector3 offset = new Vector3( UnityEngine.Random.Range(-3f, 3f), UnityEngine.Random.Range(-3f, 3f), UnityEngine.Random.Range(-3f, 3f) ); Vector3 flarePosition = startPosition + offset; BaseEntity fireEntity = GameManager.server.CreateEntity("assets/prefabs/tools/flareold/flare.deployed.prefab", flarePosition, Quaternion.identity); if (fireEntity != null) { fireEntity.Spawn(); fireEntity.transform.position = flarePosition; cometEntities.Add(fireEntity); } } ins.Puts($"Total comet entities created: {cometEntities.Count}"); } void CreatePeriodicEffects() { if (cometEntities.Count == 0 || cometEntities[0] == null || cometEntities[0].IsDestroyed || impactOccurred) { CancelInvoke(CreatePeriodicEffects); return; } Effect.server.Run("assets/bundled/prefabs/fx/fire/fire_v2.prefab", cometEntities[0].transform.position, Vector3.up); if (UnityEngine.Random.Range(0, 100) < (ins._config.cometConfig.postImpactEffectCount * 10)) { Vector3 offsetPos = cometEntities[0].transform.position + new Vector3( UnityEngine.Random.Range(-3f, 3f), UnityEngine.Random.Range(-3f, 3f), UnityEngine.Random.Range(-3f, 3f) ); Effect.server.Run("assets/prefabs/npc/patrol helicopter/effects/heli_explosion.prefab", offsetPos, Vector3.up); } Effect.server.Run("assets/bundled/prefabs/fx/fire/fire_v3.prefab", cometEntities[0].transform.position - (fallDirection * 3f), Vector3.up); } IEnumerator CometFallCoroutine() { if (ins == null || ins._config == null || ins._config.cometConfig == null) { Debug.LogError("CometFall: Plugin instance or config is null"); yield break; } ins.Puts($"Starting comet fall from {startPosition} to {impactPosition}"); ins.Puts($"Direction vector: {fallDirection}"); Vector3 totalDistanceVector = impactPosition - startPosition; float totalDistance = totalDistanceVector.magnitude; float horizontalDistance = Mathf.Sqrt( (startPosition.x - impactPosition.x) * (startPosition.x - impactPosition.x) + (startPosition.z - impactPosition.z) * (startPosition.z - impactPosition.z) ); float verticalDistance = startPosition.y - impactPosition.y; ins.Puts($"Total movement: distance={totalDistance}, horizontal={horizontalDistance}, vertical={verticalDistance}"); ins.Puts($"Movement will be diagonal with {horizontalDistance}m horizontal movement and {verticalDistance}m vertical drop"); float coveredDistance = 0f; float baseFallSpeed = 10f; foreach (BaseEntity entity in cometEntities) { if (entity == null || entity.IsDestroyed) continue; if (entity == cometEntities[0]) { entity.transform.position = startPosition; } else { Vector3 offset = new Vector3( UnityEngine.Random.Range(-3f, 3f), UnityEngine.Random.Range(-3f, 3f), UnityEngine.Random.Range(-3f, 3f) ); entity.transform.position = startPosition + offset; } } float stepDelay = 0.05f; while (coveredDistance < totalDistance && !impactOccurred) { if (ins == null || ins._config == null || ins._config.cometConfig == null) { Debug.LogError("CometFall: Plugin instance or config became null during fall"); break; } float progress = coveredDistance / totalDistance; float speedMultiplier = 0.5f + (1.5f * progress); float stepDistance = baseFallSpeed * speedMultiplier * ins._config.cometConfig.fallingSpeedScale; if (coveredDistance + stepDistance > totalDistance) { stepDistance = totalDistance - coveredDistance; } coveredDistance += stepDistance; Vector3 newMainPosition = Vector3.Lerp(startPosition, impactPosition, coveredDistance / totalDistance); if (cometEntities.Count > 0 && cometEntities[0] != null && !cometEntities[0].IsDestroyed) { cometEntities[0].transform.position = newMainPosition; for (int i = 1; i < cometEntities.Count; i++) { BaseEntity entity = cometEntities[i]; if (entity == null || entity.IsDestroyed) continue; Vector3 offset = new Vector3( UnityEngine.Random.Range(-3f, 3f), UnityEngine.Random.Range(-3f, 3f), UnityEngine.Random.Range(-3f, 3f) ); entity.transform.position = newMainPosition + offset; entity.transform.Rotate(new Vector3( UnityEngine.Random.Range(1f, 3f), UnityEngine.Random.Range(1f, 3f), UnityEngine.Random.Range(1f, 3f) ), Space.Self); } } if (coveredDistance >= totalDistance - stepDistance) { ins.Puts("Comet reached target position"); if (cometEntities.Count > 0 && cometEntities[0] != null && !cometEntities[0].IsDestroyed) { cometEntities[0].transform.position = impactPosition; for (int i = 1; i < cometEntities.Count; i++) { BaseEntity entity = cometEntities[i]; if (entity == null || entity.IsDestroyed) continue; Vector3 offset = new Vector3( UnityEngine.Random.Range(-3f, 3f), UnityEngine.Random.Range(-3f, 3f), UnityEngine.Random.Range(-3f, 3f) ); entity.transform.position = impactPosition + offset; } } OnCometImpact(impactPosition); break; } if (cometEntities.Count > 0 && cometEntities[0] != null && !cometEntities[0].IsDestroyed) { RaycastHit hit; if (Physics.Raycast(cometEntities[0].transform.position, fallDirection, out hit, stepDistance, LayerMask.GetMask("Terrain", "World", "Construction", "Tree", "Deployed"))) { ins.Puts($"Comet hit object at {hit.point}"); OnCometImpact(hit.point); break; } } yield return Wait0_05; } ins.Puts("Comet fall ended"); if (!impactOccurred) { OnCometImpact(impactPosition); } } void OnCometImpact(Vector3 exactImpactPos) { if (impactOccurred) return; impactOccurred = true; try { Vector3 finalImpactPos = impactPosition; if (Vector3.Distance(exactImpactPos, impactPosition) > 1f) { ins.Puts($"Impact adjustment: Original={exactImpactPos}, Using={finalImpactPos}"); } CancelInvoke(CreatePeriodicEffects); foreach (BaseEntity entity in cometEntities) { if (entity == null || entity.IsDestroyed) continue; entity.Kill(); } CreateExplosion(finalImpactPos); DealImpactDamage(finalImpactPos); CreateTemporaryCrater(finalImpactPos); int cleanupTime = ins?._config?.cometConfig?.impactCleanupTime ?? 30; Invoke(() => CleanupImpactSite(), cleanupTime); } catch (Exception ex) { Debug.LogError("Error in OnCometImpact: " + ex.Message); foreach (BaseEntity entity in cometEntities) { if (entity == null || entity.IsDestroyed) continue; entity.Kill(); } cometEntities.Clear(); impactEntities.Clear(); } } void CreateExplosion(Vector3 position) { Effect.server.Run("assets/prefabs/npc/patrol helicopter/effects/heli_explosion.prefab", position, Vector3.up); ServerMgr.Instance.StartCoroutine(SequentialExplosions(position)); foreach (var player in BasePlayer.activePlayerList) { if (Vector3.Distance(player.transform.position, position) < ins._config.cometConfig.playerDamageRadius) { Effect.server.Run("assets/prefabs/npc/patrol helicopter/effects/heli_explosion.prefab", player.transform.position + Vector3.up * 10, Vector3.up); if (Vector3.Distance(player.transform.position, position) < impactRadius * 2) { Vector3 pushDirection = (player.transform.position - position).normalized; player.Hurt(ins._config.cometConfig.playerDamageAmount, Rust.DamageType.Explosion); } } } } void StartFallingExplosionChain(Vector3 startPos) { ServerMgr.Instance.StartCoroutine(FallingExplosionChain(startPos)); } IEnumerator SequentialExplosions(Vector3 position) { yield return Wait0_1; for (int i = 0; i < ins._config.cometConfig.numberOfSecondaryExplosions; i++) { float angle = i * (360f / ins._config.cometConfig.numberOfSecondaryExplosions); float distance = UnityEngine.Random.Range(2f, impactRadius * 0.8f); Vector3 explosionPos = position + new Vector3( Mathf.Cos(angle * Mathf.Deg2Rad) * distance, 2f, Mathf.Sin(angle * Mathf.Deg2Rad) * distance ); StartFallingExplosionChain(explosionPos); yield return GetWait(ins._config.cometConfig.chainExplosionDelay); } float endTime = Time.time + ins._config.cometConfig.impactCleanupTime * 0.1f; int maxAdditionalExplosions = 3; int additionalExplosions = 0; while (Time.time < endTime && additionalExplosions < maxAdditionalExplosions) { Vector3 randomOffset = new Vector3( UnityEngine.Random.Range(-impactRadius * 0.7f, impactRadius * 0.7f), 1.5f, UnityEngine.Random.Range(-impactRadius * 0.7f, impactRadius * 0.7f) ); Vector3 explosionPos = position + randomOffset; StartFallingExplosionChain(explosionPos); additionalExplosions++; yield return GetWait(ins._config.cometConfig.chainExplosionDelay * 3f); } } IEnumerator FallingExplosionChain(Vector3 startPos) { Vector3 currentPos = startPos; bool shouldContinue = true; int currentChain = 0; float maxRaycastDistance = 10f; while (shouldContinue && currentChain < ins._config.cometConfig.maxChainLength) { Effect.server.Run("assets/bundled/prefabs/fx/fire/fire_v2.prefab", currentPos, Vector3.up); RaycastHit hit; if (Physics.Raycast(currentPos, Vector3.down, out hit, maxRaycastDistance, LayerMask.GetMask("Terrain", "World", "Construction", "Deployed"))) { float distanceToHit = Vector3.Distance(currentPos, hit.point); if (distanceToHit <= ins._config.cometConfig.fallSpeed) { Effect.server.Run("assets/prefabs/npc/patrol helicopter/effects/heli_explosion.prefab", hit.point, Vector3.up); Explode(hit.point, impactRadius * 0.5f, explosionDamage * ins._config.cometConfig.secondaryExplosionDamageScale); BaseCombatEntity hitEntity = hit.GetEntity() as BaseCombatEntity; if (hitEntity != null) { if (hitEntity is BuildingBlock) { hitEntity.Hurt(explosionDamage * ins._config.cometConfig.buildingDamageMultiplier, Rust.DamageType.Explosion, null, true); } yield return GetWait(ins._config.cometConfig.chainExplosionDelay); currentPos = hit.point + new Vector3( UnityEngine.Random.Range(-2f, 2f), 0.5f, UnityEngine.Random.Range(-2f, 2f) ); } else { shouldContinue = false; } currentChain++; } else { currentPos = Vector3.MoveTowards(currentPos, hit.point, ins._config.cometConfig.fallSpeed); } } else { currentPos += Vector3.down * ins._config.cometConfig.fallSpeed; if (currentPos.y < impactPosition.y - 5f) { shouldContinue = false; } } yield return GetWait(ins._config.cometConfig.chainExplosionDelay); } } void Explode(Vector3 position, float radius, float damage) { List entities = new List(); Vis.Entities(position, radius, entities); foreach (var entity in entities) { if (entity == null || entity.IsDestroyed) continue; float distance = Vector3.Distance(position, entity.transform.position); float damageAmount = damage * (1 - Mathf.Clamp01(distance / radius)); BaseCombatEntity combatEntity = entity as BaseCombatEntity; if (combatEntity != null) { if (entity is BuildingBlock) combatEntity.Hurt(damageAmount * 2f, Rust.DamageType.Explosion, null, true); else combatEntity.Hurt(damageAmount, Rust.DamageType.Explosion, null, true); } } } void DealImpactDamage(Vector3 position) { Explode(position, impactRadius * 2, explosionDamage); } void CreateTemporaryCrater(Vector3 position) { int effectCount = Mathf.Min(ins._config.cometConfig.initialExplosionCount, 8); for (int i = 0; i < effectCount; i++) { float angle = UnityEngine.Random.Range(0f, 360f); float distance = UnityEngine.Random.Range(1f, impactRadius * 0.8f); Vector3 effectPos = position + new Vector3( Mathf.Cos(angle * Mathf.Deg2Rad) * distance, 1.5f, Mathf.Sin(angle * Mathf.Deg2Rad) * distance ); StartFallingExplosionChain(effectPos); } StartFallingExplosionChain(position + Vector3.up * 1.5f); if (ins._config.cometConfig.postImpactEffectCount > 0 && ins._config.cometConfig.postImpactEffectInterval > 0) { InvokeRepeating(CreatePostImpactEffects, 1f, ins._config.cometConfig.postImpactEffectInterval); } } void CreatePostImpactEffects() { if (UnityEngine.Random.Range(0, 100) < 25) { Vector3 effectPos = impactPosition + new Vector3( UnityEngine.Random.Range(-impactRadius * 0.5f, impactRadius * 0.5f), 1.0f, UnityEngine.Random.Range(-impactRadius * 0.5f, impactRadius * 0.5f) ); StartFallingExplosionChain(effectPos); } } void CleanupImpactSite() { CancelInvoke(CreatePostImpactEffects); foreach (BaseEntity entity in impactEntities) { if (entity == null || entity.IsDestroyed) continue; entity.Kill(); } impactEntities.Clear(); if (ins != null && ins.activeComets != null) { ins.activeComets.Remove(this.gameObject); } if (ins != null && ins.activeCometComponents != null) { ins.activeCometComponents.Remove(this); } Destroy(this); } void OnDestroy() { CleanupOnPluginUnload(); } } #endregion #region Configuration private PluginConfig _config; protected override void LoadDefaultConfig() { Puts("Loading default config..."); _config = PluginConfig.DefaultConfig(); SaveConfig(); Puts("Default config loaded successfully!"); } protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) { LoadDefaultConfig(); return; } SaveConfig(); } catch (Exception e) { PrintError($"Error loading config: {e.Message}"); LoadDefaultConfig(); } } protected override void SaveConfig() => Config.WriteObject(_config); public class PluginConfig { [JsonProperty("Comet Configuration")] public CometConfig cometConfig { get; set; } [JsonProperty("Signal Configuration")] public SignalConfig signalConfig { get; set; } public static PluginConfig DefaultConfig() { return new PluginConfig() { cometConfig = new CometConfig { fallingSpeedScale = 0.5f, minFallHeight = 900f, maxFallHeight = 950f, horizontalOffset = 800f, impactRadius = 7f, explosionDamage = 500f, buildingDamageMultiplier = 1f, secondaryExplosionDamageScale = 0.75f, maxChainLength = 5, chainExplosionDelay = 0.3f, fallSpeed = 5.0f, numberOfSecondaryExplosions = 20, initialExplosionCount = 10, postImpactEffectInterval = 3f, postImpactEffectCount = 5, impactCleanupTime = 10, playerDamageRadius = 30f, playerDamageAmount = 20f }, signalConfig = new SignalConfig { DisplayName = "Comet Signal", WarmupTime = 30f, CooldownSeconds = 300f } }; } } public class CometConfig { [JsonProperty("Falling Speed Scale (lower = slower fall)")] public float fallingSpeedScale { get; set; } [JsonProperty("Minimum Height for Comet Spawn")] public float minFallHeight { get; set; } [JsonProperty("Maximum Height for Comet Spawn")] public float maxFallHeight { get; set; } [JsonProperty("Horizontal Offset for Comet Spawn (0 = above target, higher = more diagonal fall)")] public float horizontalOffset { get; set; } [JsonProperty("Impact Radius")] public float impactRadius { get; set; } [JsonProperty("Base Explosion Damage")] public float explosionDamage { get; set; } [JsonProperty("Building Damage Multiplier")] public float buildingDamageMultiplier { get; set; } [JsonProperty("Secondary Explosion Damage Scale")] public float secondaryExplosionDamageScale { get; set; } [JsonProperty("Maximum Chain Explosion Length")] public int maxChainLength { get; set; } [JsonProperty("Delay Between Chain Explosions")] public float chainExplosionDelay { get; set; } [JsonProperty("Fall Speed of Chain Explosions")] public float fallSpeed { get; set; } [JsonProperty("Number of Secondary Explosions")] public int numberOfSecondaryExplosions { get; set; } [JsonProperty("Initial Explosion Count")] public int initialExplosionCount { get; set; } [JsonProperty("Post Impact Effect Interval")] public float postImpactEffectInterval { get; set; } [JsonProperty("Post Impact Effect Count")] public int postImpactEffectCount { get; set; } [JsonProperty("Time Until Impact Site Cleanup (0 = never)")] public int impactCleanupTime { get; set; } [JsonProperty("Player Damage Radius")] public float playerDamageRadius { get; set; } [JsonProperty("Player Damage Amount")] public float playerDamageAmount { get; set; } } public class SignalConfig { [JsonProperty("Signal Name")] public string DisplayName { get; set; } [JsonProperty("Time Before Comet Falls (seconds)")] public float WarmupTime { get; set; } [JsonProperty("Cooldown Time (seconds)")] public float CooldownSeconds { get; set; } } #endregion } }