C#:
using Facepunch;
using Newtonsoft.Json.Linq;
using Oxide.Core.Plugins;
using Rust;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.AI;
namespace Oxide.Plugins
{
[Info("CustomNPC", "k1lly0u", "1.2.2")]
class CustomNPC : RustPlugin
{
#region Fields
[PluginReference] private Plugin Kits;
private Hash<Plugin, List<HumanAI>> _humanAIRef = new Hash<Plugin, List<HumanAI>>();
private static CustomNPC Instance;
#endregion
#region Oxide Hooks
private void Loaded()
{
Instance = this;
}
private void OnEntityKill(ScientistNPC scientistNpc)
{
if (scientistNpc == null)
return;
HumanAI humanAI = scientistNpc.GetComponent<HumanAI>();
if (humanAI == null)
return;
foreach (KeyValuePair<Plugin, List<HumanAI>> kvp in _humanAIRef)
kvp.Value.Remove(humanAI);
UnityEngine.Object.Destroy(humanAI);
}
private object OnNpcTarget(ScientistNPC scientistNpc, BasePlayer target)
{
if (scientistNpc == null)
return null;
HumanAI humanAI = scientistNpc.GetComponent<HumanAI>();
if (humanAI == null)
return null;
if (target.IsSleeping() || target.IsFlying)
return true;
return null;
}
private void OnPluginUnloaded(Plugin plugin)
{
List<HumanAI> list;
if (_humanAIRef.TryGetValue(plugin, out list))
{
for (int i = list.Count - 1; i >= 0; i--)
{
HumanAI humanAI = list[i];
if (humanAI != null)
humanAI.Despawn();
}
_humanAIRef.Remove(plugin);
}
}
private void Unload()
{
foreach (KeyValuePair<Plugin, List<HumanAI>> kvp in _humanAIRef)
{
for (int i = kvp.Value.Count - 1; i >= 0; i--)
{
HumanAI humanAI = kvp.Value[i];
if (humanAI != null)
humanAI.Despawn();
}
}
_humanAIRef.Clear();
Instance = null;
}
#endregion
#region Functions
private HumanAI CreateHumanAI(Vector3 position, Settings settings)
{
object point = settings.EnableNavMesh ? FindPointOnNavmesh(position, 60) : position;
if (point is Vector3)
{
position = (Vector3)point;
if (settings.EnableNavMesh && (position.y < -0.25f || (settings.KillInSafeZone && IsInSafeZone(position))))
return null;
const string SCIENTIST_PREFAB = "assets/rust.ai/agents/npcplayer/humannpc/heavyscientist/heavyscientist.prefab";
ScientistNPC npcPlayer = InstantiateEntity(SCIENTIST_PREFAB, position);
npcPlayer.enableSaving = false;
BaseAIBrain<global::HumanNPC> defaultBrain = npcPlayer.GetComponent<BaseAIBrain<global::HumanNPC>>();
defaultBrain._baseEntity = npcPlayer;
UnityEngine.Object.DestroyImmediate(defaultBrain);
HumanAI humanAI = npcPlayer.gameObject.AddComponent<HumanAI>();
npcPlayer._brain = humanAI.Brain = npcPlayer.gameObject.AddComponent<CustomAIBrain>();
npcPlayer.Spawn();
humanAI.Setup(settings);
return humanAI;
}
return null;
}
private HumanAI ConvertHumanAI(ScientistNPC scientistNPC, Settings settings)
{
Vector3 position = scientistNPC.transform.position;
object point = FindPointOnNavmesh(position, 60);
if (point is Vector3)
{
position = (Vector3)point;
if (position.y < -0.25f || (settings.KillInSafeZone && IsInSafeZone(position)))
return null;
HumanAI humanAI = scientistNPC.gameObject.AddComponent<HumanAI>();
humanAI.Setup(settings);
return humanAI;
}
return null;
}
private static ScientistNPC InstantiateEntity(string type, Vector3 position)
{
GameObject gameObject = Instantiate.GameObject(GameManager.server.FindPrefab(type), position, Quaternion.identity);
gameObject.name = type;
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(gameObject, Rust.Server.EntityScene);
UnityEngine.Object.Destroy(gameObject.GetComponent<Spawnable>());
if (!gameObject.activeSelf)
gameObject.SetActive(true);
ScientistNPC component = gameObject.GetComponent<ScientistNPC>();
return component;
}
private static bool IsInSafeZone(Vector3 position)
{
int count = Physics.OverlapSphereNonAlloc(position, 1f, Vis.colBuffer, 1 << 18, QueryTriggerInteraction.Collide);
for (int i = 0; i < count; i++)
{
Collider collider = Vis.colBuffer[i];
if (collider.GetComponent<TriggerSafeZone>())
return true;
}
return false;
}
private static void StripInventory(BasePlayer player, bool skipWear = false)
{
Item[] allItems = player.inventory.AllItems();
for (int i = allItems.Length - 1; i >= 0; i--)
{
Item item = allItems[i];
if (skipWear && item?.parent == player.inventory.containerWear)
continue;
item.RemoveFromContainer();
item.Remove();
}
}
#endregion
#region API
/// <summary>
/// Spawns a new ScientistNPC and add's a HumanAI component on top
/// </summary>
/// <param name="plugin">The plugin the NPC belongs to</param>
/// <param name="position">The spawn position</param>
/// <param name="settingsJson">The settings JObject</param>
/// <returns></returns>
private ScientistNPC SpawnNPC(Plugin plugin, Vector3 position, JObject settingsJson)
{
if (Rust.Ai.AiManager.nav_disable)
{
Debug.LogWarning($"[CustomNPC] - NPC's can not be spawned when the Navmesh is disabled!\nYou can turn it on with the 'aimanager.nav_disable false' convar");
return null;
}
Settings settings = settingsJson.ToObject<Settings>();
HumanAI npc = CreateHumanAI(position, settings);
if (npc == null)
return null;
List<HumanAI> list;
if (!_humanAIRef.TryGetValue(plugin, out list))
list = _humanAIRef[plugin] = new List<HumanAI>();
list.Add(npc);
return npc.Entity;
}
/// <summary>
/// Converts an already spawned ScientistNPC to a HumanAI
/// </summary>
/// <param name="plugin">The plugin the NPC belongs to</param>
/// <param name="scientistNPC">The target NPC</param>
/// <param name="settingsJson">The settings JObject</param>
/// <returns></returns>
private bool ConvertNPC(Plugin plugin, ScientistNPC scientistNPC, JObject settingsJson)
{
Settings settings = settingsJson.ToObject<Settings>();
HumanAI npc = ConvertHumanAI(scientistNPC, settings);
if (npc == null)
return false;
List<HumanAI> list;
if (!_humanAIRef.TryGetValue(plugin, out list))
list = _humanAIRef[plugin] = new List<HumanAI>();
list.Add(npc);
return true;
}
/// <summary>
/// Allows plugins to add a custom AI state to the NPC
/// </summary>
/// <param name="scientistNPC">The target NPC</param>
/// <param name="state">The custom AI state</param>
/// <returns>true if successfull, false if not</returns>
private bool AddCustomAIState(ScientistNPC scientistNPC, BaseAIBrain<global::HumanNPC>.BasicAIState state)
{
HumanAI humanAI = scientistNPC.GetComponent<HumanAI>();
if (humanAI != null)
{
humanAI.Brain.AddState(state);
return true;
}
return false;
}
/// <summary>
/// Set the base position the NPC will roam around
/// </summary>
/// <param name="scientistNPC">The target NPC</param>
/// <param name="position">The base roam position</param>
private void SetRoamHomePosition(ScientistNPC scientistNPC, Vector3 position)
{
HumanAI humanAI = scientistNPC.GetComponent<HumanAI>();
if (humanAI != null)
{
object point = FindPointOnNavmesh(position, 20);
if (point is Vector3)
position = (Vector3)point;
humanAI._roamBasePosition = position;
}
}
/// <summary>
/// Enables the NPC's NavMeshAgent
/// </summary>
/// <param name="scientistNPC">The target NPC</param>
/// <returns></returns>
private bool EnableNavAgent(ScientistNPC scientistNPC)
{
HumanAI humanAI = scientistNPC.GetComponent<HumanAI>();
if (humanAI != null)
{
object point = FindPointOnNavmesh(scientistNPC.transform.position, 20);
if (point is Vector3)
{
Vector3 position = (Vector3)point;
scientistNPC.NavAgent.Warp(position);
scientistNPC.NavAgent.enabled = true;
scientistNPC.transform.position = position;
humanAI._NavMeshEnabled = true;
humanAI.NavMeshEnabled = true;
scientistNPC.NavAgent.isStopped = false;
humanAI.SetDestination(position);
return true;
}
}
return false;
}
/// <summary>
/// Force the NPC to equip their weapon
/// </summary>
/// <param name="scientistNPC">The target NPC</param>
/// <returns></returns>
private void EquipWeapon(ScientistNPC scientistNPC)
{
HumanAI humanAI = scientistNPC.GetComponent<HumanAI>();
if (humanAI != null)
humanAI.EquipWeapon();
}
/// <summary>
/// Force the NPC to holster their weapon
/// </summary>
/// <param name="scientistNPC">The target NPC</param>
/// <returns></returns>
private void HolsterWeapon(ScientistNPC scientistNPC)
{
HumanAI humanAI = scientistNPC.GetComponent<HumanAI>();
if (humanAI != null)
humanAI.HolsterWeapon();
}
/// <summary>
/// Send the NPC to the target destination (requires the MoveToDestination state)
/// </summary>
/// <param name="scientistNPC">The target NPC</param>
/// <param name="destination">The destination</param>
/// <param name="speed">The speed at which they will move towards the destination</param>
/// <param name="onReachedDestination">A callback for once they have reached the destination</param>
private void SetDestination(ScientistNPC scientistNPC, Vector3 destination, global::HumanNPC.SpeedType speed, Action onReachedDestination)
{
HumanAI humanAI = scientistNPC.GetComponent<HumanAI>();
if (humanAI != null)
humanAI.SetDestination(destination, speed, onReachedDestination);
}
#endregion
#region Settings
/// <summary>
/// This the the settings class.The JObject passed through the hook will be converted to this class structure
/// </summary>
internal class Settings
{
public List<string> States = new List<string>(); // The logic states to apply to the NPC
public string NPCType; // The type of NPC (Scientist, Murderer, HeavyScientist)
public string DisplayName; // The name of the NPC
public float Health; // The NPCs initial health
public float SightRange; // The NPCs sight range
public float RoamRange; // The maximum distance from it's spawn point the NPC will roam
public float ChaseRange; // The distance from it's spawn point the NPC will chase a target to
public string Kit; // The kit to give the NPC
public bool StripCorpseLoot; // Removes the NPC corpse loot table
public bool KillInSafeZone; // Kill the NPC if they enter a safe zone, prevent spawn if in a safe zone
public float DespawnTime; // Despawn the NPC after this amount of time (seconds)
public bool StartDead; // Spawn the NPC dead
public bool StartWounded; // NPC spawns in a wounded state
public float WoundedDuration; // The amount of time the NPC will be in the wounded state before attempting to recover
public float WoundedRecoveryChance; // The chance the NPC will recover from the wounded state (0.0 - 100.0)
public bool EnableNavMesh = true; // Enables the NavMeshAgent component on spawn
public bool EquipWeapon = true; // Equips a weapon on spawn
public bool CanUseWeaponMounted; // Can equip a weapon whilst mounted
}
#endregion
#region NavMesh
private static NavMeshHit navmeshHit;
private static RaycastHit raycastHit;
private static Collider[] _buffer = new Collider[256];
private const int WORLD_LAYER = 65536;
internal static object FindPointOnNavmesh(Vector3 targetPosition, float maxDistance = 4f)
{
for (int i = 0; i < 10; i++)
{
Vector3 position = i == 0 ? targetPosition : targetPosition + (UnityEngine.Random.onUnitSphere * (maxDistance * 0.5f));
if (NavMesh.SamplePosition(position, out navmeshHit, maxDistance * 0.5f, 1))
{
if (IsInRockPrefab(navmeshHit.position))
continue;
if (IsNearWorldCollider(navmeshHit.position))
continue;
return navmeshHit.position;
}
}
return null;
}
private static bool IsInRockPrefab(Vector3 position)
{
Physics.queriesHitBackfaces = true;
bool isInRock = Physics.Raycast(position, Vector3.up, out raycastHit, 20f, WORLD_LAYER, QueryTriggerInteraction.Ignore) &&
blockedColliders.Any(s => raycastHit.collider?.gameObject?.name.Contains(s) ?? false);
Physics.queriesHitBackfaces = false;
return isInRock;
}
private static bool IsNearWorldCollider(Vector3 position)
{
Physics.queriesHitBackfaces = true;
int count = Physics.OverlapSphereNonAlloc(position, 2f, _buffer, WORLD_LAYER, QueryTriggerInteraction.Ignore);
Physics.queriesHitBackfaces = false;
int removed = 0;
for (int i = 0; i < count; i++)
{
if (acceptedColliders.Any(s => _buffer[i].gameObject.name.Contains(s)))
removed++;
}
return removed != count;
}
private static string[] acceptedColliders = new string[] { "road", "rocket_factory", "train_track", "runway", "_grounds", "concrete_slabs", "office", "industrial", "junkyard" };
private static string[] blockedColliders = new string[] { "rock", "junk", "range", "invisible" };
#endregion
#region NPC Component
public class HumanAI : MonoBehaviour
{
internal ScientistNPC Entity { get; private set; }
internal Transform Transform { get; private set; }
internal CustomAIBrain Brain { get; set; }
internal AttackEntity _attackEntity;
internal Vector3 _roamBasePosition;
internal Settings _settings;
internal bool HasHomePosition { get; private set; }
internal float DistanceFromBase => Vector3.Distance(Transform.position, _roamBasePosition);
internal bool _NavMeshEnabled
{
get
{
return (bool)typeof(global::HumanNPC).GetField("navmeshEnabled", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance).GetValue(Entity);
}
set
{
typeof(global::HumanNPC).GetField("navmeshEnabled", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance).SetValue(Entity, value);
}
}
internal bool NavMeshEnabled { get; set; }
private const int AREA_MASK = 1;
private const int AGENT_TYPE_ID = -1372625422;
private void Awake()
{
Entity = GetComponent<ScientistNPC>();
Transform = Entity.transform;
_roamBasePosition = Transform.position;
}
private void Start()
{
//Destroy(GetComponent<HumanBrain>());
//Entity._brain = Brain = gameObject.AddComponent<CustomAIBrain>();
//Brain.UseAIDesign = false;
}
internal void Setup(Settings settings)
{
this._settings = settings;
Entity.CancelInvoke(Entity.EnableNavAgent);
Entity.CancelInvoke(Entity.EquipTest);
if (settings.StripCorpseLoot)
Entity.LootSpawnSlots = new LootContainer.LootSpawnSlot[0];
if (!string.IsNullOrEmpty(settings.DisplayName))
Entity.displayName = settings.DisplayName;
if (settings.StartDead)
{
UpdateGear();
Entity.Invoke(Die, 1f);
return;
}
if (settings.SightRange != 0f)
{
Entity.sightRange = settings.SightRange;
Entity.sightRangeLarge = settings.SightRange * 3;
}
Entity.NavAgent.areaMask = AREA_MASK;
Entity.NavAgent.agentTypeID = AGENT_TYPE_ID;
if (settings.EnableNavMesh)
Entity.Invoke(EnableNavAgent, 0.25f);
if (settings.Health != 0f)
Entity.InitializeHealth(settings.Health, settings.Health);
if (settings.DespawnTime != 0f)
Entity.Invoke(Despawn, settings.DespawnTime);
UpdateGear();
if (settings.StartWounded)
Entity.SetPlayerFlag(BasePlayer.PlayerFlags.Wounded, true);
else
{
if (settings.EquipWeapon)
Entity.Invoke(EquipWeapon, 0.25f);
}
if (_settings.NPCType.Equals("Murderer"))
{
Entity.DeathEffects = ZombieDeathEffects;
Entity.RadioChatterEffects = ZombieChatterEffects;
Entity.IdleChatterRepeatRange = new Vector2(10f, 15f);
}
Entity.InvokeRandomized(UpdateTick, 1f, 4f, 1f);
}
internal void AddStates()
{
Brain.ClearStates();
for (int i = 0; i < _settings.States.Count; i++)
{
string state = _settings.States[i];
switch (state)
{
case "IdleState":
Brain.AddState(new CustomAIBrain.IdleState(this));
break;
case "RoamState":
Brain.AddState(new CustomAIBrain.RoamState(this, _settings.RoamRange));
break;
case "ChaseState":
Brain.AddState(new CustomAIBrain.ChaseState(this, _settings.ChaseRange));
break;
case "CombatState":
Brain.AddState(new CustomAIBrain.CombatState(this, _settings.ChaseRange));
break;
case "MountedState":
Brain.AddState(new CustomAIBrain.MountedState(this, _settings.CanUseWeaponMounted));
break;
case "MoveDestinationState":
Brain.AddState(new CustomAIBrain.MoveDestinationState(this));
break;
case "FallingState":
Brain.AddState(new CustomAIBrain.FallingState(this));
break;
default:
break;
}
}
if (_settings.StartWounded && _settings.WoundedDuration != 0f)
Brain.AddState(new CustomAIBrain.WoundedState(this, _settings.WoundedDuration, _settings.WoundedRecoveryChance));
}
private void UpdateGear()
{
if (!string.IsNullOrEmpty(_settings.Kit))
{
StripInventory(Entity);
Instance.Kits?.Call("GiveKit", Entity, _settings.Kit);
}
else
{
if (_settings.NPCType == "Murderer")
{
StripInventory(Entity);
ItemManager.CreateByName("halloween.surgeonsuit").MoveToContainer(Entity.inventory.containerWear);
ItemManager.CreateByName("gloweyes").MoveToContainer(Entity.inventory.containerWear);
ItemManager.CreateByName("machete").MoveToContainer(Entity.inventory.containerBelt);
}
else if (_settings.NPCType == "Scientist")
{
StripInventory(Entity);
ItemManager.CreateByName("hazmatsuit_scientist").MoveToContainer(Entity.inventory.containerWear);
switch (UnityEngine.Random.Range(0, 2))
{
case 0:
ItemManager.CreateByName("smg.mp5").MoveToContainer(Entity.inventory.containerBelt, 0);
break;
case 1:
ItemManager.CreateByName("shotgun.spas12").MoveToContainer(Entity.inventory.containerBelt, 0);
break;
default:
ItemManager.CreateByName("pistol.m92").MoveToContainer(Entity.inventory.containerBelt, 0);
break;
}
}
}
}
internal void EnableNavAgent()
{
NavMeshHit navMeshHit;
if (!NavMesh.SamplePosition(Transform.position + (Vector3.up * 1f), out navMeshHit, 20f, -1))
{
Debug.Log("Failed to sample navmesh");
return;
}
Entity.NavAgent.Warp(navMeshHit.position);
Transform.position = navMeshHit.position;
_NavMeshEnabled = NavMeshEnabled = true;
Entity.NavAgent.enabled = true;
Entity.NavAgent.isStopped = false;
SetDestination(Transform.position);
}
internal void DisableNavAgent()
{
if (!_NavMeshEnabled)
return;
Entity.NavAgent.destination = Transform.position;
Entity.NavAgent.isStopped = true;
Entity.NavAgent.enabled = false;
_NavMeshEnabled = NavMeshEnabled = false;
}
internal void SetDestination(Vector3 destination)
{
if (NavMeshEnabled)
{
if (!_NavMeshEnabled)
_NavMeshEnabled = true;
Entity.NavAgent.enabled = true;
Entity.NavAgent.isStopped = false;
Entity.SetDestination(destination);
}
}
internal void SetDestination(Vector3 destination, global::HumanNPC.SpeedType speed, Action onReachedDestination)
{
CustomAIBrain.MoveDestinationState moveDestinationState = Brain.GetState<CustomAIBrain.MoveDestinationState>() as CustomAIBrain.MoveDestinationState;
if (moveDestinationState != null)
moveDestinationState.SetDestination(destination, speed, onReachedDestination);
}
internal void ForgetEntity(BaseEntity baseEntity)
{
Entity.myMemory.Players.Remove(baseEntity);
Entity.myMemory.Targets.Remove(baseEntity);
Entity.myMemory.Threats.Remove(baseEntity);
Entity.myMemory.Friendlies.Remove(baseEntity);
Entity.myMemory.LOS.Remove(baseEntity);
}
private void UpdateTick()
{
if (Entity == null || Entity.IsDestroyed)
{
Destroy(this);
return;
}
if (_attackEntity == null)
_attackEntity = Entity.GetAttackEntity();
}
internal void EquipWeapon()
{
if (Entity == null || Entity.IsDestroyed || Entity.IsDead())
return;
if (Entity.isMounted && !_settings.CanUseWeaponMounted)
return;
for (int i = 0; i < Entity.inventory.containerBelt.itemList.Count; i++)
{
Item slot = Entity.inventory.containerBelt.GetSlot(i);
if (slot != null)
{
Entity.UpdateActiveItem(Entity.inventory.containerBelt.GetSlot(i).uid);
BaseEntity heldEntity = slot.GetHeldEntity();
if (heldEntity != null)
{
_attackEntity = heldEntity.GetComponent<AttackEntity>();
if (_attackEntity != null)
_attackEntity.TopUpAmmo();
if (_attackEntity is BaseProjectile)
_attackEntity.effectiveRange *= 2f;
}
return;
}
}
}
internal void HolsterWeapon()
{
Entity.svActiveItemID = 0;
Item activeItem = Entity.GetActiveItem();
if (activeItem != null)
{
HeldEntity heldEntity = activeItem.GetHeldEntity() as HeldEntity;
if (heldEntity != null)
{
heldEntity.SetHeld(false);
}
}
Entity.SendNetworkUpdate(BasePlayer.NetworkQueue.Update);
Entity.inventory.UpdatedVisibleHolsteredItems();
}
internal void Die()
{
if (Entity == null || Entity.IsDestroyed || Entity.IsDead())
return;
Entity.Die(new HitInfo(Entity, Entity, DamageType.Explosion, 1000f));
}
internal void Die(HitInfo hitInfo)
{
if (Entity == null || Entity.IsDestroyed || Entity.IsDead())
return;
Entity.Die(hitInfo);
}
internal void Despawn()
{
if (Entity == null || Entity.IsDestroyed || Entity.IsDead())
return;
StripInventory(Entity);
Entity.Kill();
}
private void OnDestroy()
{
Facepunch.Pool.FreeList(ref friendlies);
Destroy(Brain);
if (Entity != null)
{
Entity.CancelInvoke(EnableNavAgent);
Entity.CancelInvoke(EquipWeapon);
Entity.CancelInvoke(UpdateTick);
Entity.CancelInvoke(Despawn);
Entity.CancelInvoke(Die);
}
}
private readonly static GameObjectRef[] ZombieDeathEffects = new GameObjectRef[]
{
new GameObjectRef(){ guid = GameManifest.guidToPath.FirstOrDefault(x => x.Value.Equals("assets/prefabs/npc/murderer/sound/death.prefab")).Key }
};
private readonly static GameObjectRef[] ZombieChatterEffects = new GameObjectRef[]
{
new GameObjectRef(){ guid = GameManifest.guidToPath.FirstOrDefault(x => x.Value.Equals("assets/prefabs/npc/murderer/sound/breathing.prefab")).Key }
};
#region Senses
private float nextSenseUpdateTime;
private float nextFriendlyUpdateTime;
private const float FRIENDLY_UPDATE_INTERVAL = 5f;
private const float SENSE_UPDATE_INTERVAL = 0.5f;
private const float SENSE_RANGE = 40f;
private const float VISION_CONE = -0.4f;
private static readonly BaseEntity[] QueryResults = new BaseEntity[64];
private static readonly BasePlayer[] PlayerQueryResults = new BasePlayer[64];
private List<HumanAI> friendlies = Facepunch.Pool.GetList<HumanAI>();
internal void UpdateSenses()
{
if (Entity == null)
return;
if (Time.time < nextSenseUpdateTime)
return;
nextSenseUpdateTime = Time.time + SENSE_UPDATE_INTERVAL;
SenseBrains();
SensePlayers();
}
internal void UpdateNearbyFriendlies()
{
if (Entity == null)
return;
if (Time.time < nextFriendlyUpdateTime)
return;
nextFriendlyUpdateTime = Time.time + FRIENDLY_UPDATE_INTERVAL;
friendlies.Clear();
BaseEntity.Query.Server.GetPlayersInSphere(Transform.position, 40f, PlayerQueryResults, new Func<BasePlayer, bool>(IsFriendlyPlayer));
}
private void SenseBrains()
{
int brainsInSphere = BaseEntity.Query.Server.GetBrainsInSphere(Transform.position, _settings.SightRange, QueryResults, new Func<BaseEntity, bool>(CaresAbout));
for (int i = 0; i < brainsInSphere; i++)
Entity.myMemory.SetKnown(QueryResults[i], Entity, null);
}
private void SensePlayers()
{
ScientistNPC scientistNPC = Entity;
int playersInSphere = BaseEntity.Query.Server.GetPlayersInSphere(Transform.position, _settings.SightRange, PlayerQueryResults, new Func<BasePlayer, bool>(CaresAbout));
for (int i = 0; i < playersInSphere; i++)
{
BasePlayer result = PlayerQueryResults[i];
Entity.myMemory.SetKnown(result, Entity, null);
for (int y = 0; y < friendlies.Count; y++)
{
scientistNPC = friendlies[y]?.Entity;
if (scientistNPC != null)
scientistNPC.myMemory.SetKnown(result, scientistNPC, null);
}
}
}
private bool IsFriendlyPlayer(BasePlayer player)
{
if (!player?.IsNpc ?? false)
return false;
HumanAI humanAI = player.GetComponent<HumanAI>();
if (humanAI != null)
{
friendlies.Add(humanAI);
return true;
}
return false;
}
private bool CaresAbout(BaseEntity entity)
{
if (entity == null)
return false;
if (entity.EqualNetID(Entity))
return false;
if (entity.Health() <= 0f)
return false;
if (!CanSenseType(entity))
return false;
if (entity is BasePlayer && (entity as BasePlayer).InSafeZone())
return false;
if (entity is BaseCombatEntity && (entity as BaseCombatEntity).TimeSinceLastNoise <= 1f && (entity as BaseCombatEntity).CanLastNoiseBeHeard(Transform.position, SENSE_RANGE))
return true;
if (!IsTargetInVision(entity))
return false;
bool hasLOS = CanSeeTarget(entity);
Entity.myMemory.SetLOS(entity, hasLOS);
if (!hasLOS)
return false;
return true;
}
private bool CanSenseType(BaseEntity ent)
{
BasePlayer basePlayer = ent as BasePlayer;
if (basePlayer != null && !basePlayer.IsNpc)
return true;
if (ent is BaseNpc)
return true;
if (ent is TimedExplosive)
return true;
return false;
}
private bool CanSeeTarget(BaseEntity entity)
{
BasePlayer basePlayer = entity as BasePlayer;
if (basePlayer == null)
return true;
return Entity.IsPlayerVisibleToUs(basePlayer);
}
private bool IsTargetInVision(BaseEntity target)
{
Vector3 vector3 = Vector3Ex.Direction(target.transform.position, Transform.position);
return Vector3.Dot((Entity != null ? Entity.eyes.BodyForward() : Transform.forward), vector3) >= VISION_CONE;
}
#endregion
}
#endregion
#region States
public class CustomAIBrain : BaseAIBrain<global::HumanNPC>
{
private HumanAI humanAI;
private void Awake()
{
humanAI = GetComponent<HumanAI>();
}
public override void AddStates()
{
base.AddStates();
humanAI.AddStates();
}
public override void InitializeAI()
{
global::HumanNPC humanNpc = GetEntity();
UseAIDesign = false;
base.InitializeAI();
ThinkMode = AIThinkMode.Interval;
thinkRate = 0.25f;
PathFinder = new HumanPathFinder();
((HumanPathFinder)PathFinder).Init(humanNpc);
}
public override void Think(float delta)
{
if (!ConVar.AI.think || states == null)
return;
lastThinkTime = Time.time;
if (sleeping)
return;
humanAI.UpdateNearbyFriendlies();
humanAI.UpdateSenses();
if (CurrentState != null)
CurrentState.StateThink(delta);
if (CurrentState == null || CurrentState.CanLeave())
{
float highest = 0f;
BasicAIState state = null;
foreach (BasicAIState value in states.Values)
{
if (value == null || !value.CanEnter())
continue;
float weight = value.GetWeight();
if (weight <= highest)
continue;
highest = weight;
state = value;
}
if (state != CurrentState)
SwitchToState(state, -1);
}
}
internal void ClearStates()
{
if (states != null)
states.Clear();
}
internal BasicAIState GetState<T>() where T : BasicAIState
{
foreach(BasicAIState basicAIState in states.Values)
{
if (basicAIState is T)
return basicAIState;
}
return null;
}
public class IdleState : BaseAIBrain<global::HumanNPC>.BasicAIState
{
private readonly HumanAI humanAI;
public IdleState(HumanAI humanAI) : base(AIState.Idle)
{
this.humanAI = humanAI;
}
public override float GetWeight() => 0.1f;
public override void StateEnter()
{
humanAI.Entity.SetDesiredSpeed(global::HumanNPC.SpeedType.SlowWalk);
base.StateEnter();
}
}
public class WoundedState : BaseAIBrain<global::HumanNPC>.BasicAIState
{
private readonly HumanAI humanAI;
private readonly float woundedDuration;
private readonly float woundedRecoveryChance;
private bool isIncapacitated;
private Vector3 destination;
public WoundedState(HumanAI humanAI, float woundedDuration, float woundedRecoveryChance) : base(AIState.Orbit)
{
this.humanAI = humanAI;
this.woundedDuration = woundedDuration;
this.woundedRecoveryChance = woundedRecoveryChance;
}
public override float GetWeight() => humanAI.Entity.HasPlayerFlag(BasePlayer.PlayerFlags.Wounded) ? 100f : 0f;
public override void StateEnter()
{
base.StateEnter();
if (UnityEngine.Random.value > 0.5f)
{
humanAI.Entity.health = (float)UnityEngine.Random.Range(ConVar.Server.crawlingminhealth, ConVar.Server.crawlingmaxhealth);
humanAI.Entity.metabolism.bleeding.@value = 0f;
humanAI.Entity.healingWhileCrawling = 0f;
isIncapacitated = false;
destination = humanAI.Entity.ServerPosition;
humanAI.Entity.NavAgent.speed = BasePlayer.crawlSpeed * 0.75f;
}
else
{
humanAI.Entity.health = UnityEngine.Random.Range(2f, 6f);
humanAI.Entity.metabolism.bleeding.@value = 0f;
humanAI.Entity.healingWhileCrawling = 0f;
humanAI.Entity.SetPlayerFlag(BasePlayer.PlayerFlags.Incapacitated, true);
isIncapacitated = true;
}
humanAI.Entity.SetDesiredSpeed(global::HumanNPC.SpeedType.Crouch);
humanAI.Entity.SetServerFall(true);
humanAI.Entity.SendNetworkUpdateImmediate(false);
}
public override void StateLeave()
{
base.StateLeave();
humanAI.Entity.SetPlayerFlag(BasePlayer.PlayerFlags.Wounded, false);
humanAI.Entity.SetPlayerFlag(BasePlayer.PlayerFlags.Incapacitated, false);
humanAI.Entity.SetServerFall(false);
}
public override StateStatus StateThink(float delta)
{
base.StateThink(delta);
if (!humanAI.Entity.IsDead())
{
if (!isIncapacitated)
{
if (Vector3.Distance(humanAI.transform.position, destination) < 1f)
{
Vector2 random = UnityEngine.Random.insideUnitCircle.normalized * 25f;
object d = FindPointOnNavmesh(humanAI.transform.position + new Vector3(random.x, 0f, random.y), 4f);
if (d is Vector3)
{
destination = (Vector3)d;
humanAI.SetDestination(destination);
}
}
Vector3 dest = humanAI.Transform.position;
if (humanAI.Entity.HasPath)
dest = humanAI.Entity.NavAgent.nextPosition;
humanAI.Entity.ServerPosition = dest;
humanAI.Entity.SetAimDirection(humanAI.Entity.GetAimDirection());
}
if (TimeInState >= woundedDuration)
{
if (UnityEngine.Random.Range(0, 100) >= woundedRecoveryChance)
humanAI.Die();
else
{
humanAI.Entity.SetPlayerFlag(BasePlayer.PlayerFlags.Wounded, false);
humanAI.EquipWeapon();
}
return StateStatus.Finished;
}
}
return StateStatus.Running;
}
}
public class ChaseState : BaseAIBrain<global::HumanNPC>.BasicAIState
{
private readonly HumanAI humanAI;
private readonly float chaseRange;
public ChaseState(HumanAI humanAI, float chaseRange) : base(AIState.Chase)
{
this.humanAI = humanAI;
this.chaseRange = chaseRange;
}
public override float GetWeight()
{
float weight = 0f;
if (!humanAI.Entity.HasTarget())
return 0f;
if (humanAI.HasHomePosition && humanAI.DistanceFromBase > chaseRange)
return 0f;
if ((humanAI.Entity.currentTarget as BasePlayer)?.IsFlying ?? false)
{
humanAI.ForgetEntity(humanAI.Entity.currentTarget);
humanAI.Entity.currentTarget = null;
humanAI.Entity.currentTargetLOS = false;
return 0f;
}
if (humanAI._attackEntity is BaseProjectile && (humanAI.Entity.AmmoFractionRemaining() < 0.3f || humanAI.Entity.IsReloading()))
weight -= 1f;
if (humanAI.Entity.HasTarget())
weight += 0.5f;
if (humanAI.Entity.CanSeeTarget())
weight -= 0.5f;
else weight += 1f;
if (!(humanAI._attackEntity is BaseProjectile) || humanAI.Entity.DistanceToTarget() > humanAI.Entity.GetIdealDistanceFromTarget())
weight += 1f;
return weight;
}
public override void StateEnter()
{
base.StateEnter();
humanAI.Entity.SetDesiredSpeed(global::HumanNPC.SpeedType.Walk);
}
public override StateStatus StateThink(float delta)
{
base.StateThink(delta);
if (!humanAI.Entity.HasTarget())
return StateStatus.Error;
bool hasProjectileWeapon = humanAI._attackEntity is BaseProjectile;
float distanceToTarget = Vector3.Distance(humanAI.Entity.currentTarget.transform.position, humanAI.Transform.position);
if (!hasProjectileWeapon)
humanAI.Entity.SetDesiredSpeed(global::HumanNPC.SpeedType.Sprint);
else humanAI.Entity.SetDesiredSpeed(distanceToTarget < 5f ? global::HumanNPC.SpeedType.SlowWalk :
distanceToTarget >= 10f ? global::HumanNPC.SpeedType.Sprint :
global::HumanNPC.SpeedType.Walk);
Vector3 position = humanAI.Transform.position;
if (!hasProjectileWeapon)
humanAI.SetDestination(humanAI.Entity.currentTarget.transform.position);
else
{
AIInformationZone aiInformationZone = humanAI.Entity.GetInformationZone(humanAI.Entity.currentTarget.transform.position);
if (aiInformationZone != null)
{
AIMovePoin
// ... [Code preview truncated due to length. Full source in attached file] ...