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> _npcLookup; private static CustomNPC Instance; #endregion #region Oxide Hooks private void Loaded() { Instance = this; _npcLookup = new Hash>(); } 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 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> 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 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()) 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 list; if (!_npcLookup.TryGetValue(plugin, out list)) list = _npcLookup[plugin] = new List(); 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(); NPCPlayerNavigator scientistNavigator = scientistNPC.GetComponent(); CustomScientistNPC customScientist = scientistNPC.gameObject.AddComponent(); CustomScientistBrain customScientistBrain = scientistNPC.gameObject.AddComponent(); CustomScientistNavigator customScientistNavigator = scientistNPC.gameObject.AddComponent(); CopySerializeableFields(scientistNPC, customScientist); CopySerializeableFields(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(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 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(); _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(); _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(); _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(); _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 NPCGrid = new Spatial.Grid(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(); NavAgent.areaMask = AREA_MASK; NavAgent.agentTypeID = AGENT_TYPE_ID; GetComponent().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 ProjectileEffectiveRange = new Hash { ["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 _effectiveRangeDefaults = new Hash(); 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 { 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(); 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 == AIState.Chase) customNpc.NotifyNearby(); else customNpc.CancelInvoke(customNpc.TriggerDown); } public override void OnDestroy() { if (Rust.Application.isQuitting) return; CustomScientistNPC customNpc = GetEntity() as CustomScientistNPC; BaseEntity.Query.Server.RemoveBrain(customNpc); LeaveGroup(); } public class RoamState : BasicAIState { private StateStatus status = StateStatus.Error; private static readonly Vector3[] preferedTopologySamples = new Vector3[4]; private static readonly Vector3[] topologySamples = new Vector3[4]; private bool isAlert; public RoamState() : base(AIState.Roam) { } public override void StateEnter() { base.StateEnter(); status = StateStatus.Error; if (brain.PathFinder == null) return; CustomScientistNPC customNpc = GetEntity() as CustomScientistNPC; isAlert = customNpc.IsAlert; Vector3 bestRoamPosition; if (customNpc.DestinationOverride != Vector3.zero) { bestRoamPosition = GetBestRoamPosition(brain.Navigator, customNpc.DestinationOverride, brain.Events.Memory.Position.Get(4), 0f, 20f); customNpc.DestinationOverride = Vector3.zero; isAlert = true; } else { bestRoamPosition = customNpc.Settings.RoamRange <= 0f ? GetBestRoamPosition(brain.Navigator, customNpc.Transform.position, brain.Events.Memory.Position.Get(4), 20f, 100f) : GetBestRoamPosition(brain.Navigator, customNpc.HomePosition, brain.Events.Memory.Position.Get(4), 10f, customNpc.Settings.RoamRange); } if (brain.Navigator.SetDestination(bestRoamPosition, isAlert ? BaseNavigator.NavigationSpeed.Fast : BaseNavigator.NavigationSpeed.Slow, 0f, 0f)) { status = StateStatus.Running; return; } status = StateStatus.Error; } public override void StateLeave() { base.StateLeave(); Stop(); if (isAlert) (GetEntity() as CustomScientistNPC).IsAlert = false; isAlert = false; } public override StateStatus StateThink(float delta) { base.StateThink(delta); if (status == StateStatus.Error) return status; if (brain.Navigator.Moving) return StateStatus.Running; return StateStatus.Finished; } private void Stop() => brain.Navigator.Stop(); private Vector3 GetBestRoamPosition(BaseNavigator navigator, Vector3 localTo, Vector3 fallbackPos, float minRange, float maxRange) { int topologyIndex = 0; int preferredTopologyIndex = 0; for (float degree = 0f; degree < 360f; degree += 90f) { Vector3 position; Vector3 pointOnCircle = BasePathFinder.GetPointOnCircle(localTo, Random.Range(minRange, maxRange), degree + Random.Range(0f, 90f)); if (navigator.GetNearestNavmeshPosition(pointOnCircle, out position, 20f) && navigator.IsAcceptableWaterDepth(position)) { topologySamples[topologyIndex] = position; topologyIndex++; if (navigator.IsPositionATopologyPreference(position)) { preferedTopologySamples[preferredTopologyIndex] = position; preferredTopologyIndex++; } } } Vector3 chosenPosition; if (Random.Range(0f, 1f) <= 0.9f && preferredTopologyIndex > 0) chosenPosition = preferedTopologySamples[Random.Range(0, preferredTopologyIndex)]; else if (topologyIndex > 0) chosenPosition = topologySamples[Random.Range(0, topologyIndex)]; else chosenPosition = fallbackPos; return chosenPosition; } } public class ChaseState : BasicAIState { private StateStatus status = StateStatus.Error; private float nextPositionUpdateTime; private float originalStopDistance; private bool unreachableLastUpdate; private float targetLostTime; private float targetLastVisibleTime; private NavMeshHit navmeshHit; public ChaseState() : base(AIState.Chase) { AgrresiveState = true; } public override void StateLeave() { base.StateLeave(); Stop(); brain.Navigator.StoppingDistance = originalStopDistance; } public override void StateEnter() { base.StateEnter(); status = StateStatus.Error; if (brain.PathFinder == null) return; status = StateStatus.Running; nextPositionUpdateTime = 0f; originalStopDistance = brain.Navigator.StoppingDistance; targetLostTime = 0; targetLastVisibleTime = Time.time; AttackEntity attackEntity = (GetEntity() as CustomScientistNPC).CurrentWeapon; if (attackEntity is BaseMelee) brain.Navigator.StoppingDistance = 0.1f; brain.Navigator.SetCurrentSpeed(BaseNavigator.NavigationSpeed.Fast); } private void Stop() { brain.Navigator.Stop(); brain.Navigator.ClearFacingDirectionOverride(); } public override StateStatus StateThink(float delta) { base.StateThink(delta); if (status == StateStatus.Error) return status; CustomScientistNPC customNpc = GetEntity() as CustomScientistNPC; BaseEntity baseEntity = brain.Events.Memory.Entity.Get(brain.Events.CurrentInputMemorySlot); if (baseEntity == null || !customNpc.CanTargetEntity(baseEntity) || (baseEntity is BasePlayer && !customNpc.CanTargetBasePlayer(baseEntity as BasePlayer))) { brain.Events.Memory.Entity.Remove(brain.Events.CurrentInputMemorySlot); Stop(); return StateStatus.Error; } FaceTarget(customNpc, baseEntity); if (Vector3.Distance(baseEntity.transform.position, customNpc.Transform.position) > brain.Senses.TargetLostRange) { targetLostTime += delta; if (targetLostTime > customNpc.Settings.Sensory.TargetLostRangeTime) { Stop(); return StateStatus.Finished; } } else targetLostTime = 0; if (!brain.Senses.Memory.IsLOS(baseEntity)) { if (Time.time - targetLastVisibleTime > customNpc.Settings.Sensory.TargetLostLOSTime) { Stop(); return StateStatus.Finished; } } else targetLastVisibleTime = Time.time; float distanceFromHome = Vector3.Distance(customNpc.Transform.position, customNpc.HomePosition); if (customNpc.Settings.ChaseRange > 0f && distanceFromHome > customNpc.Settings.ChaseRange) { Vector3 position = GetRandomPositionAround(ClosestPositionToTargetInHomeRange(customNpc.HomePosition, customNpc.Settings.ChaseRange, baseEntity), 1f, 10f); if (brain.Navigator.SetDestination(position, BaseNavigator.NavigationSpeed.Fast, 0f, 0f)) { nextPositionUpdateTime = Random.Range(1f, 3f); return StateStatus.Running; } } if (Time.time > nextPositionUpdateTime) { if (!(customNpc.CurrentWeapon is BaseProjectile)) { if (unreachableLastUpdate) { Vector3 position = GetRandomPositionAround(baseEntity.transform.position, 3f, 10f); brain.Navigator.SetDestination(position, BaseNavigator.NavigationSpeed.Fast, 0.1f, 0f); nextPositionUpdateTime = Time.time + 3f; unreachableLastUpdate = false; return StateStatus.Running; } brain.Navigator.SetDestination(baseEntity.transform.position, BaseNavigator.NavigationSpeed.Fast, 0.1f, 0f); if (brain.Navigator.Agent.path.status > NavMeshPathStatus.PathComplete) unreachableLastUpdate = true; nextPositionUpdateTime = Time.time + 0.1f; if (!brain.Navigator.Moving) return StateStatus.Finished; } else { Vector3 position = GetRandomPositionAround(baseEntity.transform.position, 10f, customNpc.EngagementRange() * 0.75f); if (brain.Navigator.SetDestination(position, BaseNavigator.NavigationSpeed.Fast, 0f, 0f)) nextPositionUpdateTime = Random.Range(3f, 6f); } } return StateStatus.Running; } private void FaceTarget(CustomScientistNPC customNpc, BaseEntity baseEntity) { float distanceToTarget = Vector3.Distance(baseEntity.transform.position, customNpc.Transform.position); if (!(customNpc.CurrentWeapon is BaseProjectile) && (brain.Senses.Memory.IsLOS(baseEntity) || distanceToTarget <= 10f)) brain.Navigator.SetFacingDirectionEntity(baseEntity); else if (customNpc.CurrentWeapon is BaseProjectile && brain.Senses.Memory.IsLOS(baseEntity)) brain.Navigator.SetFacingDirectionEntity(baseEntity); else brain.Navigator.ClearFacingDirectionOverride(); } private Vector3 ClosestPositionToTargetInHomeRange(Vector3 homePosition, float chaseRange, BaseEntity baseEntity) { float targetDistanceToHome = Vector3.Distance(baseEntity.transform.position, homePosition); if (targetDistanceToHome < chaseRange) return baseEntity.transform.position; return Vector3.Lerp(homePosition, baseEntity.transform.position, chaseRange / targetDistanceToHome); } private Vector3 GetRandomPositionAround(Vector3 position, float minDistFrom = 0f, float maxDistFrom = 2f) { if (maxDistFrom < 0f) maxDistFrom = 0f; Vector2 vector = Random.insideUnitCircle * maxDistFrom; float x = Mathf.Clamp(Mathf.Max(Mathf.Abs(vector.x), minDistFrom), minDistFrom, maxDistFrom) * Mathf.Sign(vector.x); float z = Mathf.Clamp(Mathf.Max(Mathf.Abs(vector.y), minDistFrom), minDistFrom, maxDistFrom) * Mathf.Sign(vector.y); Vector3 random = position + new Vector3(x, 0f, z); if (NavMesh.SamplePosition(position, out navmeshHit, 50f, brain.Navigator.Agent.areaMask)) random.y = navmeshHit.position.y; else random.y = TerrainMeta.HeightMap.GetHeight(position); return random; } } public class MoveToDestinationState : BasicAIState { public MoveToDestinationState() : base(AIState.MoveToPoint) { } public override void StateEnter() { base.StateEnter(); Vector3 position = brain.Events.Memory.Position.Get(5); if (!brain.Navigator.SetDestination(position, BaseNavigator.NavigationSpeed.Fast, 0.25f, 0f)) return; } public override StateStatus StateThink(float delta) { base.StateThink(delta); CustomScientistNPC customNpc = GetEntity() as CustomScientistNPC; Vector3 position = brain.Events.Memory.Position.Get(5); if (!brain.Navigator.SetDestination(position, BaseNavigator.NavigationSpeed.Fast, 0.25f, 0f)) return StateStatus.Error; if (Vector3.Distance(customNpc.Transform.position, position) < 3f) { if (customNpc.onDestinationReached != null) { customNpc.onDestinationReached.Invoke(); customNpc.onDestinationReached = null; } return StateStatus.Finished; } return StateStatus.Running; } } public class WoundedState : BasicAIState { private bool canLeaveState; private bool isIncapacitated; private Vector3 destination; private float woundedDuration; public WoundedState() : base(AIState.Orbit) { } public override bool CanLeave() => canLeaveState; public override void StateEnter() { base.StateEnter(); canLeaveState = false; CustomScientistNPC customNpc = GetEntity() as CustomScientistNPC; if (Random.value > 0.5f) { customNpc.health = (float)Random.Range(ConVar.Server.crawlingminimumhealth, ConVar.Server.crawlingmaximumhealth); customNpc.metabolism.bleeding.@value = 0f; customNpc.healingWhileCrawling = 0f; isIncapacitated = false; destination = customNpc.ServerPosition; brain.Navigator.SetCurrentSpeed(BaseNavigator.NavigationSpeed.Slowest); } else { customNpc.health = Random.Range(2f, 6f); customNpc.metabolism.bleeding.@value = 0f; customNpc.healingWhileCrawling = 0f; customNpc.SetPlayerFlag(BasePlayer.PlayerFlags.Incapacitated, true); isIncapacitated = true; } woundedDuration = Random.Range(customNpc.Settings.WoundedDurationMin, customNpc.Settings.WoundedDurationMax); brain.Navigator.SetCurrentSpeed(BaseNavigator.NavigationSpeed.Slowest); customNpc.SetServerFall(true); customNpc.SendNetworkUpdateImmediate(false); } public override void StateLeave() { base.StateLeave(); CustomScientistNPC customNpc = GetEntity() as CustomScientistNPC; customNpc.SetPlayerFlag(BasePlayer.PlayerFlags.Wounded, false); customNpc.SetPlayerFlag(BasePlayer.PlayerFlags.Incapacitated, false); customNpc.SetServerFall(false); } public override StateStatus StateThink(float delta) { base.StateThink(delta); CustomScientistNPC customNpc = GetEntity() as CustomScientistNPC; if (!customNpc.IsDead()) { if (!isIncapacitated) { if (Vector3.Distance(customNpc.Transform.position, destination) < 2f) { Vector3 random = Random.insideUnitCircle.normalized * 25f; if (NavmeshSpawnPoint.Find(customNpc.Transform.position + new Vector3(random.x, 0f, random.y), 10f, out destination)) brain.Navigator.SetDestination(destination, BaseNavigator.NavigationSpeed.Slowest); } } if (TimeInState >= woundedDuration) { if (Random.Range(0, 100) >= customNpc.Settings.WoundedRecoveryChance) customNpc.Die(); else { customNpc.SetPlayerFlag(BasePlayer.PlayerFlags.Wounded, false); customNpc.InitializeHealth(customNpc.Settings.Vitals.Health, customNpc.Settings.Vitals.Health); customNpc.EquipWeapon(); } canLeaveState = true; return StateStatus.Finished; } } return StateStatus.Running; } } public class FallingState : BasicAIState { private bool canLeaveState; public FallingState() : base(AIState.Land) { } public override bool CanLeave() => canLeaveState; public override void StateEnter() { base.StateEnter(); canLeaveState = false; CustomScientistNPC customNpc = GetEntity() as CustomScientistNPC; customNpc.HolsterWeapon(); brain.Navigator.SetCurrentSpeed(BaseNavigator.NavigationSpeed.Slowest); brain.Navigator.Pause(); } public override void StateLeave() { base.StateLeave(); CustomScientistNPC customNpc = GetEntity() as CustomScientistNPC; customNpc.EquipWeapon(); brain.Navigator.SetCurrentSpeed(BaseNavigator.NavigationSpeed.Slow); brain.Navigator.Resume(); } public override StateStatus StateThink(float delta) { base.StateThink(delta); CustomScientistNPC customNpc = GetEntity() as CustomScientistNPC; if (customNpc.modelState.flying) return StateStatus.Running; customNpc.HomePosition = customNpc.Transform.position; canLeaveState = true; return StateStatus.Finished; } } private static readonly byte[] DefaultDesign = new byte[] { 8, 1, 8, 2, 8, 3, 8, 6, 8, 19, 8, 9, 8, 11, 18, 61, 8, 0, 16, 1, 26, 25, 8, 0, 16, 1, 24, 0, 32, 0, 40, 0, 48, 0, 162, 6, 10, 13, 0, 0, 0, 0, 21, 0, 0, 128, 63, 26, 12, 8, 3, 16, 2, 24, 0, 32, 4, 40, 0, 48, 0, 26, 12, 8, 14, 16, 2, 24, 0, 32, 0, 40, 0, 48, 0, 32, 0, 18, 62, 8, 1, 16, 2, 26, 12, 8, 4, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 26, 12, 8, 2, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 26, 12, 8, 3, 16, 2, 24, 0, 32, 4, 40, 0, 48, 0, 26, 12, 8, 14, 16, 2, 24, 0, 32, 0, 40, 0, 48, 0, 32, 0, 18, 103, 8, 2, 16, 3, 26, 12, 8, 4, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 26, 12, 8, 2, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 26, 21, 8, 15, 16, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1, 24, 1, 32, 0, 40, 0, 48, 0, 26, 21, 8, 5, 16, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1, 24, 0, 32, 0, 40, 0, 48, 0, 26, 21, 8, 16, 16, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1, 24, 0, 32, 0, 40, 0, 48, 0, 32, 0, 18, 20, 8, 3, 16, 24, 26, 12, 8, 4, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 32, 0, 18, 20, 8, 4, 16, 6, 26, 12, 8, 17, 16, 8, 24, 1, 32, 4, 40, 3, 48, 0, 32, 0, 18, 34, 8, 8, 16, 19, 26, 12, 8, 4, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 26, 12, 8, 2, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 32, 0, 18, 56, 8, 5, 16, 9, 26, 34, 8, 0, 16, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1, 24, 0, 32, 0, 40, 0, 48, 0, 162, 6, 10, 13, 255, 255, 127, 127, 21, 255, 255, 127, 127, 26, 12, 8, 4, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 32, 0, 18, 56, 8, 6, 16, 11, 26, 34, 8, 0, 16, 255, 255, 255, 255, 255, 255, 255, 255, 255, 1, 24, 0, 32, 0, 40, 0, 48, 0, 162, 6, 10, 13, 255, 255, 127, 127, 21, 255, 255, 127, 127, 26, 12, 8, 4, 16, 0, 24, 0, 32, 0, 40, 0, 48, 0, 32, 0, 24, 0, 34, 14, 68, 101, 102, 97, 117, 108, 116, 32, 68, 101, 115, 105, 103, 110, 40, 0, 48, 0 }; } #endregion #region Testing [ChatCommand("spawnnpc")] private void cmdSpawnNPC(BasePlayer player, string command, string[] args) { if (!player.IsAdmin) return; int amount = 3; if (args.Length > 0) int.TryParse(args[0], out amount); for (int i = 0; i < amount; i++) { Vector3 position = player.transform.position;// + (Random.onUnitSphere * 20); NPCSettings settings = new NPCSettings { DisplayNames = new string[] { "Test 1", "Test 2", "Test 3" }, Types = new NPCType[] { NPCType.Scientist, NPCType.HeavyScientist }, WoundedChance = 20, WoundedDurationMin = 30, WoundedDurationMax = 60, WoundedRecoveryChance = 75, KillUnderWater = false, DropInventoryOnDeath = true, RoamRange = 30f, ChaseRange = 90f, Sensory = new NPCSettings.SensoryStats { TargetLostRange = 90 } }; SpawnNPC(this, position, settings); } } public bool InitializeStates(BaseAIBrain customNPCBrain) => false; public bool WantsToPopulateLoot(CustomNPC.CustomScientistNPC customNpc, NPCPlayerCorpse npcplayerCorpse) => false; public byte[] GetCustomDesign() => null; #endregion #region Config private ConfigData configData; private class ConfigData { public Oxide.Core.VersionNumber Version { get; set; } } protected override void LoadConfig() { base.LoadConfig(); configData = Config.ReadObject(); if (configData.Version < Version) UpdateConfigValues(); Config.WriteObject(configData, true); } protected override void LoadDefaultConfig() => configData = GetBaseConfig(); private ConfigData GetBaseConfig() { return new ConfigData { Version = Version }; } protected override void SaveConfig() => Config.WriteObject(configData, true); private void UpdateConfigValues() { PrintWarning("Config update detected! Updating config values..."); configData.Version = Version; PrintWarning("Config update completed!"); } #endregion } namespace CustomNPCEx { public interface ICustomNPCPlugin { bool InitializeStates(BaseAIBrain customNPCBrain); bool WantsToPopulateLoot(CustomNPC.CustomScientistNPC customNpc, NPCPlayerCorpse npcplayerCorpse); byte[] GetCustomDesign(); } } }