C#:
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Oxide.Core.Plugins;
using Rust;
using Rust.Ai;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.AI;
using Random = UnityEngine.Random;
namespace Oxide.Plugins {
using CustomNPCEx;
[Info("CustomNPC", "k1lly0u", "2.0.5")]
public class CustomNPC : RustPlugin, ICustomNPCPlugin {
#region Fields
[PluginReference]
private Plugin Kits;
private static Hash<Plugin, List<CustomScientistNPC>> _npcLookup;
private static CustomNPC Instance;
#endregion
#region Oxide Hooks
private void Loaded() {
Instance = this;
_npcLookup = new Hash<Plugin, List<CustomScientistNPC>>();
}
private void OnEntityKill(CustomScientistNPC customNpc) {
if (customNpc == null || customNpc.Plugin == null) return;
_npcLookup[customNpc.Plugin].Remove(customNpc);
}
private void OnEntityTakeDamage(CustomScientistNPC customNpc, HitInfo hitInfo) {
if (customNpc == null || hitInfo == null || hitInfo.InitiatorPlayer == null) return;
CustomScientistNPC initiator = hitInfo.InitiatorPlayer as CustomScientistNPC;
if (initiator != null && customNpc.Settings.PreventFriendlyFire) {
if (customNpc.Plugin == initiator.Plugin) {
hitInfo.damageTypes.Clear();
hitInfo.HitEntity = null;
hitInfo.HitMaterial = 0;
hitInfo.PointStart = Vector3.zero;
}
}
}
private object OnNpcTarget(CustomScientistNPC customNpc, BasePlayer target) {
if (customNpc == null) return null;
if (target.IsSleeping() || target.IsFlying) return true;
return null;
}
private object CanLootPlayer(CustomScientistNPC customNpc, BasePlayer player) => customNpc != null && customNpc.IsWounded() ? (object)false : null;
private object OnPlayerAssist(CustomScientistNPC customNpc, BasePlayer player) => false;
private void OnPluginUnloaded(Plugin plugin) {
List<CustomScientistNPC> list;
if (_npcLookup.TryGetValue(plugin, out list)) {
for (int i = list.Count - 1; i >= 0; i--) {
CustomScientistNPC customNpc = list[i];
if (customNpc != null) customNpc.Kill(BaseNetworkable.DestroyMode.None);
}
_npcLookup.Remove(plugin);
}
}
private void Unload() {
foreach (KeyValuePair<Plugin, List<CustomScientistNPC>> kvp in _npcLookup) {
for (int i = kvp.Value.Count - 1; i >= 0; i--) {
CustomScientistNPC customNpc = kvp.Value[i];
if (customNpc != null) customNpc.Kill(BaseNetworkable.DestroyMode.None);
}
}
_npcLookup.Clear();
_npcLookup = null;
Instance = null;
}
#endregion
#region Functions
public static void CopySerializeableFields<T>(T src, T dst) {
FieldInfo[] srcFields = typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance);
foreach (FieldInfo field in srcFields) {
object value = field.GetValue(src);
field.SetValue(dst, value);
}
}
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;
}
#endregion
#region API
public static CustomScientistNPC SpawnNPC(Plugin plugin, Vector3 position, NPCSettings settings) {
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;
}
if (!(plugin is ICustomNPCPlugin)) {
Debug.LogError($"Plugin {plugin.Name} is attempting to create a CustomNPC but is not setup correctly");
return null;
}
CustomScientistNPC customNpc = CreateCustomNPC(position, settings, plugin);
if (customNpc == null) return null;
RegisterNPC(plugin, customNpc);
return customNpc;
}
public static void RegisterNPC(Plugin plugin, CustomScientistNPC customNpc) {
List<CustomScientistNPC> list;
if (!_npcLookup.TryGetValue(plugin, out list)) list = _npcLookup[plugin] = new List<CustomScientistNPC>();
list.Add(customNpc);
}
private static CustomScientistNPC CreateCustomNPC(Vector3 position, NPCSettings settings, Plugin plugin) {
if (settings.EnableNavMesh && NavmeshSpawnPoint.Find(position, 60, out position)) {
if (settings.EnableNavMesh && (position.y < -0.25f || (settings.KillInSafeZone && IsInSafeZone(position)))) return null;
}
const string SCIENTIST_PREFAB = "assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_heavy.prefab";
ScientistNPC scientistNPC = GameManager.server.CreateEntity(SCIENTIST_PREFAB, position, Quaternion.identity, false) as ScientistNPC;
ScientistBrain scientistBrain = scientistNPC.GetComponent<ScientistBrain>();
NPCPlayerNavigator scientistNavigator = scientistNPC.GetComponent<NPCPlayerNavigator>();
CustomScientistNPC customScientist = scientistNPC.gameObject.AddComponent<CustomScientistNPC>();
CustomScientistBrain customScientistBrain = scientistNPC.gameObject.AddComponent<CustomScientistBrain>();
CustomScientistNavigator customScientistNavigator = scientistNPC.gameObject.AddComponent<CustomScientistNavigator>();
CopySerializeableFields<ScientistNPC>(scientistNPC, customScientist);
CopySerializeableFields<NPCPlayerNavigator>(scientistNavigator, customScientistNavigator);
customScientist.enableSaving = false;
customScientist.Plugin = plugin;
customScientist.Settings = settings;
customScientistBrain.UseQueuedMovementUpdates = scientistBrain.UseQueuedMovementUpdates;
customScientistBrain.AllowedToSleep = false;
customScientistBrain.DefaultDesignSO = scientistBrain.DefaultDesignSO;
customScientistBrain.Designs = new List<AIDesignSO>(scientistBrain.Designs);
customScientistBrain.InstanceSpecificDesign = scientistBrain.InstanceSpecificDesign;
customScientistBrain.CheckLOS = scientistBrain.CheckLOS;
customScientistBrain.UseAIDesign = true;
customScientistBrain.Pet = false;
scientistBrain._baseEntity = scientistNPC;
UnityEngine.Object.DestroyImmediate(scientistNavigator, true);
UnityEngine.Object.DestroyImmediate(scientistBrain, true);
UnityEngine.Object.DestroyImmediate(scientistNPC, true);
customScientist.gameObject.AwakeFromInstantiate();
customScientist.Spawn();
return customScientist;
}
public static void SetRoamHomePosition(CustomScientistNPC customNpc, Vector3 position) {
if (customNpc == null) return;
customNpc.HomePosition = position;
if (customNpc.CurrentState == AIState.Roam) customNpc.Brain.SwitchToState(AIState.Idle, 0);
}
public static void SetDestination(CustomScientistNPC customNpc, Vector3 destination, Action onDestinationReached = null) {
if (customNpc == null) return;
if (NavmeshSpawnPoint.Find(destination, 20, out destination)) customNpc.SetDestination(destination, onDestinationReached);
}
#endregion
#region Settings
[Serializable]
public class NPCSettings {
[JsonProperty(PropertyName = "NPC types (HeavyScientist, Scientist, Scarecrow, BanditGuard, TunnelDweller)")]
public NPCType[] Types {
get;
set;
}
= new NPCType[] {
NPCType.HeavyScientist
}
;
[JsonProperty(PropertyName = "Display names (Chosen at random)")]
public string[] DisplayNames {
get;
set;
}
= new string[0];
[JsonProperty(PropertyName = "Kits (Chosen at random)")]
public string[] Kits {
get;
set;
}
= new string[0];
[JsonProperty(PropertyName = "Don't drop loot with corpse")]
public bool StripCorpseLoot {
get;
set;
}
[JsonProperty(PropertyName = "Drop inventory as loot")]
public bool DropInventoryOnDeath {
get;
set;
}
[JsonProperty(PropertyName = "Max roam range")]
public float RoamRange {
get;
set;
}
= -1f;
[JsonProperty(PropertyName = "Max chase range")]
public float ChaseRange {
get;
set;
}
= -1f;
[JsonProperty(PropertyName = "Aim cone scale")]
public float AimConeScale {
get;
set;
}
= 2f;
[JsonProperty(PropertyName = "Kill in safe zone")]
public bool KillInSafeZone {
get;
set;
}
= true;
[JsonProperty(PropertyName = "Despawn time (seconds)")]
public float DespawnTime {
get;
set;
}
= 0f;
[JsonIgnore]
public bool StartDead {
get;
set;
}
[JsonProperty(PropertyName = "Wounded chance (x out of 100)")]
public float WoundedChance {
get;
set;
}
= 0f;
[JsonProperty(PropertyName = "Wounded duration min (seconds)")]
public float WoundedDurationMin {
get;
set;
}
= 0f;
[JsonProperty(PropertyName = "Wounded duration max (seconds)")]
public float WoundedDurationMax {
get;
set;
}
= 0f;
[JsonProperty(PropertyName = "Wounded recovery chance (x out of 100)")]
public float WoundedRecoveryChance {
get;
set;
}
= 100f;
[JsonProperty(PropertyName = "Prevent friendly fire")]
public bool PreventFriendlyFire {
get;
set;
}
= true;
[JsonIgnore]
public bool EnableNavMesh {
get;
set;
}
= true;
[JsonIgnore]
public bool EquipWeapon {
get;
set;
}
= true;
[JsonIgnore]
public bool CanUseWeaponMounted {
get;
set;
}
= false;
[JsonProperty(PropertyName = "Kill if under water")]
public bool KillUnderWater {
get;
set;
}
= true;
public VitalStats Vitals {
get;
set;
}
= new VitalStats();
public MovementStats Movement {
get;
set;
}
= new MovementStats();
public SensoryStats Sensory {
get;
set;
}
= new SensoryStats();
public class VitalStats {
public float Health {
get;
set;
}
= 200f;
}
public class MovementStats {
public float Speed {
get;
set;
}
= 6.2f;
public float Acceleration {
get;
set;
}
= 12f;
[JsonProperty(PropertyName = "Turn speed")]
public float TurnSpeed {
get;
set;
}
= 120f;
[JsonProperty(PropertyName = "Speed multiplier - Slowest")]
public float SlowestSpeedFraction {
get;
set;
}
= 0.1f;
[JsonProperty(PropertyName = "Speed multiplier - Slow")]
public float SlowSpeedFraction {
get;
set;
}
= 0.3f;
[JsonProperty(PropertyName = "Speed multiplier - Normal")]
public float NormalSpeedFraction {
get;
set;
}
= 0.5f;
[JsonProperty(PropertyName = "Speed multiplier - Fast")]
public float FastSpeedFraction {
get;
set;
}
= 1f;
[JsonProperty(PropertyName = "Speed multiplier - Low health")]
public float LowHealthMaxSpeedFraction {
get;
set;
}
= 0.5f;
public void ApplySettingsToNavigator(BaseNavigator baseNavigator) {
baseNavigator.Acceleration = Acceleration;
baseNavigator.FastSpeedFraction = FastSpeedFraction;
baseNavigator.LowHealthMaxSpeedFraction = LowHealthMaxSpeedFraction;
baseNavigator.NormalSpeedFraction = NormalSpeedFraction;
baseNavigator.SlowestSpeedFraction = SlowestSpeedFraction;
baseNavigator.SlowSpeedFraction = SlowSpeedFraction;
baseNavigator.Speed = Speed;
baseNavigator.TurnSpeed = TurnSpeed;
baseNavigator.topologyPreference = (TerrainTopology.Enum)1673010749;
}
}
public class SensoryStats {
[JsonProperty(PropertyName = "Attack range multiplier")]
public float AttackRangeMultiplier {
get;
set;
}
= 1.5f;
[JsonProperty(PropertyName = "Sense range")]
public float SenseRange {
get;
set;
}
= 30f;
[JsonProperty(PropertyName = "Listen range")]
public float ListenRange {
get;
set;
}
= 20f;
[JsonProperty(PropertyName = "Target lost range")]
public float TargetLostRange {
get;
set;
}
= 90f;
[JsonProperty(PropertyName = "Target lost range time (seconds)")]
public float TargetLostRangeTime {
get;
set;
}
= 5f;
[JsonProperty(PropertyName = "Target lost LOS time (seconds)")]
public float TargetLostLOSTime {
get;
set;
}
= 5f;
[JsonProperty(PropertyName = "Ignore sneaking outside of vision range")]
public bool IgnoreNonVisionSneakers {
get;
set;
}
= true;
[JsonProperty(PropertyName = "Vision cone (0 - 180 degrees)")]
public float VisionCone {
get;
set;
}
= 135f;
[JsonProperty(PropertyName = "Ignore players in safe zone")]
public bool IgnoreSafeZonePlayers {
get;
set;
}
= true;
public void ApplySettingsToBrain(BaseAIBrain<global : : HumanNPC> brain) {
brain.MaxGroupSize = int.MaxValue;
brain.AttackRangeMultiplier = 1f;
brain.SenseRange = SenseRange;
brain.ListenRange = ListenRange;
brain.TargetLostRange = TargetLostRange;
brain.CheckVisionCone = IgnoreNonVisionSneakers;
brain.IgnoreNonVisionSneakers = IgnoreNonVisionSneakers;
brain.IgnoreSafeZonePlayers = IgnoreSafeZonePlayers;
brain.VisionCone = Vector3.Dot(Vector3.forward, Quaternion.Euler(0f, VisionCone, 0f) * Vector3.forward);
}
}
}
[JsonConverter(typeof(StringEnumConverter))]
public enum NPCType {
HeavyScientist, Scientist, Scarecrow, BanditGuard, TunnelDweller
}
#endregion
#region NavMesh
public static class NavmeshSpawnPoint {
private static NavMeshHit navmeshHit;
private static RaycastHit raycastHit;
private static readonly Collider[] _buffer = new Collider[256];
private const int WORLD_LAYER = 65536;
public static bool Find(Vector3 targetPosition, float maxDistance, out Vector3 position) {
for (int i = 0; i < 10; i++) {
position = i == 0 ? targetPosition : targetPosition + (Random.onUnitSphere.XZ3D() * maxDistance);
if (NavMesh.SamplePosition(position, out navmeshHit, maxDistance, 1)) {
if (IsInRockPrefab(navmeshHit.position)) continue;
if (IsNearWorldCollider(navmeshHit.position)) continue;
if (navmeshHit.position.y < TerrainMeta.WaterMap.GetHeight(navmeshHit.position)) continue;
position = navmeshHit.position;
return true;
}
}
position = default(Vector3);
return false;
}
private static bool IsInRockPrefab(Vector3 position) {
Physics.queriesHitBackfaces = true;
bool isInRock = Physics.Raycast(position, Vector3.up, out raycastHit, 20f, WORLD_LAYER, QueryTriggerInteraction.Ignore) && BLOCKED_COLLIDERS.Any(s => raycastHit.collider?.gameObject?.name.Contains(s, CompareOptions.OrdinalIgnoreCase) ?? 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 (ACCEPTED_COLLIDERS.Any(s => _buffer[i].gameObject.name.Contains(s, CompareOptions.OrdinalIgnoreCase))) removed++;
}
return count - removed > 0;
}
private static readonly string[] ACCEPTED_COLLIDERS = new string[] {
"road", "carpark", "rocket_factory", "range", "train_track", "runway", "_grounds", "concrete_slabs", "lighthouse", "cave", "office", "walkways", "sphere", "tunnel", "industrial", "junkyard", "iceberg", "temperate"
}
;
private static readonly string[] BLOCKED_COLLIDERS = new string[] {
"rock", "cliff", "junk", "range", "invisible"
}
;
}
#endregion
#region Loadouts
private static Definitions _scientistDefinition;
private static Definitions _scarecrowDefinition;
private static Definitions _tunnelDwellerDefinition;
private static Definitions _banditGuardDefinition;
private static Definitions ScientistDefinition {
get {
if (_scientistDefinition == null) {
_scientistDefinition = new Definitions();
ScientistNPC scientistNPC = GameManager.server.FindPrefab("assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_roam.prefab").GetComponent<ScientistNPC>();
_scientistDefinition.LootSpawns = scientistNPC.LootSpawnSlots;
_scientistDefinition.Loadouts = scientistNPC.loadouts;
}
return _scientistDefinition;
}
}
private static Definitions ScarecrowDefinition {
get {
if (_scarecrowDefinition == null) {
_scarecrowDefinition = new Definitions();
ScarecrowNPC scarecrowNPC = GameManager.server.FindPrefab("assets/prefabs/npc/scarecrow/scarecrow.prefab").GetComponent<ScarecrowNPC>();
_scarecrowDefinition.LootSpawns = scarecrowNPC.LootSpawnSlots;
_scarecrowDefinition.Loadouts = scarecrowNPC.loadouts;
}
return _scarecrowDefinition;
}
}
private static Definitions TunnelDwellerDefinition {
get {
if (_tunnelDwellerDefinition == null) {
_tunnelDwellerDefinition = new Definitions();
TunnelDweller tunnelDweller = GameManager.server.FindPrefab("assets/rust.ai/agents/npcplayer/humannpc/tunneldweller/npc_tunneldweller.prefab").GetComponent<TunnelDweller>();
_tunnelDwellerDefinition.LootSpawns = tunnelDweller.LootSpawnSlots;
_tunnelDwellerDefinition.Loadouts = tunnelDweller.loadouts;
}
return _tunnelDwellerDefinition;
}
}
private static Definitions BanditGuardDefinition {
get {
if (_banditGuardDefinition == null) {
_banditGuardDefinition = new Definitions();
BanditGuard scarecrowNPC = GameManager.server.FindPrefab("assets/rust.ai/agents/npcplayer/humannpc/banditguard/npc_bandit_guard.prefab").GetComponent<BanditGuard>();
_banditGuardDefinition.LootSpawns = scarecrowNPC.LootSpawnSlots;
_banditGuardDefinition.Loadouts = scarecrowNPC.loadouts;
}
return _banditGuardDefinition;
}
}
private class Definitions {
public LootContainer.LootSpawnSlot[] LootSpawns;
public PlayerInventoryProperties[] Loadouts;
}
#endregion
#region NPC Component
public class CustomScientistNPC : ScientistNPC, IAIAttack {
internal static readonly Spatial.Grid<CustomScientistNPC> NPCGrid = new Spatial.Grid<CustomScientistNPC>(32, 8096f);
private static readonly CustomScientistNPC[] QueryResults = new CustomScientistNPC[4];
public Plugin Plugin {
get;
set;
}
public NPCSettings Settings {
get;
set;
}
public Transform Transform {
get;
private set;
}
public BaseEntity CurrentTarget {
get;
protected set;
}
public AttackEntity CurrentWeapon {
get;
private set;
}
public AIState CurrentState {
get;
internal set;
}
= AIState.Idle;
public Vector3 HomePosition {
get;
internal set;
}
public bool IsPaused {
get;
internal set;
}
public bool IsAlert {
get;
internal set;
}
private float nextAlertTime;
public Vector3 DestinationOverride {
get;
internal set;
}
internal Action onDestinationReached;
private bool isEquippingWeapon;
private NPCType npcType;
private const int AREA_MASK = 1;
private const int AGENT_TYPE_ID = -1372625422;
private static readonly LootContainer.LootSpawnSlot[] EMPTY_SLOTS = new LootContainer.LootSpawnSlot[0];
public override void ServerInit() {
Transform = transform;
HomePosition = transform.position;
npcType = Settings.Types.GetRandom();
loadouts = Settings.Kits?.Length > 0 ? null : GetLoadoutForType(npcType);
if (Settings.StripCorpseLoot) LootSpawnSlots = EMPTY_SLOTS;
else LootSpawnSlots = GetLootSpawnSlotsForType(npcType);
if (NavAgent == null) NavAgent = GetComponent<NavMeshAgent>();
NavAgent.areaMask = AREA_MASK;
NavAgent.agentTypeID = AGENT_TYPE_ID;
GetComponent<BaseNavigator>().DefaultArea = "Walkable";
startHealth = Settings.Vitals.Health;
base.ServerInit();
if (npcType != NPCType.HeavyScientist && npcType != NPCType.Scientist) {
CancelInvoke(PlayRadioChatter);
CancelInvoke(IdleCheck);
}
Invoke(SetDisplayName, 1f);
if (Settings.Kits?.Length > 0) Instance.Kits?.Call("GiveKit", this, Settings.Kits.GetRandom());
AdjustWeaponRanges();
bool startWounded = Settings.WoundedChance >= 100 || (Settings.WoundedChance > 0 && Random.Range(0f, 100f) <= Settings.WoundedChance);
if (!Settings.EquipWeapon || Settings.StartDead || startWounded) CancelInvoke(EquipTest);
if (Settings.StartDead) {
Invoke(KillSelf, 1.5f);
return;
}
if (startWounded) {
SetPlayerFlag(PlayerFlags.Wounded, true);
Invoke(() => Brain.SwitchToState(AIState.Orbit, 4), 1f);
}
if (Settings.DespawnTime > 0f) Invoke(Despawn, Settings.DespawnTime);
InvokeRepeating(LightCheck, 1f, 30f);
NPCGrid.Add(this, Transform.position.x, Transform.position.z);
}
public override void DestroyShared() {
NPCGrid.Remove(this);
base.DestroyShared();
}
private void SetDisplayName() => displayName = Settings.DisplayNames.Length > 0 ? Settings.DisplayNames.GetRandom() : npcType.ToString();
private void KillSelf() => Die(new HitInfo(this, this, DamageType.Explosion, 1000f));
private void Despawn() => Kill(DestroyMode.None);
#region Loadouts
private LootContainer.LootSpawnSlot[] GetLootSpawnSlotsForType(NPCType type) {
switch (type) {
case NPCType.HeavyScientist : return LootSpawnSlots;
case NPCType.Scientist : return ScientistDefinition.LootSpawns;
case NPCType.Scarecrow : return ScarecrowDefinition.LootSpawns;
case NPCType.BanditGuard : return BanditGuardDefinition.LootSpawns;
case NPCType.TunnelDweller : return TunnelDwellerDefinition.LootSpawns;
default : return null;
}
}
private PlayerInventoryProperties[] GetLoadoutForType(NPCType type) {
switch (type) {
case NPCType.HeavyScientist : return loadouts;
case NPCType.Scientist : return ScientistDefinition.Loadouts;
case NPCType.Scarecrow : return ScarecrowDefinition.Loadouts;
case NPCType.BanditGuard : return BanditGuardDefinition.Loadouts;
case NPCType.TunnelDweller : return TunnelDwellerDefinition.Loadouts;
default : return null;
}
}
#endregion
#region Targeting
public override void OnSensation(Sensation sensation) {
if (sensation.Initiator != null && !(sensation.Initiator is CustomScientistNPC) && CurrentState < AIState.Chase) MoveTowardsTarget(sensation.Initiator, false);
}
public new BaseEntity GetBestTarget() {
BaseEntity target = null;
float delta = -1f;
foreach (BaseEntity baseEntity in Brain.Senses.Memory.Targets) {
if (baseEntity == null || baseEntity.Health() <= 0f) continue;
if (!CanTargetEntity(baseEntity)) continue;
if (baseEntity is BasePlayer && !CanTargetBasePlayer(baseEntity as BasePlayer)) continue;
float distanceToTarget = Vector3.Distance(baseEntity.transform.position, Transform.position);
if (lastAttacker != baseEntity && distanceToTarget > Brain.Senses.TargetLostRange) continue;
float rangeDelta = 1f - Mathf.InverseLerp(1f, Brain.SenseRange, distanceToTarget);
float dot = Vector3.Dot((baseEntity.transform.position - eyes.position).normalized, eyes.BodyForward());
if (lastAttacker != baseEntity && Settings.Sensory.IgnoreNonVisionSneakers && dot < Brain.VisionCone) continue;
rangeDelta += Mathf.InverseLerp(Brain.VisionCone, 1f, dot) / 2f;
rangeDelta += (Brain.Senses.Memory.IsLOS(baseEntity) ? 2f : 0f);
if (lastAttacker != baseEntity && rangeDelta <= delta) continue;
target = baseEntity;
delta = rangeDelta;
}
CurrentTarget = target;
return target;
}
public virtual bool CanTargetBasePlayer(BasePlayer player) {
if (player.IsFlying) return false;
if (player.IsSleeping()) return false;
if (!player.IsNpc && !player.userID.IsSteamId() && player.faction != Faction.Horror) return false;
if (Settings.Sensory.IgnoreSafeZonePlayers && player.InSafeZone()) return false;
return true;
}
public bool CanTargetEntity(BaseEntity baseEntity) => baseEntity is BasePlayer || baseEntity is BaseNpc;
public bool IsValidDistance(float distanceToTarget) {
if (Settings.ChaseRange > 0f && distanceToTarget > Settings.ChaseRange) return false;
if (distanceToTarget > Brain.TargetLostRange) return false;
return true;
}
public void NotifyNearby() {
if (CurrentTarget == null) return;
int hits = NPCGrid.Query(Transform.position.x, Transform.position.z, 80f, QueryResults, (CustomScientistNPC found) => IsFellowNPC(this, found));
for (int i = 0; i < hits; i++) {
CustomScientistNPC customNpc = QueryResults[i];
if (customNpc != this && customNpc.CurrentState < AIState.Chase && customNpc.CurrentTarget == null) customNpc.MoveTowardsTarget(CurrentTarget, true);
}
}
public void MoveTowardsTarget(BaseEntity target, bool force) {
if (Time.time > nextAlertTime) {
Brain.Senses.Memory.SetKnown(target, this, null);
DestinationOverride = target.transform.position;
IsAlert = true;
nextAlertTime = Time.time + 3f;
Brain.SwitchToState(AIState.Idle, 0);
}
}
private static bool IsFellowNPC(CustomScientistNPC notifier, CustomScientistNPC found) => notifier.Plugin == found.Plugin && notifier != found;
#endregion
#region Equip Weapons
public override void EquipWeapon() {
if (!isEquippingWeapon) StartCoroutine(EquipItem());
}
public void HolsterWeapon() {
svActiveItemID = 0;
Item activeItem = GetActiveItem();
if (activeItem != null) {
HeldEntity heldEntity = activeItem.GetHeldEntity() as HeldEntity;
if (heldEntity != null) {
heldEntity.SetHeld(false);
}
}
SendNetworkUpdate(NetworkQueue.Update);
inventory.UpdatedVisibleHolsteredItems();
CurrentWeapon = null;
}
private IEnumerator EquipItem(Item slot = null) {
if (inventory != null && inventory.containerBelt != null) {
isEquippingWeapon = true;
if (slot == null) {
for (int i = 0; i < inventory.containerBelt.itemList.Count; i++) {
Item item = inventory.containerBelt.GetSlot(i);
if (item != null && item.GetHeldEntity() is AttackEntity) {
slot = item;
break;
}
}
}
if (slot != null) {
if (CurrentWeapon != null) {
CurrentWeapon.SetHeld(false);
CurrentWeapon = null;
SendNetworkUpdate(NetworkQueue.Update);
inventory.UpdatedVisibleHolsteredItems();
}
yield return CoroutineEx.waitForSeconds(0.5f);
UpdateActiveItem(slot.uid);
HeldEntity heldEntity = slot.GetHeldEntity() as HeldEntity;
if (heldEntity != null) {
if (heldEntity is AttackEntity) (heldEntity as AttackEntity).TopUpAmmo();
if (heldEntity is Chainsaw) (heldEntity as Chainsaw).ServerNPCStart();
}
CurrentWeapon = heldEntity as AttackEntity;
}
isEquippingWeapon = false;
}
}
#endregion
#region Corpses
protected override string OverrideCorpseName() => displayName;
public override BaseCorpse CreateCorpse() {
ResetModifiedWeaponRange();
NPCPlayerCorpse npcplayerCorpse = DropCorpse("assets/prefabs/npc/murderer/murderer_corpse.prefab") as NPCPlayerCorpse;
if (npcplayerCorpse) {
npcplayerCorpse.transform.position = npcplayerCorpse.transform.position + Vector3.down * NavAgent.baseOffset;
npcplayerCorpse.SetLootableIn(2f);
npcplayerCorpse.SetFlag(Flags.Reserved5, HasPlayerFlag(PlayerFlags.DisplaySash), false, true);
npcplayerCorpse.SetFlag(Flags.Reserved2, true, false, true);
npcplayerCorpse.TakeFrom(new ItemContainer[] {
inventory.containerMain, inventory.containerWear, inventory.containerBelt
}
);
npcplayerCorpse.playerName = displayName;
npcplayerCorpse.playerSteamID = userID;
npcplayerCorpse.Spawn();
npcplayerCorpse.TakeChildren(this);
if (Settings.DropInventoryOnDeath) {
npcplayerCorpse.containers[1].Clear();
}
else {
ItemContainer[] containers = npcplayerCorpse.containers;
for (int i = 0; i < containers.Length; i++) containers[i].Clear();
if ((Plugin as ICustomNPCPlugin).WantsToPopulateLoot(this, npcplayerCorpse)) return npcplayerCorpse;
if (LootSpawnSlots.Length != 0) {
object obj = Core.Interface.CallHook("OnCorpsePopulate", this, npcplayerCorpse);
if (obj is BaseCorpse) {
return (BaseCorpse)obj;
}
LootContainer.LootSpawnSlot[] lootSpawnSlots = this.LootSpawnSlots;
for (int i = 0; i < (int)lootSpawnSlots.Length;
i++) {
LootContainer.LootSpawnSlot lootSpawnSlot = lootSpawnSlots[i];
for (int j = 0; j < lootSpawnSlot.numberToSpawn; j++) {
if (Random.Range(0f, 1f) <= lootSpawnSlot.probability) {
lootSpawnSlot.definition.SpawnIntoContainer(npcplayerCorpse.containers[0]);
}
}
}
}
}
}
return npcplayerCorpse;
}
#endregion
#region Weapon Range
private void AdjustWeaponRanges() {
float effectiveRange;
for (int i = 0; i < inventory.containerBelt.itemList.Count; i++) {
Item item = inventory.containerBelt.itemList[i];
if (item != null) {
HeldEntity heldEntity = item.GetHeldEntity() as HeldEntity;
if (heldEntity != null) {
if (heldEntity is BaseProjectile) {
if (ProjectileEffectiveRange.TryGetValue(item.info.shortname, out effectiveRange)) {
if (!_effectiveRangeDefaults.ContainsKey(item.info.shortname)) _effectiveRangeDefaults[item.info.shortname] = (heldEntity as BaseProjectile).effectiveRange;
(heldEntity as BaseProjectile).effectiveRange = effectiveRange;
}
}
}
}
}
}
private void ResetModifiedWeaponRange() {
float effectiveRange;
for (int i = 0; i < inventory.containerBelt.itemList.Count; i++) {
Item item = inventory.containerBelt.itemList[i];
if (item != null) {
HeldEntity heldEntity = item.GetHeldEntity() as HeldEntity;
if (heldEntity != null) {
if (heldEntity is BaseProjectile) {
if (GetDefaultEffectiveRange(item.info.shortname, out effectiveRange)) (heldEntity as BaseProjectile).effectiveRange = effectiveRange;
}
}
}
}
}
private static readonly Hash<string, float> ProjectileEffectiveRange = new Hash<string, float> {
["bow.compound"]
= 40, ["bow.hunting"] = 30, ["crossbow"] = 40, ["flamethrower"] = 8, ["gun.water"] = 10, ["lmg.m249"] = 150, ["multiplegrenadelauncher"] = 40, ["pistol.eoka"] = 5, ["pistol.m92"] = 45, ["pistol.nailgun"] = 20, ["pistol.python"] = 45, ["pistol.revolver"] = 45, ["pistol.semiauto"] = 45, ["pistol.water"] = 10, ["rifle.ak"] = 80, ["rifle.bolt"] = 150, ["rifle.l96"] = 200, ["rifle.lr300"] = 100, ["rifle.m39"] = 120, ["rifle.semiauto"] = 120, ["rocket.launcher"] = 50, ["shotgun.double"] = 25, ["shotgun.pump"] = 25, ["shotgun.spas12"] = 25, ["shotgun.waterpipe"] = 15, ["smg.2"] = 40, ["smg.mp5"] = 60, ["smg.thompson"] = 50, ["snowballgun"] = 20, ["speargun"] = 20,
}
;
private static Hash<string, float> _effectiveRangeDefaults = new Hash<string, float>();
private static bool GetDefaultEffectiveRange(string shortname, out float value) => _effectiveRangeDefaults.TryGetValue(shortname, out value);
#endregion
public override string Categorize() => Plugin.Name;
public override float GetAimConeScale() => Settings.AimConeScale;
internal void SetDestination(Vector3 destination, Action onDestinationReached) {
Brain.Events.Memory.Position.Set(destination, 5);
this.onDestinationReached = onDestinationReached;
Brain.SwitchToState(AIState.MoveToPoint, 3);
}
public void SetPaused(bool paused) => IsPaused = paused;
}
public class CustomScientistNavigator : NPCPlayerNavigator {
public override void Init(BaseCombatEntity entity, NavMeshAgent agent) {
TriggerStuckEvent = true;
base.Init(entity, agent);
}
public override void OnStuck() {
CustomScientistNPC customNpc = BaseEntity as CustomScientistNPC;
if (customNpc.Brain != null && customNpc.Brain.Navigator != null && customNpc.Brain.Events != null) customNpc.Brain.SwitchToState(AIState.Idle, 0);
}
public override void ApplyFacingDirectionOverride() {
base.ApplyFacingDirectionOverride();
if (overrideFacingDirectionMode == OverrideFacingDirectionMode.None) return;
if (overrideFacingDirectionMode == OverrideFacingDirectionMode.Direction) {
NPCPlayerEntity.SetAimDirection(facingDirectionOverride);
return;
}
if (facingDirectionEntity != null) {
Vector3 aimDirection = GetAimDirection(NPCPlayerEntity, facingDirectionEntity);
facingDirectionOverride = aimDirection;
NPCPlayerEntity.SetAimDirection(facingDirectionOverride);
}
}
protected override bool CanUpdateMovement() {
if (BaseEntity == null || !BaseEntity.IsAlive()) return false;
if (CurrentNavigationType == NavigationType.NavMesh && (NPCPlayerEntity.IsDormant || !NPCPlayerEntity.syncPosition) && Agent.enabled) {
SetDestination(NPCPlayerEntity.ServerPosition, 1f, 0f, 0f);
return false;
}
return true;
}
private static Vector3 GetAimDirection(BasePlayer aimingPlayer, BaseEntity target) {
if (target == null) return Vector3Ex.Direction2D(aimingPlayer.transform.position + aimingPlayer.eyes.BodyForward() * 1000f, aimingPlayer.transform.position);
if (Vector3Ex.Distance2D(aimingPlayer.transform.position, target.transform.position) <= 0.75f) return Vector3Ex.Direction2D(target.transform.position, EyesPosition(aimingPlayer));
return (TargetAimPositionOffset(target) - EyesPosition(aimingPlayer)).normalized;
}
private static Vector3 EyesPosition(BasePlayer aimingPlayer) => aimingPlayer.eyes.position - Vector3.up * 0.15f;
private static Vector3 TargetAimPositionOffset(BaseEntity target) {
BasePlayer basePlayer = target as BasePlayer;
if (basePlayer == null) return target.CenterPoint();
if (basePlayer.IsSleeping() || basePlayer.IsWounded()) return basePlayer.transform.position + Vector3.up * 0.1f;
return basePlayer.eyes.position - Vector3.up * 0.15f;
}
}
public class CustomScientistBrain : BaseAIBrain<global : : HumanNPC> {
public override void InitializeAI() {
SenseTypes = EntityType.Player | EntityType.BasePlayerNPC | EntityType.NPC;
CustomScientistNPC customNpc = GetEntity() as CustomScientistNPC;
customNpc.Settings.Sensory.ApplySettingsToBrain(this);
base.InitializeAI();
ThinkMode = AIThinkMode.Interval;
thinkRate = 0.25f;
PathFinder = new HumanPathFinder();
((HumanPathFinder)PathFinder).Init(customNpc);
customNpc.Settings.Movement.ApplySettingsToNavigator(Navigator);
Navigator.MaxRoamDistanceFromHome = customNpc.Settings.RoamRange;
byte[] design = (customNpc.Plugin as ICustomNPCPlugin).GetCustomDesign() ?? DefaultDesign;
LoadAIDesign(ProtoBuf.AIDesign.Deserialize(design), null, 0);
}
public override void AddStates() {
states = new Dictionary<AIState, BasicAIState>();
CustomScientistNPC customNpc = GetEntity() as CustomScientistNPC;
if (!(customNpc.Plugin as ICustomNPCPlugin).InitializeStates(this)) {
AddState(new BaseIdleState());
if (customNpc.IsDead()) return;
AddState(new RoamState());
AddState(new ChaseState());
AddState(new MoveToDestinationState());
AddState(new BaseMountedState());
AddState(new BaseDismountedState());
AddState(new WoundedState());
AddState(new FallingState());
}
}
public override void Think(float delta) {
CustomScientistNPC customNpc = GetEntity() as CustomScientistNPC;
if (customNpc.IsPaused) {
Navigator?.Pause();
return;
}
base.Think(delta);
if (sleeping) return;
if (customNpc.Settings.KillUnderWater) {
if (customNpc != null && customNpc.WaterFactor() > 0.85f && !customNpc.IsDestroyed) {
const float DROWN_TIMER = 5f;
customNpc.Hurt(delta * (customNpc.MaxHealth() / DROWN_TIMER), DamageType.Drowned, null, true);
}
}
CustomScientistNPC.NPCGrid.Move(customNpc, customNpc.Transform.position.x, customNpc.Transform.position.z);
}
protected override void OnStateChanged() {
base.OnStateChanged();
CustomScientistNPC customNpc = GetEntity() as CustomScientistNPC;
customNpc.CurrentState = CurrentState.StateType;
if (customNpc.CurrentState
// ... [Code preview truncated due to length. Full source in attached file] ...