C#:
using System.Collections.Generic;
using Facepunch;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
using Newtonsoft.Json;
namespace Oxide.Plugins
{
[Info("HauntedDoors", "RustFlash", "1.8.2")]
[Description("Spawns scary ghosts when opening double doors")]
public class HauntedDoors : RustPlugin
{
#region Configuration
private Configuration config;
public class Configuration
{
[JsonProperty("Ghost spawn chance (0.0 - 1.0)")]
public float SpawnChance { get; set; } = 0.15f;
[JsonProperty("Ghost visibility duration (seconds)")]
public float GhostDuration { get; set; } = 4f;
[JsonProperty("Play sound effects")]
public bool EnableSounds { get; set; } = true;
[JsonProperty("Show visual effects (smoke/fog)")]
public bool EnableVisualEffects { get; set; } = true;
[JsonProperty("Allow single doors to trigger ghosts")]
public bool SingleDoors { get; set; } = true;
[JsonProperty("Allow double doors to trigger ghosts")]
public bool DoubleDoors { get; set; } = true;
}
#endregion
#region Fields
private readonly Dictionary<BaseEntity, string> ghostDespawnSounds = new Dictionary<BaseEntity, string>();
private readonly HashSet<BaseEntity> ghostEntities = new HashSet<BaseEntity>();
private readonly System.Random random = new System.Random();
private readonly LayerMask groundRaycastMask = LayerMask.GetMask("Terrain", "World", "Construction");
private static readonly HashSet<string> singleDoorShortNames = new HashSet<string>
{
"door.hinged.wood",
"door.hinged.metal",
"door.hinged.toptier"
};
private static readonly HashSet<string> doubleDoorShortNames = new HashSet<string>
{
"door.double.hinged.wood",
"door.double.hinged.metal",
"door.double.hinged.toptier"
};
private readonly List<GhostType> ghostTypes = new List<GhostType>
{
new GhostType
{
Prefab = "assets/prefabs/misc/halloween/scarecrow/scarecrow.deployed.prefab",
SpawnSound = "assets/prefabs/npc/patrol helicopter/effects/rocket_fire.prefab",
DespawnSound = "assets/bundled/prefabs/fx/missing.prefab"
},
new GhostType
{
Prefab = "assets/rust.ai/agents/zombie/zombie.prefab",
SpawnSound = "assets/bundled/prefabs/fx/missing.prefab",
DespawnSound = "assets/prefabs/locks/keypad/effects/lock.code.denied.prefab"
}
};
private readonly List<string> ghostEffects = new List<string>
{
"assets/bundled/prefabs/fx/explosions/explosion_03.prefab",
"assets/prefabs/npc/patrol helicopter/effects/heli_explosion.prefab",
"assets/bundled/prefabs/fx/explosions/explosion_02.prefab"
};
private class GhostType
{
public string Prefab { get; set; }
public string SpawnSound { get; set; }
public string DespawnSound { get; set; }
}
#endregion
#region Hooks
private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
{
if (!(entity is BasePlayer))
return null;
if (!ghostEntities.Contains(entity))
return null;
return true;
}
#endregion
#region Initialization
private void Init()
{
Unsubscribe(nameof(OnEntityTakeDamage));
}
protected override void LoadDefaultConfig()
{
config = new Configuration();
SaveConfig();
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<Configuration>();
if (config == null)
{
PrintError("Configuration file is empty! Using default values for this session.");
config = new Configuration();
}
}
catch
{
PrintError("Configuration file is corrupt! Using default values for this session.");
config = new Configuration();
}
}
protected override void SaveConfig() => Config.WriteObject(config);
private void Unload()
{
var ghosts = Pool.Get<List<BaseEntity>>();
ghosts.AddRange(ghostEntities);
foreach (var ghost in ghosts)
{
if (ghost != null && !ghost.IsDestroyed)
ghost.Kill();
}
Pool.FreeUnmanaged(ref ghosts);
ghostEntities.Clear();
ghostDespawnSounds.Clear();
}
#endregion
#region Door Events
private void OnDoorOpened(Door door, BasePlayer player)
{
if (door == null || player == null || player.IsNpc)
return;
if (!IsEligibleDoor(door))
return;
if (random.NextDouble() > config.SpawnChance)
return;
SpawnGhost(door, player);
}
#endregion
#region Core Methods
private bool IsEligibleDoor(Door door)
{
if (door?.ShortPrefabName == null)
return false;
if (config.DoubleDoors && doubleDoorShortNames.Contains(door.ShortPrefabName))
return true;
if (config.SingleDoors && singleDoorShortNames.Contains(door.ShortPrefabName))
return true;
return false;
}
private void SpawnGhost(Door door, BasePlayer player)
{
if (ghostTypes.Count == 0)
{
PrintError("No ghost types configured!");
return;
}
GhostType selectedGhost = ghostTypes[random.Next(0, ghostTypes.Count)];
Vector3 spawnPos = CalculateGhostSpawnPosition(player);
ApplyGhostEffects(spawnPos, selectedGhost.SpawnSound);
timer.Once(0.5f, () =>
{
if (player == null || player.IsDestroyed)
return;
BaseEntity ghost = GameManager.server.CreateEntity(selectedGhost.Prefab, spawnPos);
if (ghost == null)
{
PrintError($"Failed to create ghost entity: {selectedGhost.Prefab}");
return;
}
Vector3 direction = (player.transform.position - spawnPos).normalized;
if (direction != Vector3.zero)
ghost.transform.rotation = Quaternion.LookRotation(direction);
ghost.EnableSaving(false);
ghost.Spawn();
if (ghostEntities.Count == 0)
Subscribe(nameof(OnEntityTakeDamage));
ghostEntities.Add(ghost);
ghostDespawnSounds[ghost] = selectedGhost.DespawnSound;
timer.Once(0.1f, () =>
{
if (ghost != null && !ghost.IsDestroyed)
{
ConfigureGhost(ghost, player);
}
});
timer.Once(config.GhostDuration, () => DespawnGhost(ghost));
});
}
private void ConfigureGhost(BaseEntity ghost, BasePlayer player)
{
if (ghost is NPCPlayer npc)
{
npc.InitializeHealth(999999f, 999999f);
npc.startHealth = 999999f;
npc.lifestate = BaseCombatEntity.LifeState.Alive;
var navigator = npc.GetComponent<NPCPlayerNavigator>();
if (navigator != null)
navigator.enabled = false;
var brain = npc.GetComponent<BaseAIBrain>();
if (brain != null)
brain.enabled = false;
npc.SetDestination(npc.transform.position);
}
else if (ghost is ScarecrowNPC scarecrow)
{
scarecrow.InitializeHealth(999999f, 999999f);
scarecrow.startHealth = 999999f;
scarecrow.lifestate = BaseCombatEntity.LifeState.Alive;
var brain = scarecrow.GetComponent<ScarecrowBrain>();
if (brain != null)
brain.enabled = false;
}
else
{
if (ghost is BaseCombatEntity combatEntity)
{
combatEntity.InitializeHealth(999999f, 999999f);
combatEntity.startHealth = 999999f;
combatEntity.lifestate = BaseCombatEntity.LifeState.Alive;
}
if (player != null && !player.IsDestroyed)
{
Vector3 direction = (player.transform.position - ghost.transform.position).normalized;
if (direction != Vector3.zero)
ghost.transform.rotation = Quaternion.LookRotation(direction);
}
}
}
private void DespawnGhost(BaseEntity ghost)
{
if (ghost == null || ghost.IsDestroyed)
return;
if (ghostDespawnSounds.Remove(ghost, out var despawnSound))
{
ApplyGhostEffects(ghost.transform.position, despawnSound);
}
ghostEntities.Remove(ghost);
if (ghostEntities.Count == 0)
Unsubscribe(nameof(OnEntityTakeDamage));
ghost.Kill();
}
#endregion
#region Effects
private void ApplyGhostEffects(Vector3 position, string sound)
{
if (config.EnableSounds && !string.IsNullOrEmpty(sound))
{
Effect.server.Run(sound, position);
}
string randomEffect = GetRandomEffect();
if (config.EnableVisualEffects && randomEffect != null)
{
Effect.server.Run(randomEffect, position);
}
}
private string GetRandomEffect()
{
if (ghostEffects.Count == 0)
return null;
return ghostEffects[random.Next(0, ghostEffects.Count)];
}
#endregion
#region Helper Methods
private Vector3 CalculateGhostSpawnPosition(BasePlayer player)
{
Vector3 playerForward = player.eyes.HeadForward();
Vector3 spawnPos = player.transform.position + (playerForward * 2f);
RaycastHit hit;
if (Physics.Raycast(new Ray(spawnPos + Vector3.up * 2f, Vector3.down), out hit, 4f, groundRaycastMask))
{
spawnPos.y = hit.point.y;
}
else
{
spawnPos.y = player.transform.position.y;
}
return spawnPos;
}
#endregion
}
}