C#:
using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using Rust.Ai.Gen2;
using UnityEngine;
using UnityEngine.AI;
namespace Oxide.Plugins
{
[Info("Custom Wounded NPC", "Razor", "1.0.5")]
[Description("Makes NPCs enter a configurable wounded state before recovering or dying.")]
public class CustomWoundedNPC : RustPlugin
{
#region Things
private const string WoundedScreamEffect = "assets/bundled/prefabs/fx/player/beartrap_scream.prefab";
private ConfigData configData;
#endregion
#region Config
private class ConfigData
{
[JsonProperty(PropertyName = "Settings")]
public Settings settings { get; set; }
[JsonProperty(PropertyName = "NPC settings by ShortPrefabName")]
public Dictionary<string, NpcSettings> Npcs { get; set; }
public class Settings
{
[JsonProperty(PropertyName = "Excluded NPC prefab paths (exact match)")]
public List<string> ExcludedPrefabPaths { get; set; }
[JsonProperty(PropertyName = "Excluded NPC partial names (case-insensitive)")]
public List<string> ExcludedPartialNames { get; set; }
}
public class NpcSettings
{
[JsonProperty(PropertyName = "Can become wounded")]
public bool CanBecomeWounded { get; set; }
[JsonProperty(PropertyName = "Chance to become wounded (0-100)")]
public float WoundedChance { get; set; }
[JsonProperty(PropertyName = "Allow becoming wounded from headshots")]
public bool AllowHeadshotWounding { get; set; }
[JsonProperty(PropertyName = "Wounded position downward offset")]
public float DownwardOffset { get; set; }
[JsonProperty(PropertyName = "Chance to get back up (0-100)")]
public float RecoveryChance { get; set; }
[JsonProperty(PropertyName = "Wounded duration in seconds")]
public float WoundedDuration { get; set; }
[JsonProperty(PropertyName = "Health after getting back up")]
public float RecoveryHealth { get; set; }
[JsonProperty(PropertyName = "Play Rust's built-in wounded scream effect")]
public bool PlayScreamEffect { get; set; }
[JsonProperty(PropertyName = "Number of times to play the scream effect")]
public int ScreamPlayCount { get; set; }
[JsonProperty(PropertyName = "Delay between scream effects in seconds")]
public float ScreamRepeatDelay { get; set; }
}
}
protected override void LoadConfig()
{
base.LoadConfig();
configData = Config.ReadObject<ConfigData>();
ValidateConfig();
Config.WriteObject(configData, true);
}
protected override void LoadDefaultConfig()
{
configData = GetBaseConfig();
}
private ConfigData GetBaseConfig()
{
return new ConfigData
{
settings = new ConfigData.Settings
{
ExcludedPrefabPaths = new List<string>(),
ExcludedPartialNames = new List<string> { "zombie" }
},
Npcs = new Dictionary<string, ConfigData.NpcSettings>()
};
}
private ConfigData.NpcSettings GetDefaultNpcSettings()
{
return new ConfigData.NpcSettings
{
CanBecomeWounded = true,
WoundedChance = 25f,
AllowHeadshotWounding = false,
DownwardOffset = 0f,
RecoveryChance = 50f,
WoundedDuration = 30f,
RecoveryHealth = 25f,
PlayScreamEffect = true,
ScreamPlayCount = 3,
ScreamRepeatDelay = 10f
};
}
protected override void SaveConfig()
{
Config.WriteObject(configData, true);
}
private void ValidateConfig()
{
if (configData == null)
configData = GetBaseConfig();
if (configData.settings == null)
configData.settings = GetBaseConfig().settings;
if (configData.settings.ExcludedPrefabPaths == null)
configData.settings.ExcludedPrefabPaths = new List<string>();
if (configData.settings.ExcludedPartialNames == null)
configData.settings.ExcludedPartialNames = new List<string>();
if (configData.Npcs == null)
configData.Npcs = new Dictionary<string, ConfigData.NpcSettings>();
foreach (var entry in configData.Npcs)
{
if (entry.Value == null)
continue;
entry.Value.WoundedChance = Mathf.Clamp(entry.Value.WoundedChance, 0f, 100f);
entry.Value.DownwardOffset = Mathf.Clamp(entry.Value.DownwardOffset, 0f, 2f);
entry.Value.RecoveryChance = Mathf.Clamp(entry.Value.RecoveryChance, 0f, 100f);
entry.Value.WoundedDuration = Mathf.Max(0.1f, entry.Value.WoundedDuration);
entry.Value.RecoveryHealth = Mathf.Max(1f, entry.Value.RecoveryHealth);
entry.Value.ScreamPlayCount = Mathf.Max(0, entry.Value.ScreamPlayCount);
entry.Value.ScreamRepeatDelay = Mathf.Max(0.1f, entry.Value.ScreamRepeatDelay);
}
}
#endregion
#region Hooks
private void OnServerInitialized()
{
AddKnownNpcSettings();
}
private object OnEntityTakeDamage(BasePlayer player, HitInfo info)
{
if (!IsEligibleNpc(player) || info == null || IsExcluded(player))
return null;
var npcSettings = GetNpcSettings(player);
if (npcSettings == null)
return null;
if (!npcSettings.CanBecomeWounded)
return null;
if (info.isHeadshot && !npcSettings.AllowHeadshotWounding)
return null;
var controller = player.GetComponent<WoundedNpcController>();
if (controller != null)
{
if (info.damageTypes.Total() >= player.health)
controller.PrepareDeath();
return null;
}
if (info.damageTypes.Total() < player.health)
return null;
if (UnityEngine.Random.Range(0f, 100f) >= npcSettings.WoundedChance)
return null;
info.damageTypes.Clear();
player.health = 1f;
player.metabolism.bleeding.value = 0f;
player.healingWhileCrawling = 0f;
player.SetPlayerFlag(BasePlayer.PlayerFlags.Wounded, true);
player.SetFlagLocal(BaseEntity.Flags.Busy, true);
player.SetServerFall(true);
player.lastWoundedStartTime = Time.realtimeSinceStartup;
player.CancelInvoke(player.WoundingTick);
player.SendNetworkUpdateImmediate();
var willRecover = UnityEngine.Random.Range(0f, 100f) < npcSettings.RecoveryChance;
controller = player.gameObject.AddComponent<WoundedNpcController>();
controller.Initialize(player, npcSettings.WoundedDuration, willRecover, npcSettings.RecoveryHealth, npcSettings.DownwardOffset, npcSettings.PlayScreamEffect, npcSettings.ScreamPlayCount, npcSettings.ScreamRepeatDelay);
return true;
}
private object OnBotReSpawnNPCTarget(ScientistNPC npc, BasePlayer player)
{
if (npc == null || npc.GetComponent<WoundedNpcController>() == null)
return null;
return true;
}
#endregion
#region NPC Handling
private bool IsEligibleNpc(BasePlayer player)
{
if (player == null || player.IsDestroyed || player.IsDead())
return false;
if (player.isMounted)
return false;
if (IsSafeZoneNpc(player))
return false;
return player.IsNpc || !player.userID.IsSteamId();
}
private bool IsSafeZoneNpc(BasePlayer player)
{
return player is NPCTalking || player is NPCShopKeeper || player is BanditGuard;
}
private bool IsExcluded(BasePlayer player)
{
var prefabPath = player.PrefabName ?? string.Empty;
foreach (var excludedPrefab in configData.settings.ExcludedPrefabPaths)
{
if (string.Equals(prefabPath, excludedPrefab, StringComparison.OrdinalIgnoreCase))
return true;
}
var displayName = player.displayName ?? string.Empty;
var shortPrefabName = player.ShortPrefabName ?? string.Empty;
var typeName = player.GetType().Name;
foreach (var partialName in configData.settings.ExcludedPartialNames)
{
if (displayName.IndexOf(partialName, StringComparison.OrdinalIgnoreCase) >= 0 || shortPrefabName.IndexOf(partialName, StringComparison.OrdinalIgnoreCase) >= 0 || typeName.IndexOf(partialName, StringComparison.OrdinalIgnoreCase) >= 0)
return true;
}
return false;
}
private void AddKnownNpcSettings()
{
var changed = false;
foreach (var prefabPath in GameManifest.Current.entities)
{
var prefab = GameManager.server.FindPrefab(prefabPath);
if (prefab == null)
continue;
var player = prefab.GetComponent<BasePlayer>();
if (player == null || player.GetType() == typeof(BasePlayer) && !player.IsNpc)
continue;
if (IsSafeZoneNpc(player))
continue;
var shortPrefabName = player.ShortPrefabName;
if (string.IsNullOrWhiteSpace(shortPrefabName))
shortPrefabName = Path.GetFileNameWithoutExtension(prefabPath);
if (configData.Npcs.ContainsKey(shortPrefabName))
continue;
configData.Npcs[shortPrefabName] = GetDefaultNpcSettings();
changed = true;
}
if (changed)
SaveConfig();
}
private ConfigData.NpcSettings GetNpcSettings(BasePlayer player)
{
var shortPrefabName = player.ShortPrefabName;
if (string.IsNullOrWhiteSpace(shortPrefabName))
shortPrefabName = player.GetType().Name;
ConfigData.NpcSettings npcSettings;
if (configData.Npcs.TryGetValue(shortPrefabName, out npcSettings))
return npcSettings;
return null;
}
#endregion
#region Wounded Controller
private class WoundedNpcController : FacepunchBehaviour
{
private BasePlayer player;
private bool willRecover;
private float recoveryHealth;
private float downwardOffset;
private BaseAIBrain brain;
private NpcShootingComponent shootingComponent;
private Scientist2FSM scientistFsm;
private NavMeshAgent navMeshAgent;
private ItemId activeItemId;
private bool brainWasEnabled;
private bool shootingWasEnabled;
private bool scientistFsmWasEnabled;
private bool navMeshAgentWasEnabled;
private bool positionLowered;
private bool finishing;
private bool playScreamEffect;
private int screamPlaysRemaining;
private float screamRepeatDelay;
public void Initialize(BasePlayer player, float woundedDuration, bool willRecover, float recoveryHealth, float downwardOffset, bool playScreamEffect, int screamPlayCount, float screamRepeatDelay)
{
this.player = player;
this.willRecover = willRecover;
this.recoveryHealth = recoveryHealth;
this.downwardOffset = downwardOffset;
this.playScreamEffect = playScreamEffect;
screamPlaysRemaining = screamPlayCount;
this.screamRepeatDelay = screamRepeatDelay;
brain = player.GetComponent<BaseAIBrain>();
shootingComponent = player.GetComponent<NpcShootingComponent>();
scientistFsm = player.GetComponent<Scientist2FSM>();
navMeshAgent = player.GetComponent<NavMeshAgent>();
ClearNpcTargetAndMemory();
var activeItem = player.GetActiveItem();
if (activeItem != null)
activeItemId = activeItem.uid;
if (brain != null)
{
brainWasEnabled = brain.enabled;
brain.enabled = false;
}
if (shootingComponent != null)
{
shootingWasEnabled = shootingComponent.enabled;
shootingComponent.enabled = false;
}
if (scientistFsm != null)
{
scientistFsmWasEnabled = scientistFsm.enabled;
scientistFsm.enabled = false;
}
if (navMeshAgent != null)
{
navMeshAgentWasEnabled = navMeshAgent.enabled;
navMeshAgent.enabled = false;
}
BaseNavigator _navigator = player.GetComponent<BaseNavigator>();
if (_navigator != null)
_navigator.SetNavMeshEnabled(false);
var heldEntity = player.GetHeldEntity();
if (heldEntity != null)
{
heldEntity.CancelInvoke();
heldEntity.SetHeld(false);
}
player.UpdateActiveItem(default(ItemId));
InvokeRepeating(KeepNpcDisabled, 0.1f, 0.1f);
if (downwardOffset > 0f)
{
player.transform.position += Vector3.down * downwardOffset;
player.SendNetworkUpdate_Position();
positionLowered = true;
}
PlayScreamEffect();
if (!willRecover)
Invoke(SetIncapacitated, Mathf.Max(0f, woundedDuration - 10f));
Invoke(FinishWoundedState, woundedDuration);
}
private void KeepNpcDisabled()
{
if (player == null || player.IsDestroyed || player.IsDead() || !player.IsWounded())
return;
if (brain != null && brain.enabled)
brain.enabled = false;
if (shootingComponent != null && shootingComponent.enabled)
shootingComponent.enabled = false;
if (scientistFsm != null && scientistFsm.enabled)
scientistFsm.enabled = false;
if (navMeshAgent != null && navMeshAgent.enabled)
navMeshAgent.enabled = false;
ClearNpcTargetAndMemory();
var heldEntity = player.GetHeldEntity();
if (heldEntity == null)
return;
heldEntity.CancelInvoke();
heldEntity.SetHeld(false);
player.UpdateActiveItem(default(ItemId));
player.SendNetworkUpdateImmediate();
}
private void ClearNpcTargetAndMemory()
{
if (brain == null)
return;
if (brain.Events != null && brain.Events.Memory != null)
brain.Events.Memory.Clear();
if (brain.Senses == null || brain.Senses.Memory == null)
return;
var memory = brain.Senses.Memory;
memory.All.Clear();
memory.Players.Clear();
memory.LOS.Clear();
memory.Targets.Clear();
memory.Threats.Clear();
memory.Friendlies.Clear();
}
private void PlayScreamEffect()
{
if (!playScreamEffect || screamPlaysRemaining <= 0 || player == null || player.IsDestroyed || player.IsDead() || !player.IsWounded())
return;
Effect.server.Run(WoundedScreamEffect, player, 0, Vector3.zero, Vector3.zero, null, true);
screamPlaysRemaining--;
if (screamPlaysRemaining > 0)
Invoke(PlayScreamEffect, screamRepeatDelay);
}
public void PrepareDeath()
{
if (finishing)
return;
finishing = true;
if (positionLowered)
RestorePosition();
}
private void SetIncapacitated()
{
if (player == null || player.IsDestroyed || player.IsDead() || !player.IsWounded())
return;
player.SetPlayerFlag(BasePlayer.PlayerFlags.Incapacitated, true);
player.SendNetworkUpdateImmediate();
}
private void FinishWoundedState()
{
if (player == null || player.IsDestroyed || player.IsDead() || !player.IsWounded())
{
Destroy(this);
return;
}
if (!willRecover)
{
PrepareDeath();
player.DieInstantly();
Destroy(this);
return;
}
finishing = true;
player.StopWounded();
player.SetPlayerFlag(BasePlayer.PlayerFlags.Incapacitated, false);
player.SetFlagLocal(BaseEntity.Flags.Busy, false);
player.ResetWoundingVars();
player.health = Mathf.Max(1f, recoveryHealth);
if (positionLowered)
RestorePosition();
player.SendNetworkUpdateImmediate();
Invoke(FinishRecovery, 0.1f);
}
private void FinishRecovery()
{
if (player == null || player.IsDestroyed || player.IsDead())
{
Destroy(this);
return;
}
RestoreNpc();
player.SendNetworkUpdateImmediate();
Destroy(this);
}
private void RestorePosition()
{
if (player == null || player.IsDestroyed || !positionLowered)
return;
player.transform.position += Vector3.up * downwardOffset;
player.SendNetworkUpdate_Position();
positionLowered = false;
}
private void RestoreNpc()
{
RestoreNavMeshAgent();
RestoreActiveItem();
if (brain != null)
brain.enabled = brainWasEnabled;
if (shootingComponent != null)
shootingComponent.enabled = shootingWasEnabled;
if (scientistFsm != null)
scientistFsm.enabled = scientistFsmWasEnabled;
}
private void RestoreNavMeshAgent()
{
BaseNavigator _navigator = player.GetComponent<BaseNavigator>();
if (_navigator != null)
_navigator.SetNavMeshEnabled(true);
if (navMeshAgent == null || !navMeshAgentWasEnabled)
return;
NavMeshHit navMeshHit;
if (NavMesh.SamplePosition(player.transform.position, out navMeshHit, 2f, NavMesh.AllAreas))
{
player.transform.position = navMeshHit.position;
player.SendNetworkUpdate_Position();
}
navMeshAgent.enabled = true;
navMeshAgent.Warp(player.transform.position);
}
private void RestoreActiveItem()
{
if (activeItemId == default(ItemId) || player.inventory == null || player.inventory.containerBelt == null)
return;
var activeItem = player.inventory.containerBelt.FindItemByUID(activeItemId);
if (activeItem == null)
return;
player.svActiveItemID = activeItem.uid;
var heldEntity = activeItem.GetHeldEntity() as HeldEntity;
if (heldEntity != null)
heldEntity.SetHeld(true);
player.inventory.UpdatedVisibleHolsteredItems();
player.SendNetworkUpdateImmediate();
}
private void OnDestroy()
{
CancelInvoke();
if (player == null || player.IsDestroyed || player.IsDead())
return;
if (positionLowered)
RestorePosition();
if (finishing)
return;
player.StopWounded();
player.SetPlayerFlag(BasePlayer.PlayerFlags.Incapacitated, false);
player.SetFlagLocal(BaseEntity.Flags.Busy, false);
player.ResetWoundingVars();
RestoreNpc();
player.SendNetworkUpdateImmediate();
}
}
#endregion
}
}