C#:
using Facepunch;
using Newtonsoft.Json.Linq;
using Rust;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.AI;
namespace Oxide.Plugins {
[Info("NpcSpawn", "TopPlugin.ru", "1.0.0")]
class NpcSpawn : RustPlugin {
#region Config
internal class NpcItem {
public string ShortName;
public int Amount;
public ulong SkinID;
}
internal class NpcConfig {
public string Name {
get;
set;
}
public List<NpcItem> WearItems {
get;
set;
}
public List<NpcItem> BeltItems {
get;
set;
}
public float Health {
get;
set;
}
public float RoamRange {
get;
set;
}
public float ChaseRange {
get;
set;
}
public float DamageScale {
get;
set;
}
public float AimConeScale {
get;
set;
}
public bool DisableRadio {
get;
set;
}
public bool EnableNavMesh {
get;
set;
}
public bool Stationary {
get;
set;
}
public bool KillInSafeZone {
get;
set;
}
public bool CanUseWeaponMounted {
get;
set;
}
public float Speed {
get;
set;
}
public SensoryStats Sensory {
get;
set;
}
}
public class SensoryStats {
public float AttackRangeMultiplier {
get;
set;
}
public float SenseRange {
get;
set;
}
public bool CheckVisionCone {
get;
set;
}
public float VisionCone {
get;
set;
}
public bool IgnoreSafeZonePlayers {
get;
set;
}
public void ApplySettingsToBrain(BaseAIBrain<global : : HumanNPC> brain) {
brain.MaxGroupSize = int.MaxValue;
brain.AttackRangeMultiplier = AttackRangeMultiplier;
brain.SenseRange = SenseRange;
brain.TargetLostRange = SenseRange * 2f;
brain.CheckVisionCone = CheckVisionCone;
brain.VisionCone = Vector3.Dot(Vector3.forward, Quaternion.Euler(0f, VisionCone, 0f) * Vector3.forward);
brain.IgnoreSafeZonePlayers = IgnoreSafeZonePlayers;
}
}
#endregion Config
#region Methods
private ScientistNPC SpawnNpc(Vector3 position, JObject configJson) {
if (Rust.Ai.AiManager.nav_disable) return null;
CustomScientistNPC npc = CreateCustomNPC(position, configJson.ToObject<NpcConfig>());
if (npc != null) Npcs.Add(npc);
return npc;
}
private CustomScientistNPC CreateCustomNPC(Vector3 position, NpcConfig config) {
if (config.EnableNavMesh) if (!NavmeshSpawnPoint.Find(position, config.RoamRange, out position)) return null;
if (position.y < -0.25f || (config.KillInSafeZone && IsInSafeZone(position))) return null;
ScientistNPC scientistNPC = GameManager.server.CreateEntity("assets/rust.ai/agents/npcplayer/humannpc/scientist/scientistnpc_heavy.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.Config = config;
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;
UnityEngine.Object.DestroyImmediate(scientistNavigator);
UnityEngine.Object.DestroyImmediate(scientistBrain, true);
UnityEngine.Object.DestroyImmediate(scientistNPC, true);
customScientist.gameObject.AwakeFromInstantiate();
customScientist.Spawn();
return customScientist;
}
private 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 Methods
#region Controller
internal class DefaultSettings {
public float effectiveRange;
public float attackLengthMin;
public float attackLengthMax;
}
private Dictionary<string, DefaultSettings> Weapons = new Dictionary<string, DefaultSettings> {
["rifle.bolt"]
= new DefaultSettings {
effectiveRange = 150f, attackLengthMin = -1f, attackLengthMax = -1f
}
, ["bow.compound"] = new DefaultSettings {
effectiveRange = 15f, attackLengthMin = -1f, attackLengthMax = -1f
}
, ["smg.2"] = new DefaultSettings {
effectiveRange = 20f, attackLengthMin = 0.4f, attackLengthMax = 0.4f
}
, ["shotgun.double"] = new DefaultSettings {
effectiveRange = 10f, attackLengthMin = 0.3f, attackLengthMax = 1f
}
, ["pistol.eoka"] = new DefaultSettings {
effectiveRange = 10f, attackLengthMin = -1f, attackLengthMax = -1f
}
, ["rifle.l96"] = new DefaultSettings {
effectiveRange = 150f, attackLengthMin = -1f, attackLengthMax = -1f
}
, ["pistol.nailgun"] = new DefaultSettings {
effectiveRange = 15f, attackLengthMin = 0f, attackLengthMax = 0.46f
}
, ["pistol.python"] = new DefaultSettings {
effectiveRange = 15f, attackLengthMin = 0.175f, attackLengthMax = 0.525f
}
, ["pistol.semiauto"] = new DefaultSettings {
effectiveRange = 15f, attackLengthMin = 0f, attackLengthMax = 0.46f
}
, ["smg.thompson"] = new DefaultSettings {
effectiveRange = 20f, attackLengthMin = 0.4f, attackLengthMax = 0.4f
}
, ["shotgun.waterpipe"] = new DefaultSettings {
effectiveRange = 10f, attackLengthMin = -1f, attackLengthMax = -1f
}
,
}
;
public class CustomScientistNPC : ScientistNPC, IAIAttack {
public NpcConfig Config {
get;
set;
}
public Transform Transform {
get;
private set;
}
public BaseEntity CurrentTarget {
get;
set;
}
public AttackEntity CurrentWeapon {
get;
private set;
}
public Vector3 HomePosition {
get;
internal set;
}
public float DistanceFromBase => Vector3.Distance(Transform.position, HomePosition);
public float DistanceToTarget => Vector3.Distance(Transform.position, CurrentTarget.transform.position);
private bool isEquippingWeapon;
public override void ServerInit() {
Transform = transform;
HomePosition = Transform.position;
if (NavAgent == null) NavAgent = GetComponent<NavMeshAgent>();
NavAgent.areaMask = 1;
NavAgent.agentTypeID = -1372625422;
startHealth = Config.Health;
damageScale = Config.DamageScale;
base.ServerInit();
if (Config.DisableRadio) {
CancelInvoke(PlayRadioChatter);
RadioChatterEffects = new GameObjectRef[0];
DeathEffects = new GameObjectRef[0];
}
UpdateInventory();
Invoke(SetDisplayName, 1f);
InvokeRepeating(LightCheck, 1f, 30f);
InvokeRepeating(UpdateTick, 1f, 2f);
}
private void UpdateInventory() {
if (Config.WearItems.Count > 0) AddItems(inventory.containerWear, Config.WearItems);
if (Config.BeltItems.Count > 0) AddItems(inventory.containerBelt, Config.BeltItems);
}
private void AddItems(ItemContainer container, List<NpcItem> items) {
List<Item> allItems = container.itemList;
for (int i = allItems.Count - 1; i >= 0; i--) {
Item item = allItems[i];
item.RemoveFromContainer();
item.Remove();
}
foreach (NpcItem npcItem in items) {
Item item = ItemManager.CreateByName(npcItem.ShortName, npcItem.Amount, npcItem.SkinID);
if (!item.MoveToContainer(container)) item.Remove();
}
}
private void SetDisplayName() => displayName = Config.Name;
private void OnDestroy() {
if (HealCoroutine != null) ServerMgr.Instance.StopCoroutine(HealCoroutine);
CancelInvoke(UpdateTick);
CancelInvoke(FinishReloadGrenade);
CancelInvoke(FinishReloadSmoke);
CancelInvoke(FinishRunAwayWater);
}
private void UpdateTick() {
if (CurrentWeapon is FlameThrower && (CurrentWeapon as FlameThrower).ammo <= 0) CurrentWeapon.TopUpAmmo();
if (CanRunAwayWater()) RunAwayWater();
if (CanThrownGrenade()) ThrownGrenade(CurrentTarget.transform.position);
if (CanHeal()) HealCoroutine = ServerMgr.Instance.StartCoroutine(Heal());
}
#region Targeting
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;
BasePlayer basePlayer = baseEntity as BasePlayer;
if (!CanTargetBasePlayer(basePlayer)) continue;
float rangeDelta = 1f - Mathf.InverseLerp(1f, Brain.SenseRange, Vector3.Distance(basePlayer.transform.position, Transform.position));
if (Config.Sensory.CheckVisionCone) {
float dot = Vector3.Dot((basePlayer.transform.position - eyes.position).normalized, eyes.BodyForward());
if (dot < Brain.VisionCone) continue;
rangeDelta += Mathf.InverseLerp(Brain.VisionCone, 1f, dot) / 2f;
}
rangeDelta += (Brain.Senses.Memory.IsLOS(basePlayer) ? 2f : 0f);
if (rangeDelta <= delta) continue;
target = basePlayer;
delta = rangeDelta;
}
return target;
}
public bool CanTargetBasePlayer(BasePlayer player) {
if (player == null || IsRunAwayWater) return false;
if (player.IsFlying || player.IsSleeping() || player.IsWounded() || player.IsDead()) return false;
if (!player.userID.IsSteamId()) return false;
if (Config.Sensory.IgnoreSafeZonePlayers && player.InSafeZone()) return false;
return true;
}
#endregion Targeting
#region Equip Weapons
public override void EquipWeapon() {
if (!isEquippingWeapon) {
Item slot = inventory.containerBelt.itemList.Where(x => x.info.shortname != "syringe.medical" && x.info.shortname != "grenade.f1" && x.info.shortname != "grenade.beancan" && x.info.shortname != "grenade.smoke" && x.info.shortname != "explosive.timed" && x.info.shortname != "rocket.launcher").FirstOrDefault();
if (slot != null) StartCoroutine(EquipItem(slot));
}
}
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();
if (heldEntity is BaseProjectile && ins.Weapons.ContainsKey(slot.info.shortname)) {
AttackEntity attackEntity = heldEntity as AttackEntity;
attackEntity.effectiveRange = ins.Weapons[slot.info.shortname].effectiveRange;
attackEntity.attackLengthMin = ins.Weapons[slot.info.shortname].attackLengthMin;
attackEntity.attackLengthMax = ins.Weapons[slot.info.shortname].attackLengthMax;
}
}
CurrentWeapon = heldEntity as AttackEntity;
}
isEquippingWeapon = false;
}
}
internal void HolsterWeapon() {
svActiveItemID = 0;
Item activeItem = GetActiveItem();
if (activeItem != null) {
HeldEntity heldEntity = activeItem.GetHeldEntity() as HeldEntity;
if (heldEntity != null) heldEntity.SetHeld(false);
}
SendNetworkUpdate(BasePlayer.NetworkQueue.Update);
inventory.UpdatedVisibleHolsteredItems();
}
#endregion Equip Weapons
protected override string OverrideCorpseName() => displayName;
public override float GetAimConeScale() => Config.AimConeScale;
#region Heal
Coroutine HealCoroutine = null;
private bool IsReloadHeal = false;
private bool CanHeal() {
if (IsReloadHeal || health >= Config.Health) return false;
if (!inventory.containerBelt.itemList.Any(x => x.info.shortname == "syringe.medical")) return false;
if (CurrentTarget != null) return false;
return true;
}
private IEnumerator Heal() {
IsReloadHeal = true;
Item syringe = inventory.containerBelt.itemList.Where(x => x.info.shortname == "syringe.medical").First();
UpdateActiveItem(syringe.uid);
yield return CoroutineEx.waitForSeconds(1.5f);
EquipWeapon();
InitializeHealth(health + 15f > Config.Health ? Config.Health : health + 15f, Config.Health);
if (syringe != null) {
if (syringe.amount == 1) syringe.Remove();
else {
syringe.amount--;
syringe.MarkDirty();
}
}
IsReloadHeal = false;
}
#endregion Heal
#region Grenades
private HashSet<string> Barricades = new HashSet<string> {
"barricade.cover.wood", "barricade.sandbags", "barricade.concrete", "barricade.stone"
}
;
private bool IsReloadGrenade = false;
private bool IsReloadSmoke = false;
private void FinishReloadGrenade() => IsReloadGrenade = false;
private void FinishReloadSmoke() => IsReloadSmoke = false;
private bool CanThrownGrenade() {
if (IsReloadGrenade || CurrentTarget == null) return false;
if (isMounted && !Config.CanUseWeaponMounted) return false;
if (Vector3.Distance(Transform.position, CurrentTarget.transform.position) < 15f && inventory.containerBelt.itemList.Any(x => x.info.shortname == "grenade.f1" || x.info.shortname == "grenade.beancan") && (!CanSeeTarget(CurrentTarget) || IsBehindBarricade())) return true;
return false;
}
internal bool IsBehindBarricade() => CanSeeTarget(CurrentTarget) && IsBarricade();
private bool IsBarricade() {
SetAimDirection((CurrentTarget.transform.position - Transform.position).normalized);
RaycastHit[] hits = Physics.RaycastAll(eyes.HeadRay());
GamePhysics.Sort(hits);
foreach (RaycastHit hit in hits) {
Barricade ent = hit.GetEntity() as Barricade;
if (ent != null && Barricades.Contains(ent.ShortPrefabName) && Vector3.Distance(Transform.position, ent.transform.position) < Vector3.Distance(Transform.position, CurrentTarget.transform.position)) return true;
}
return false;
}
private void ThrownGrenade(Vector3 target) {
Item item = inventory.containerBelt.itemList.Where(x => x.info.shortname == "grenade.f1" || x.info.shortname == "grenade.beancan").FirstOrDefault();
if (item != null) {
GrenadeWeapon weapon = item.GetHeldEntity() as GrenadeWeapon;
if (weapon != null) {
Brain.Navigator.Stop();
SetAimDirection((target - Transform.position).normalized);
weapon.ServerThrow(target);
IsReloadGrenade = true;
Invoke(FinishReloadGrenade, 10f);
}
}
}
internal void ThrownSmoke() {
if (!IsReloadSmoke) {
Item item = inventory.containerBelt.itemList.Where(x => x.info.shortname == "grenade.smoke").FirstOrDefault();
if (item != null) {
GrenadeWeapon weapon = item.GetHeldEntity() as GrenadeWeapon;
if (weapon != null) {
weapon.ServerThrow(Transform.position);
IsReloadSmoke = true;
Invoke(FinishReloadSmoke, 30f);
}
}
}
}
#endregion Grenades
#region Run Away Water
internal bool IsRunAwayWater = false;
private bool CanRunAwayWater() {
if (IsRunAwayWater || CurrentTarget == null) return false;
if (Transform.position.y > -0.25f || TerrainMeta.HeightMap.GetHeight(CurrentTarget.transform.position) > -0.25f) return false;
if (CurrentWeapon is BaseProjectile && DistanceToTarget < EngagementRange()) return false;
if (CurrentWeapon is BaseMelee && DistanceToTarget < CurrentWeapon.effectiveRange) return false;
return true;
}
private void RunAwayWater() {
IsRunAwayWater = true;
CurrentTarget = null;
Invoke(FinishRunAwayWater, 20f);
}
private void FinishRunAwayWater() => IsRunAwayWater = false;
#endregion Run Away Water
}
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> {
private CustomScientistNPC npc = null;
public override void InitializeAI() {
SenseTypes = EntityType.Player;
npc.Config.Sensory.ApplySettingsToBrain(this);
base.InitializeAI();
UseAIDesign = false;
ThinkMode = AIThinkMode.Interval;
thinkRate = 0.25f;
PathFinder = new HumanPathFinder();
((HumanPathFinder)PathFinder).Init(npc);
Navigator.DefaultArea = "Walkable";
Navigator.topologyPreference = (TerrainTopology.Enum)1673010749;
Navigator.Speed = npc.Config.Speed;
}
public override void AddStates() {
npc = GetEntity() as CustomScientistNPC;
states = new Dictionary<AIState, BasicAIState>();
if (npc.Config.Stationary) {
AddState(new IdleState(npc));
AddState(new CombatStationaryState(npc));
}
else {
AddState(new RoamState(npc));
AddState(new ChaseState(npc));
AddState(new CombatState(npc));
AddState(new MountedState(npc));
}
}
public override void Think(float delta) {
if (npc == null) return;
Senses.Update();
base.Think(delta);
if (sleeping) return;
if (!npc.IsRunAwayWater) npc.CurrentTarget = npc.GetBestTarget();
}
public override void OnDestroy() {
if (Rust.Application.isQuitting) return;
BaseEntity.Query.Server.RemoveBrain(npc);
LeaveGroup();
}
public class RoamState : BasicAIState {
private CustomScientistNPC npc;
public RoamState(CustomScientistNPC npc) : base(AIState.Roam) {
this.npc = npc;
}
public override float GetWeight() {
if (npc.isMounted) return 0f;
if (npc.IsRunAwayWater || npc.CurrentTarget == null) return 25f;
return 0f;
}
public override void StateEnter() {
base.StateEnter();
npc.SetPlayerFlag(BasePlayer.PlayerFlags.Relaxed, true);
}
public override void StateLeave() {
base.StateLeave();
npc.ThrownSmoke();
npc.SetPlayerFlag(BasePlayer.PlayerFlags.Relaxed, false);
}
public override StateStatus StateThink(float delta) {
base.StateThink(delta);
if (npc.DistanceFromBase > npc.Config.RoamRange) brain.Navigator.SetDestination(npc.HomePosition, BaseNavigator.NavigationSpeed.Fast);
else if (!brain.Navigator.Moving) brain.Navigator.SetDestination(GetRoamPosition(), BaseNavigator.NavigationSpeed.Slowest);
return StateStatus.Running;
}
private Vector3 GetRoamPosition() {
Vector2 random = UnityEngine.Random.insideUnitCircle * (npc.Config.RoamRange - 5f);
Vector3 result = npc.HomePosition + new Vector3(random.x, 0f, random.y);
result.y = TerrainMeta.HeightMap.GetHeight(result);
NavMeshHit navmeshHit;
if (NavMesh.SamplePosition(result, out navmeshHit, 5f, npc.NavAgent.areaMask)) {
NavMeshPath path = new NavMeshPath();
if (NavMesh.CalculatePath(npc.Transform.position, navmeshHit.position, npc.NavAgent.areaMask, path)) {
if (path.status == NavMeshPathStatus.PathComplete) result = navmeshHit.position;
else result = path.corners.Last();
}
else result = npc.HomePosition;
}
else result = npc.HomePosition;
return result;
}
}
public class ChaseState : BasicAIState {
private CustomScientistNPC npc;
public ChaseState(CustomScientistNPC npc) : base(AIState.Chase) {
this.npc = npc;
}
public override float GetWeight() {
if (npc.CurrentTarget == null) return 0f;
if (npc.DistanceFromBase > npc.Config.ChaseRange) return 0f;
if (npc.IsRunAwayWater) return 0f;
if (npc.isMounted) return 0f;
if (!(npc.CurrentTarget is BasePlayer) || !npc.CanTargetBasePlayer(npc.CurrentTarget as BasePlayer)) return 0f;
return 50f;
}
public override StateStatus StateThink(float delta) {
base.StateThink(delta);
if (npc.CurrentTarget == null) return StateStatus.Error;
if (npc.CurrentWeapon is BaseProjectile) brain.Navigator.SetDestination(brain.PathFinder.GetRandomPositionAround(npc.CurrentTarget.transform.position, 1f, 2f), npc.DistanceToTarget >= 10f ? BaseNavigator.NavigationSpeed.Fast : BaseNavigator.NavigationSpeed.Normal);
else brain.Navigator.SetDestination(npc.CurrentTarget.transform.position, BaseNavigator.NavigationSpeed.Fast);
return StateStatus.Running;
}
}
public class CombatState : BasicAIState {
private CustomScientistNPC npc;
private float nextStrafeTime;
public CombatState(CustomScientistNPC npc) : base(AIState.Combat) {
this.npc = npc;
}
public override float GetWeight() {
if (npc.CurrentTarget == null || npc.CurrentWeapon == null) return 0f;
if (npc.DistanceFromBase > npc.Config.ChaseRange) return 0f;
if (npc.IsRunAwayWater) return 0f;
if (npc.isMounted) return 0f;
if (!(npc.CurrentTarget is BasePlayer) || !npc.CanTargetBasePlayer(npc.CurrentTarget as BasePlayer)) return 0f;
if (!npc.CanSeeTarget(npc.CurrentTarget) || (npc.CanSeeTarget(npc.CurrentTarget) && npc.IsBehindBarricade())) return 0f;
if (npc.DistanceToTarget > npc.EngagementRange()) return 0f;
return 75f;
}
public override void StateEnter() {
base.StateEnter();
brain.mainInterestPoint = npc.Transform.position;
brain.Navigator.SetCurrentSpeed(BaseNavigator.NavigationSpeed.Normal);
brain.Navigator.SetFacingDirectionEntity(npc.CurrentTarget);
}
public override void StateLeave() {
base.StateLeave();
npc.SetDucked(false);
brain.Navigator.ClearFacingDirectionOverride();
}
public override StateStatus StateThink(float delta) {
base.StateThink(delta);
if (npc.CurrentTarget == null) return StateStatus.Error;
brain.Navigator.SetFacingDirectionEntity(npc.CurrentTarget);
if (npc.CurrentWeapon is BaseProjectile) {
if (Time.time > nextStrafeTime) {
if (UnityEngine.Random.Range(0, 3) == 1) {
nextStrafeTime = Time.time + UnityEngine.Random.Range(1f, 2f);
npc.SetDucked(true);
brain.Navigator.Stop();
}
else {
nextStrafeTime = Time.time + UnityEngine.Random.Range(2f, 3f);
npc.SetDucked(false);
brain.Navigator.SetDestination(brain.PathFinder.GetRandomPositionAround(brain.mainInterestPoint, 1f, 2f), BaseNavigator.NavigationSpeed.Normal);
}
npc.ShotTest(npc.DistanceToTarget);
}
}
else if (npc.CurrentWeapon is BaseMelee) {
npc.nextTriggerTime = Time.time + 30f;
if (npc.DistanceToTarget < npc.CurrentWeapon.effectiveRange) DoMeleeAttack();
else brain.Navigator.SetDestination(npc.CurrentTarget.transform.position, BaseNavigator.NavigationSpeed.Fast);
}
return StateStatus.Running;
}
private void DoMeleeAttack() {
if (npc.CurrentWeapon is BaseMelee) {
BaseMelee baseMelee = npc.CurrentWeapon as BaseMelee;
if (!baseMelee.HasAttackCooldown()) {
baseMelee.StartAttackCooldown(baseMelee.repeatDelay * 2f);
npc.SignalBroadcast(BaseEntity.Signal.Attack, string.Empty, null);
if (baseMelee.swingEffect.isValid) Effect.server.Run(baseMelee.swingEffect.resourcePath, baseMelee.transform.position, Vector3.forward, npc.net.connection);
DoMeleeDamage(baseMelee);
}
}
}
private void DoMeleeDamage(BaseMelee baseMelee) {
Vector3 position = npc.eyes.position;
Vector3 forward = npc.eyes.BodyForward();
for (int i = 0; i < 2; i++) {
List<RaycastHit> list = Pool.GetList<RaycastHit>();
GamePhysics.TraceAll(new Ray(position - (forward * (i == 0 ? 0f : 0.2f)), forward), (i == 0 ? 0f : baseMelee.attackRadius), list, baseMelee.effectiveRange + 0.2f, 1219701521, QueryTriggerInteraction.UseGlobal);
bool hasHit = false;
for (int j = 0; j < list.Count; j++) {
RaycastHit raycastHit = list[j];
BaseEntity hitEntity = raycastHit.GetEntity();
if (hitEntity != null && hitEntity != npc && !hitEntity.EqualNetID(npc) && !(hitEntity is ScientistNPC)) {
float damageAmount = 0f;
foreach (DamageTypeEntry damageType in baseMelee.damageTypes) damageAmount += damageType.amount;
hitEntity.OnAttacked(new HitInfo(npc, hitEntity, DamageType.Slash, damageAmount * baseMelee.npcDamageScale));
HitInfo hitInfo = Pool.Get<HitInfo>();
hitInfo.HitEntity = hitEntity;
hitInfo.HitPositionWorld = raycastHit.point;
hitInfo.HitNormalWorld = -forward;
if (hitEntity is BaseNpc || hitEntity is BasePlayer) hitInfo.HitMaterial = StringPool.Get("Flesh");
else hitInfo.HitMaterial = StringPool.Get((raycastHit.GetCollider().sharedMaterial != null ? raycastHit.GetCollider().sharedMaterial.GetName() : "generic"));
Effect.server.ImpactEffect(hitInfo);
Pool.Free(ref hitInfo);
hasHit = true;
if (hitEntity.ShouldBlockProjectiles()) break;
}
}
Pool.FreeList(ref list);
if (hasHit) break;
}
}
}
public class MountedState : BasicAIState {
private CustomScientistNPC npc;
public MountedState(CustomScientistNPC npc) : base(AIState.Mounted) {
this.npc = npc;
}
public override float GetWeight() {
if (npc.isMounted) return 100f;
else return 0f;
}
public override void StateEnter() {
base.StateEnter();
if (!npc.Config.CanUseWeaponMounted) npc.HolsterWeapon();
DisableNavAgent();
}
public override void StateLeave() {
base.StateLeave();
EnableNavAgent();
npc.EquipWeapon();
}
private void EnableNavAgent() {
Vector3 position = npc.Transform.position;
if (npc.Config.EnableNavMesh) if (!NavmeshSpawnPoint.Find(npc.Transform.position, npc.Config.RoamRange, out position)) return;
npc.NavAgent.Warp(position);
npc.Transform.position = position;
npc.HomePosition = position;
npc.NavAgent.enabled = true;
npc.NavAgent.isStopped = false;
brain.Navigator.SetDestination(position, BaseNavigator.NavigationSpeed.Fast);
}
private void DisableNavAgent() {
if (npc.NavAgent.enabled) {
npc.NavAgent.destination = npc.Transform.position;
npc.NavAgent.isStopped = true;
npc.NavAgent.enabled = false;
}
}
}
public class IdleState : BasicAIState {
private CustomScientistNPC npc;
public IdleState(CustomScientistNPC npc) : base(AIState.Idle) {
this.npc = npc;
}
public override float GetWeight() => 50f;
public override void StateEnter() {
base.StateEnter();
npc.SetPlayerFlag(BasePlayer.PlayerFlags.Relaxed, true);
}
public override void StateLeave() {
base.StateLeave();
npc.ThrownSmoke();
npc.SetPlayerFlag(BasePlayer.PlayerFlags.Relaxed, false);
}
}
public class CombatStationaryState : BasicAIState {
private CustomScientistNPC npc;
private float nextStrafeTime;
public CombatStationaryState(CustomScientistNPC npc) : base(AIState.CombatStationary) {
this.npc = npc;
}
public override float GetWeight() {
if (npc.CurrentTarget == null || npc.CurrentWeapon == null) return 0f;
if (!(npc.CurrentTarget is BasePlayer) || !npc.CanTargetBasePlayer(npc.CurrentTarget as BasePlayer)) return 0f;
if (!npc.CanSeeTarget(npc.CurrentTarget) || (npc.CanSeeTarget(npc.CurrentTarget) && npc.IsBehindBarricade())) return 0f;
if (npc.DistanceToTarget > npc.EngagementRange()) return 0f;
return 100f;
}
public override void StateEnter() {
base.StateEnter();
brain.Navigator.SetFacingDirectionEntity(npc.CurrentTarget);
}
public override void StateLeave() {
base.StateLeave();
npc.SetDucked(false);
brain.Navigator.ClearFacingDirectionOverride();
}
public override StateStatus StateThink(float delta) {
base.StateThink(delta);
if (npc.CurrentTarget == null) return StateStatus.Error;
brain.Navigator.SetFacingDirectionEntity(npc.CurrentTarget);
if (npc.CurrentWeapon is BaseProjectile) {
if (Time.time > nextStrafeTime) {
if (UnityEngine.Random.Range(0, 3) == 1) {
nextStrafeTime = Time.time + UnityEngine.Random.Range(1f, 2f);
npc.SetDucked(true);
}
else {
nextStrafeTime = Time.time + UnityEngine.Random.Range(2f, 3f);
npc.SetDucked(false);
}
npc.ShotTest(npc.DistanceToTarget);
}
}
return StateStatus.Running;
}
}
}
#endregion Controller
#region NavMesh
private 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;
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"
}
;
private static readonly string[] BLOCKED_COLLIDERS = new string[] {
"rock", "cliff", "junk", "range", "invisible"
}
;
public static bool Find(Vector3 targetPosition, float maxDistance, out Vector3 position) {
for (int i = 0; i < 10; i++) {
position = i == 0 ? targetPosition : targetPosition + (UnityEngine.Random.onUnitSphere * maxDistance);
if (NavMesh.SamplePosition(position, out navmeshHit, maxDistance, 1)) {
if (IsInRockPrefab(navmeshHit.position)) continue;
if (IsNearWorldCollider(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;
}
}
#endregion
#region Oxide Hooks
private static NpcSpawn ins;
private List<CustomScientistNPC> Npcs = new List<CustomScientistNPC>();
private void Init() => ins = this;
private void Unload() {
foreach (CustomScientistNPC npc in Npcs) if (npc != null) npc.Kill(BaseNetworkable.DestroyMode.None);
ins = null;
}
private void OnEntityKill(CustomScientistNPC npc) {
if (npc != null && Npcs.Contains(npc)) Npcs.Remove(npc);
}
private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info) {
if (entity == null || info == null) return null;
BaseEntity attacker = info.Initiator;
if (entity is CustomScientistNPC && Npcs.Contains(entity as CustomScientistNPC)) {
CustomScientistNPC npc = entity as CustomScientistNPC;
if (npc.CurrentTarget == null && npc.CanTargetBasePlayer(attacker as BasePlayer)) npc.CurrentTarget = attacker as BasePlayer;
if (attacker is AutoTurret || attacker is GunTrap || attacker is FlameTurret) {
info.damageTypes.ScaleAll(0.1f);
return null;
}
if (attacker is BasePlayer && (attacker as BasePlayer).userID.IsSteamId()) return null;
else return true;
}
if (attacker is CustomScientistNPC && Npcs.Contains(attacker as CustomScientistNPC)) {
if (entity is BasePlayer) {
if ((entity as BasePlayer).userID.IsSteamId()) return null;
else return true;
}
else if (entity.OwnerID.IsSteamId()) return null;
else return true;
}
return null;
}
private object CanBeTargeted(CustomScientistNPC npc, MonoBehaviour behaviour) {
if (npc == null || behaviour == null) return null;
if (Npcs.Contains(npc)) {
BaseEntity attacker = behaviour as BaseEntity;
if (attacker != null) {
if ((attacker is AutoTurret || attacker is GunTrap || attacker is FlameTurret) && attacker.OwnerID != 0 && attacker.OwnerID.IsSteamId()) return null;
else return false;
}
else return false;
}
else return null;
}
private object OnNpcTarget(CustomScientistNPC npc, BaseEntity entity) {
// ... [Code preview truncated due to length. Full source in attached file] ...