1764 lines
79 KiB
C#
1764 lines
79 KiB
C#
using Facepunch;
|
|
using Oxide.Core.Plugins;
|
|
using ProtoBuf;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
namespace Oxide.Plugins
|
|
{
|
|
[Info("RECustomBots", "bmgjet", "1.0.6")]
|
|
[Description("Improves bots created with NPC_Spawner in rust edit")]
|
|
public class RECustomBots : RustPlugin
|
|
{
|
|
public bool DebugInfo = false;
|
|
//Plugin will remove any prefab at this exact location on server starting. You use it as a copy protection by placing something that stops
|
|
//servers from booting like a invisable pumpjack item on your map. Then copy its position value into below.
|
|
Vector3 MapLockLocation = new Vector3(0, 0, 0);
|
|
//Fixes doors collider to a smaller size so player can walk closer to them. But not break collider allowing bots to shoot though.
|
|
bool FixDoors = true;
|
|
//Prefab placeholder
|
|
string prefabplaceholder = "assets/prefabs/deployable/playerioents/gates/randswitch/electrical.random.switch.deployed.prefab";
|
|
//Bot Tick Rate
|
|
float bot_tick = 1f; //Default Apex was 0.5f, Default AINew was 0.25f
|
|
//Place holder scan distance
|
|
float ScanDistance = 1f;
|
|
//mount scan distance
|
|
float MountScan = 8f;
|
|
//Change the icon of the message announcer
|
|
ulong AnnouncementIcon = 0;
|
|
//Colour of announcer
|
|
string color = "red";
|
|
//Protect Sleepers
|
|
bool SleepProtect = true;
|
|
//Parachute Suiside Timer (will cut parachute off after this many seconds)
|
|
float ChuteSider = 20f;
|
|
//Checks this area around parachute for collision
|
|
float ColliderDistance = 2f;
|
|
//List of random taunt dead messages to send {N} gets replaced with attacker/victims name.
|
|
private string[] Dead =
|
|
{
|
|
"You got me, GG",
|
|
":(",
|
|
"Thats not nice {N}",
|
|
"Come on man?",
|
|
"Did you have to do that {N}",
|
|
"Argh again",
|
|
"Ur 2 gud {N}",
|
|
"SAD GUY",
|
|
};
|
|
private string[] Killed =
|
|
{
|
|
"haha your bad {N}",
|
|
":P EZ",
|
|
"Git Gud {N}",
|
|
"Wasted",
|
|
"Im not even trying {N}",
|
|
"HAHAHAHA {N}",
|
|
};
|
|
// * New Settings method using keywords in any order.
|
|
// * If a keyword isnt provided it will use the default for that setting.
|
|
// * Only what you provide is adjusted
|
|
// *
|
|
// * How to use:
|
|
// * Seperate keywords by "." which will show as " " when in rust edit looking at prefab groups.
|
|
// * Prefab group name must start with BMGBOT.
|
|
// *
|
|
// * Avaliable keywords
|
|
// * name= if set it will apply a specific name to the bot, Other wise will pick one at random based on userid the spawner creates.
|
|
// * kit= if set will apply kit to bot, Can do male/female specific kits by using ^ between them kit=malekit^femalekit
|
|
// * stationary if this keyword is present bot will remain stationary.
|
|
// * parachute if this keyword is present bot will parachute to navmesh.
|
|
// * replace if this keyword is present bot replace default items with kit items.
|
|
// * strip if this keyword is present bot will strip all its lot on death.
|
|
// * radiooff if this keyword is present bot will not use the radio to chatter.
|
|
// * peacekeeper if this keyword is present bot will only fire on hostile players.
|
|
// * mount if this keyword is present bot will mount the closest seat.
|
|
// * taunt if this keyword is present bot will make taunts in the chat to players it interacts with.
|
|
// * killnotice if this keyword is present kills/deaths of this bot will be announced to chat.
|
|
// * health= if set will adjust the bots health to this example health=150
|
|
// * attack= if set the boot will attack only up to this range loss of sight is a further 10f from this setting.
|
|
// * roam= if set this is how far the bot can move from its home spawn before it wants to return to home.
|
|
// * cooldown= if set changes the default home check rate.
|
|
// * height= if set can make small adjustment to bots navmesh height usuall settings range will be -3 to +3.
|
|
// * speed= if set adjusts how fast the bot can run.
|
|
// * steamicon= if set the bot will use the steamicon from this steamid example would be steamicon=76561198188915047
|
|
// * apcattack if this keyword is present bot will be targeted by the APC
|
|
// * canheal if this keyword is present bot will heal its self when its at half halth if its kitted with medical items.
|
|
// * damageScale= What percentage of normal damage the bot will do. 100% damage is the default which is same damage a player does.
|
|
// * accuracy= What percentage of shots will be at the target. 50% is the default.
|
|
// *
|
|
// * Example: bot with 500hp health and is stionary
|
|
// * BMGBOT.stationary.health=500
|
|
// *
|
|
// * Example: bot that has a 2 different kits for males and females, parachutes in, radio chatter disabled, default items removed.
|
|
// * BMGBOT.kit=guy1^girl1.radiooff.parachute.replace
|
|
// *
|
|
// * Example: bot with custom name is a peacekeeper and sitting in a chair
|
|
// * BMGBOT.name=Lazy Bot.peacekeeper.mount
|
|
|
|
//Weapons with issues
|
|
//crossbow.entity
|
|
//bow_hunter.entity
|
|
//speargun.entity
|
|
|
|
//Layers of collision
|
|
int parachuteLayer = 1 << (int)Rust.Layer.Water | 1 << (int)Rust.Layer.Transparent | 1 << (int)Rust.Layer.World | 1 << (int)Rust.Layer.Construction | 1 << (int)Rust.Layer.Debris | 1 << (int)Rust.Layer.Default | 1 << (int)Rust.Layer.Terrain | 1 << (int)Rust.Layer.Tree | 1 << (int)Rust.Layer.Vehicle_Large | 1 << (int)Rust.Layer.Deployed;
|
|
//Stored location of spawner and its settings
|
|
Dictionary<Vector3, BotsSettings> NPC_Spawners = new Dictionary<Vector3, BotsSettings>();
|
|
//Stored bot ID and what location to get settings from
|
|
Dictionary<ulong, Vector3> NPC_Bots = new Dictionary<ulong, Vector3>();
|
|
//Store bots items
|
|
Dictionary<ulong, List<Botsinfo>> NPC_Items = new Dictionary<ulong, List<Botsinfo>>();
|
|
//Ignored shots
|
|
List<ulong> IgnoredShots = new List<ulong>();
|
|
//Store List of place holders to remove
|
|
List<BaseEntity> PlaceHolders = new List<BaseEntity>();
|
|
//Check if fully started.
|
|
private bool HasLoadedSpawner = false;
|
|
//Random list of Steam IDs to use for bot images
|
|
private ulong[] SteamIds = {
|
|
76561198201252634,
|
|
76561199110323558,
|
|
76561199194720194,
|
|
76561198129951438,
|
|
76561198144464080,
|
|
76561198101891231,
|
|
76561198157637478,
|
|
76561199225578578,
|
|
76561198188915047,
|
|
76561199219052430,
|
|
76561198138463675,
|
|
76561199172929853,
|
|
76561198401529908,
|
|
76561198032752525,
|
|
76561199135596964,
|
|
76561198065829079,
|
|
76561198800018143,
|
|
76561198091879314
|
|
};
|
|
//Last taunt said to prevent it repeating them in a row.
|
|
int lasttaunt = -1;
|
|
//reference to kits plugin
|
|
[PluginReference]
|
|
private Plugin Kits;
|
|
//reference to self
|
|
private static RECustomBots plugin;
|
|
//Process Kit items into Botinfo
|
|
Botsinfo ProcessKitItem(Item item)
|
|
{
|
|
Botsinfo bi = new Botsinfo();
|
|
bi.item_ammount = item.amount;
|
|
bi.idef = item.info;
|
|
bi.item_skin = item.skin;
|
|
return bi;
|
|
}
|
|
//Info on items held by bot.
|
|
private class Botsinfo
|
|
{
|
|
public ItemDefinition idef;
|
|
public int item_ammount;
|
|
public ulong item_skin;
|
|
}
|
|
//Settings of the bot.
|
|
private class BotsSettings
|
|
{
|
|
//BotSettings Defaults
|
|
public string kitname = "bmgjet";
|
|
public bool stationary = false;
|
|
public int health = 0;
|
|
public float AttackRange = 30f;
|
|
public int roamrange = 20;
|
|
public int cooldown = 30;
|
|
public bool replaceitems = false;
|
|
public bool parachute = false;
|
|
public string name = "";
|
|
public bool radiooff = false;
|
|
public bool peacekeeper = false;
|
|
public bool taunts = false;
|
|
public bool killnotice = false;
|
|
public bool apcattack = false;
|
|
public bool canheal = false;
|
|
public float height = 0f;
|
|
public ulong steamicon = 0;
|
|
public bool strip = false;
|
|
public bool mount = false;
|
|
public int speed = -1;
|
|
public int damageScale = 100;
|
|
public int accuracy = 30;
|
|
}
|
|
//Bot script
|
|
public class BMGBOT : MonoBehaviour
|
|
{
|
|
//vars
|
|
int LastInteraction = 0;
|
|
int FailedHomes = 0;
|
|
bool flying = true;
|
|
Vector3 Home;
|
|
BotsSettings settings;
|
|
//References
|
|
BasePlayer bp;
|
|
HumanNPC bot;
|
|
NPCPlayer NPC;
|
|
ScarecrowBrain SBrain;
|
|
BaseNavigator BN;
|
|
//is healing
|
|
bool Healing = false;
|
|
//Held Gun
|
|
Item Gun;
|
|
|
|
//Keep track of parachute collider for other plugins
|
|
CapsuleCollider paracol;
|
|
private void Awake()
|
|
{
|
|
bp = this.GetComponent<BasePlayer>();
|
|
bot = this.GetComponent<HumanNPC>();
|
|
BN = this.GetComponent<BaseNavigator>();
|
|
NPC = this.GetComponent<NPCPlayer>();
|
|
if (bot == null)
|
|
{
|
|
SBrain = this.GetComponent<ScarecrowNPC>().Brain;
|
|
}
|
|
|
|
if (bp == null || BN == null || NPC == null) return;
|
|
//Check if its a bot from spawner
|
|
if (plugin.NPC_Bots.ContainsKey(bp.userID))
|
|
{
|
|
//Set a home point
|
|
Home = plugin.NPC_Bots[bp.userID];
|
|
var col = bp.gameObject.AddComponent<BoxCollider>();
|
|
col.size = new Vector3(1, 1f, 1);
|
|
//Load settings
|
|
settings = plugin.NPC_Spawners[plugin.NPC_Bots[bp.userID]];
|
|
//Check there are settings
|
|
if (settings == null)
|
|
{
|
|
//No Settings so be default
|
|
return;
|
|
}
|
|
//Pick a random Icon
|
|
if (settings.steamicon == 0)
|
|
{
|
|
settings.steamicon = plugin.SteamIds[Random.Range(0, plugin.SteamIds.Length - 1)] + (uint)100;
|
|
}
|
|
//Set as own owner
|
|
bp.OwnerID = bp.userID;
|
|
//Set bots name
|
|
if (settings.name == "" || settings.name == "0")
|
|
{
|
|
//Face Punches random function using steamid
|
|
settings.name = RandomUsernames.Get((int)bp.userID);
|
|
bp.name = settings.name;
|
|
plugin.NPC_Spawners[plugin.NPC_Bots[bp.userID]].name = settings.name;
|
|
}
|
|
else
|
|
{
|
|
//Set custom botname
|
|
bp.name = settings.name;
|
|
}
|
|
//Update the bot
|
|
bp.displayName = settings.name;
|
|
//Set bot health
|
|
if (settings.health != 0)
|
|
{
|
|
bp.startHealth = settings.health;
|
|
bp.InitializeHealth(settings.health, settings.health);
|
|
}
|
|
//Stop bot moving until its activated
|
|
if (settings.stationary)
|
|
{
|
|
BN.CanUseNavMesh = false;
|
|
}
|
|
//Stops attack range being completely 0 or negitive
|
|
if (settings.AttackRange <= 0)
|
|
{
|
|
settings.AttackRange = 1.5f;
|
|
}
|
|
if (settings.roamrange <= 0)
|
|
{
|
|
settings.roamrange = 5;
|
|
}
|
|
//Adds parachute to spawning
|
|
if (settings.parachute)
|
|
{
|
|
//Trigger flying and get collider around NPC
|
|
flying = true;
|
|
paracol = bp.GetComponent<CapsuleCollider>();
|
|
if (paracol != null)
|
|
{
|
|
//If not created then adjust radius
|
|
paracol.isTrigger = true;
|
|
bp.GetComponent<CapsuleCollider>().radius += 4f;
|
|
}
|
|
//Move bot
|
|
bp.transform.position = Home + new Vector3(0, 100, 0);
|
|
bp.gameObject.layer = 0;
|
|
//Adjust phyics stuff
|
|
var rb = bp.gameObject.GetComponent<Rigidbody>();
|
|
if (rb != null)
|
|
{
|
|
rb.drag = 0f;
|
|
rb.useGravity = false;
|
|
rb.isKinematic = false;
|
|
rb.velocity = new Vector3(bp.transform.forward.x * 0, 0, bp.transform.forward.z * 0) - new Vector3(0, 10, 0);
|
|
}
|
|
//Create the parachute
|
|
var Chute = GameManager.server.CreateEntity("assets/prefabs/misc/parachute/parachute.prefab", bp.transform.position, Quaternion.Euler(0, 0, 0));
|
|
if (Chute != null)
|
|
{
|
|
Chute.gameObject.Identity();
|
|
//Offset it to players back
|
|
Chute.transform.localPosition = Chute.transform.localPosition + new Vector3(0f, 1.3f, 0f);
|
|
//Attach player and spawn parachute
|
|
Chute.SetParent(bp);
|
|
Chute.Spawn();
|
|
plugin.ParachuteSuiside(bp);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//Attach bot to the ground.
|
|
ClipGround();
|
|
}
|
|
//Sets kit onto bot if ones set
|
|
if (settings.kitname != "")
|
|
{
|
|
string kitname = settings.kitname;
|
|
if (settings.kitname.Contains("^"))
|
|
{
|
|
//Do male or female specific kits
|
|
if (IsFemale(NPC.userID))
|
|
{
|
|
kitname = kitname.Split('^')[1];
|
|
}
|
|
else
|
|
{
|
|
kitname = kitname.Split('^')[0];
|
|
}
|
|
}
|
|
if (plugin.IsKit(kitname))
|
|
{
|
|
plugin.BotSkin(NPC, kitname, settings.replaceitems);
|
|
}
|
|
}
|
|
//stops down spamming when cool down set too low
|
|
if (settings.cooldown < 5)
|
|
{
|
|
settings.cooldown = 10;
|
|
}
|
|
//Do setting up of behavour
|
|
if (bot != null)
|
|
{
|
|
bot.Brain.SenseRange = settings.AttackRange;
|
|
bot.Brain.ListenRange = settings.AttackRange;
|
|
bot.Brain.TargetLostRange = settings.AttackRange;
|
|
bot.Brain.Navigator.MaxRoamDistanceFromHome = settings.roamrange;
|
|
bot.Brain.AttackRangeMultiplier = 1f;
|
|
bot.Brain.Senses.Memory.Targets.Clear();
|
|
bot.Brain.IgnoreSafeZonePlayers = true;
|
|
//Adjust speed
|
|
if (settings.speed != -1)
|
|
{
|
|
bot.Brain.Navigator.Speed = settings.speed;
|
|
}
|
|
//peacekeeper check;
|
|
bot.Brain.HostileTargetsOnly = settings.peacekeeper;
|
|
bot.Brain.Senses.Update();
|
|
}
|
|
else if (SBrain != null)
|
|
{
|
|
SBrain.SenseRange = settings.AttackRange;
|
|
SBrain.ListenRange = settings.AttackRange;
|
|
SBrain.TargetLostRange = settings.AttackRange;
|
|
SBrain.Navigator.MaxRoamDistanceFromHome = settings.roamrange;
|
|
SBrain.AttackRangeMultiplier = 1f;
|
|
SBrain.Senses.Memory.Targets.Clear();
|
|
SBrain.IgnoreSafeZonePlayers = true;
|
|
//Adjust speed
|
|
if (settings.speed != -1)
|
|
{
|
|
SBrain.Navigator.Speed = settings.speed;
|
|
}
|
|
//peacekeeper check;
|
|
SBrain.HostileTargetsOnly = settings.peacekeeper;
|
|
SBrain.Senses.Update();
|
|
}
|
|
else
|
|
{
|
|
bp.Kill();
|
|
return;
|
|
}
|
|
//Allows bot every topo
|
|
BN.topologyPreference = ((TerrainTopology.Enum)TerrainTopology.EVERYTHING);
|
|
NPC.damageScale = settings.damageScale / 100f;
|
|
//Output Debug Info
|
|
if (plugin.DebugInfo) plugin.Puts("Bot " + bp.displayName + " spawned,Health:" + bp.health + " Kit:" + settings.kitname + " Range:" + settings.AttackRange.ToString() + " Roam:" + settings.roamrange.ToString() + " Cooldown:" + settings.cooldown.ToString() + " Default Items:" + !settings.replaceitems + " Stationary:" + settings.stationary.ToString() + " Parachute:" + settings.parachute);
|
|
//Update Server With Bot
|
|
bp.SendNetworkUpdate();
|
|
//Setup repeating script after 5 secs at tick rate
|
|
InvokeRepeating("_tick", 5, plugin.bot_tick);
|
|
}
|
|
}
|
|
|
|
public BasePlayer GetBestTarget()
|
|
{
|
|
//Find best target from the list of targets the bot has
|
|
List<BaseEntity> Targets = new List<BaseEntity>();
|
|
//Get settings from senses
|
|
float SenseRange = settings.AttackRange;
|
|
float VisionCone = 0;
|
|
if (bot != null)
|
|
{
|
|
Targets = bot.Brain.Senses.Memory.Targets;
|
|
SenseRange = bot.Brain.SenseRange;
|
|
VisionCone = bot.Brain.VisionCone;
|
|
}
|
|
else if (SBrain != null)
|
|
{
|
|
Targets = SBrain.Senses.Memory.Targets;
|
|
SenseRange = SBrain.SenseRange;
|
|
VisionCone = SBrain.VisionCone;
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
BasePlayer target = null;
|
|
float delta = -1f;
|
|
foreach (BaseEntity baseEntity in Targets)
|
|
{
|
|
//Dont target low health or dead
|
|
if (baseEntity == null || baseEntity.Health() <= 0f) continue;
|
|
BasePlayer basePlayer = baseEntity as BasePlayer;
|
|
if (!CanTargetPlayer(basePlayer)) continue;
|
|
//Get closest player
|
|
float rangeDelta = 1f - Mathf.InverseLerp(1f, SenseRange, Vector3.Distance(basePlayer.transform.position, bp.transform.position));
|
|
float dot = Vector3.Dot((basePlayer.transform.position - bp.eyes.position).normalized, bp.eyes.BodyForward());
|
|
//Check if in vision
|
|
if (dot < VisionCone) continue;
|
|
rangeDelta += Mathf.InverseLerp(VisionCone, 1f, dot) / 2f;
|
|
if (bot != null)
|
|
{
|
|
rangeDelta += (bot.Brain.Senses.Memory.IsLOS(basePlayer) ? 2f : 0f);
|
|
}
|
|
else
|
|
{
|
|
rangeDelta += (SBrain.Senses.Memory.IsLOS(basePlayer) ? 2f : 0f);
|
|
}
|
|
if (rangeDelta <= delta) continue;
|
|
target = basePlayer;
|
|
delta = rangeDelta;
|
|
}
|
|
if (plugin.DebugInfo && target != null)
|
|
{
|
|
plugin.Puts("Found Target " + target.ToString());
|
|
}
|
|
return target;
|
|
}
|
|
|
|
public bool CanTargetPlayer(BasePlayer player)
|
|
{
|
|
//Conditions not to attack under
|
|
if (player == null || player.IsFlying || player.IsSleeping() || player.IsWounded() || player.IsDead()) return false;
|
|
return true;
|
|
}
|
|
|
|
private void ClipGround()
|
|
{
|
|
//get rigidbody reference
|
|
var rb = bp.gameObject.GetComponent<Rigidbody>();
|
|
//Scan for ground.
|
|
NavMeshHit hit;
|
|
if (NavMesh.SamplePosition(bp.transform.position, out hit, 30, -1))
|
|
{
|
|
//parachute colider reference remove
|
|
if (paracol != null)
|
|
{
|
|
paracol.isTrigger = false;
|
|
bp.GetComponent<CapsuleCollider>().radius -= 4f;
|
|
}
|
|
//Water check
|
|
if (bp.WaterFactor() > 0.9f)
|
|
{
|
|
bp.Kill();
|
|
return;
|
|
}
|
|
//Remove any phyics alterations
|
|
rb.isKinematic = true;
|
|
rb.useGravity = false;
|
|
bp.gameObject.layer = 17;
|
|
//Offset adjustment
|
|
bp.ServerPosition = hit.position -= new Vector3(0, (settings.height / 10), 0);
|
|
BN.Agent.Move(bp.ServerPosition);
|
|
//Remove any attached Parachutes
|
|
bool Destroyed = false;
|
|
foreach (var child in bp.children.Where(child => child.name.Contains("parachute")))
|
|
{
|
|
child.SetParent(null);
|
|
child.Kill();
|
|
Destroyed = true;
|
|
break;
|
|
}
|
|
//Play sound fx if a parachute gets destroyed
|
|
if (Destroyed)
|
|
{
|
|
Effect.server.Run("assets/bundled/prefabs/fx/player/groundfall.prefab", bp.transform.position);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//Unable to detect ground. Let player fall.
|
|
rb.useGravity = true;
|
|
rb.drag = 1f;
|
|
rb.velocity = new Vector3(bp.transform.forward.x * 15, 11, bp.transform.forward.z * 15);
|
|
}
|
|
//Player has been grounded
|
|
flying = false;
|
|
if (BN != null)
|
|
{
|
|
// BN.Warp(Home);
|
|
BN.CanUseNavMesh = !settings.stationary;
|
|
}
|
|
//mounts bot onto nearby mount point if set.
|
|
if (settings.mount)
|
|
{
|
|
plugin.MountBot(bp);
|
|
}
|
|
}
|
|
|
|
//Collider to remove parachute
|
|
private void OnCollisionEnter(Collision col)
|
|
{
|
|
//Ignore collider if not flying
|
|
if (!flying)
|
|
return;
|
|
//Attach to navmesh
|
|
ClipGround();
|
|
}
|
|
|
|
bool CheckColliders()
|
|
{
|
|
//Scans the area
|
|
foreach (Collider col in Physics.OverlapSphere(bp.transform.position, plugin.ColliderDistance, plugin.parachuteLayer))
|
|
{
|
|
//Converts each collider to a string
|
|
string thisobject = col.gameObject.ToString();
|
|
//Checks if collider contains partial names
|
|
if (thisobject.Contains("modding") || thisobject.Contains("props") || thisobject.Contains("structures") || thisobject.Contains("building core")) { return true; }
|
|
//Check if its a base entity
|
|
BaseEntity baseEntity = col.gameObject.ToBaseEntity();
|
|
if (baseEntity != null && (baseEntity == bp || baseEntity == bp.GetComponent<BaseEntity>()))
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void GoHome()
|
|
{
|
|
//Remove events from bot, Set to normal movement speed, Make them act if they are going to cover
|
|
if (bot != null)
|
|
{
|
|
bot.Brain.Navigator.SetDestination(Home, BaseNavigator.NavigationSpeed.Normal);
|
|
}
|
|
else if (SBrain != null)
|
|
{
|
|
SBrain.Navigator.SetDestination(Home, BaseNavigator.NavigationSpeed.Normal);
|
|
}
|
|
//Change flags
|
|
bp.SetPlayerFlag(BasePlayer.PlayerFlags.Relaxed, true);
|
|
bp.SetPlayerFlag(BasePlayer.PlayerFlags.Aiming, false);
|
|
}
|
|
|
|
void ForgetAll(int resetdelay)
|
|
{
|
|
//Clear all memory and disable senses for a little bit to gain some distance.
|
|
if (bot != null)
|
|
{
|
|
bot.Brain.Senses.Memory.Targets.Clear();
|
|
bot.Brain.Senses.Memory.Threats.Clear();
|
|
bot.Brain.Senses.Memory.All.Clear();
|
|
bot.Brain.Senses.Memory.LOS.Clear();
|
|
bot.Brain.HostileTargetsOnly = settings.peacekeeper;
|
|
bot.Brain.SetEnabled(false);
|
|
Invoke("ResetSenses", resetdelay);
|
|
}
|
|
else if (SBrain != null)
|
|
{
|
|
SBrain.Senses.Memory.Targets.Clear();
|
|
SBrain.Senses.Memory.Threats.Clear();
|
|
SBrain.Senses.Memory.All.Clear();
|
|
SBrain.Senses.Memory.LOS.Clear();
|
|
SBrain.HostileTargetsOnly = settings.peacekeeper;
|
|
SBrain.SetEnabled(false);
|
|
Invoke("ResetSenses", resetdelay);
|
|
}
|
|
}
|
|
|
|
void ResetSenses()
|
|
{
|
|
//Delayed enabling of senses
|
|
if (bot != null)
|
|
{
|
|
bot.Brain.SetEnabled(true);
|
|
}
|
|
else if (SBrain != null)
|
|
{
|
|
SBrain.SetEnabled(true);
|
|
}
|
|
}
|
|
|
|
void HomeChecks()
|
|
{
|
|
//Dont try to move if bot is sitting.
|
|
if (bp.isMounted)
|
|
{
|
|
//Do Some Seated stuff
|
|
return;
|
|
}
|
|
//Return home if no more activity.
|
|
if (LastInteraction >= settings.cooldown && Vector3.Distance(bp.transform.position, Home) > settings.roamrange && !flying)
|
|
{
|
|
//Remove details from bot of its last taget
|
|
bp.LastAttackedDir = Home;
|
|
bp.lastAttacker = null;
|
|
//If its still not with in its roam distance after 5 checks force warp it back.
|
|
if (FailedHomes >= 5)
|
|
{
|
|
if (plugin.DebugInfo) plugin.Puts(bp.displayName + " Forced Home");
|
|
BN.Agent.Warp(Home);
|
|
FailedHomes = 0;
|
|
LastInteraction = 0;
|
|
GoHome();
|
|
return;
|
|
}
|
|
FailedHomes++;
|
|
//Reset interaction and movement
|
|
LastInteraction = 0;
|
|
if (plugin.DebugInfo) plugin.Puts(bp.displayName + " Going Home");
|
|
try
|
|
{
|
|
ForgetAll(10);
|
|
GoHome();
|
|
return;
|
|
}
|
|
catch
|
|
{
|
|
bp.Kill();
|
|
return;
|
|
}
|
|
}
|
|
//No interaction this tick
|
|
LastInteraction++;
|
|
}
|
|
|
|
void AttackLogic(BasePlayer AttackPlayer)
|
|
{
|
|
//Checks if its a gun or melee
|
|
var gun = NPC.GetGun();
|
|
AttackEntity AE = NPC?.GetHeldEntity() as AttackEntity;
|
|
//Gives them a rock if they have nothing
|
|
if (AE == null)
|
|
{
|
|
ItemDefinition itemDefinition = ItemManager.FindItemDefinition("rock");
|
|
Item rock = ItemManager.Create(itemDefinition, 1, 0);
|
|
NPC.GiveItem(rock);
|
|
NPC.UpdateActiveItem(rock.uid);
|
|
}
|
|
if (gun == null && AE != null)
|
|
{
|
|
if (plugin.DebugInfo) plugin.Puts("Melee Trigger " + AE.ShortPrefabName);
|
|
switch (AE.ShortPrefabName)
|
|
{
|
|
case "flamethrower.entity":
|
|
InvokeRepeating("use", 0f, 0.025f);
|
|
plugin.BotMeleeAttack(NPC, AttackPlayer, AE, Rust.DamageType.Heat, 5, "assets/bundled/prefabs/fx/impacts/additive/fire.prefab", 100);
|
|
return;
|
|
|
|
case "watergun.entity":
|
|
//Need Fix
|
|
LiquidWeapon LW = AE as LiquidWeapon;
|
|
LW.AutoPump = true;
|
|
plugin.FireWaterGun(NPC, LW);
|
|
return;
|
|
|
|
case "waterpistol.entity":
|
|
//Need Fix
|
|
LiquidWeapon LW2 = AE as LiquidWeapon;
|
|
LW2.AutoPump = true;
|
|
plugin.FireWaterGun(NPC, LW2);
|
|
return;
|
|
|
|
case "grenade.f1.entity":
|
|
AE.repeatDelay = 5f;
|
|
plugin.ServerThrow(AttackPlayer.transform.position, AE as ThrownWeapon, NPC);
|
|
return;
|
|
case "grenade.beancan.entity":
|
|
AE.repeatDelay = 5f;
|
|
plugin.ServerThrow(AttackPlayer.transform.position, AE as ThrownWeapon, NPC);
|
|
return;
|
|
case "smoke_grenade.weapon":
|
|
AE.repeatDelay = 5f;
|
|
plugin.ServerThrow(AttackPlayer.transform.position, AE as ThrownWeapon, NPC);
|
|
return;
|
|
case "snowball.entity":
|
|
AE.repeatDelay = 5f;
|
|
return;
|
|
};
|
|
|
|
BaseMelee weapon = NPC?.GetHeldEntity() as BaseMelee;
|
|
if (weapon == null)
|
|
{
|
|
return;
|
|
}
|
|
//Do melee slash if in its reach
|
|
if (!AE.HasAttackCooldown() && Vector3.Distance(AttackPlayer.transform.position, NPC.transform.position) <= weapon.maxDistance)
|
|
{
|
|
//melee hit
|
|
if (plugin.DebugInfo) plugin.Puts("Trigger Melee Attack");
|
|
if (NPC.MeleeAttack())
|
|
{
|
|
//Apply Damage
|
|
plugin.BotMeleeAttack(NPC, AttackPlayer, weapon, Rust.DamageType.Slash, weapon.TotalDamage());
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch (NPC.GetHeldEntity().ShortPrefabName)
|
|
{
|
|
//Rocket Launcher Logic
|
|
case "rocket_launcher.entity":
|
|
gun.repeatDelay = 5f;
|
|
plugin.EmulatedFire(gun, NPC, AttackPlayer);
|
|
return;
|
|
//Grenade Launcher Logic
|
|
case "mgl.entity":
|
|
gun.repeatDelay = 2f;
|
|
plugin.EmulatedFire(gun, NPC, AttackPlayer);
|
|
return;
|
|
}
|
|
|
|
if (gun != null)
|
|
{
|
|
//Do gun trigger
|
|
if (plugin.DebugInfo) plugin.Puts("Trigger Shoot");
|
|
float triggertime = UnityEngine.Random.Range(gun.attackLengthMin, gun.attackLengthMax);
|
|
if (triggertime == -1)
|
|
{
|
|
//Some guns have -1 as there attacklength so give them one based on ammo count
|
|
triggertime = gun.primaryMagazine.contents / 4;
|
|
if (triggertime == 0) triggertime = 1f;
|
|
}
|
|
InvokeRepeating("use", 0f, 0.025f);
|
|
}
|
|
//reload gun if less than 1 shot left.
|
|
int ammo = 0;
|
|
try { ammo = gun.primaryMagazine.contents; } catch { return; }
|
|
if (ammo < 1)
|
|
{
|
|
if (plugin.DebugInfo) plugin.Puts("Trigger Reload");
|
|
NPC.AttemptReload();
|
|
}
|
|
}
|
|
}
|
|
|
|
void use()
|
|
{
|
|
if (NPC != null)
|
|
{
|
|
try
|
|
{
|
|
NPC.GetHeldEntity().ServerUse(NPC.damageScale);
|
|
}
|
|
catch { }
|
|
}
|
|
//Stop Invoke
|
|
CancelInvoke("use");
|
|
}
|
|
|
|
void SwitchGun()
|
|
{
|
|
NPC.UpdateActiveItem(Gun.uid);
|
|
NPC.SendNetworkUpdateImmediate();
|
|
Healing = false;
|
|
}
|
|
|
|
void _tick()
|
|
{
|
|
//Check if bot exsists
|
|
if (bp == null)
|
|
{
|
|
Destroy(this);
|
|
return;
|
|
}
|
|
//Fall back check if bot falls though map while parachuting with out hitting a navmesh.
|
|
if (flying)
|
|
{
|
|
if (TerrainMeta.HeightMap.GetHeight(bp.transform.position) >= bp.transform.position.y || CheckColliders())
|
|
{
|
|
ClipGround();
|
|
return;
|
|
}
|
|
}
|
|
//Stops attacking when in safe zone
|
|
if (bp.InSafeZone())
|
|
{
|
|
ForgetAll(30);
|
|
GoHome();
|
|
return;
|
|
}
|
|
//Dont run check if bot is downed or dead
|
|
if (bp.IsCrawling() || bp.IsDead() || bp.IsWounded())
|
|
{
|
|
return;
|
|
}
|
|
//Check if bot should be stationary but has moved.
|
|
if (settings.stationary)
|
|
{
|
|
if (Vector3.Distance(bp.transform.position, Home) > 1f)
|
|
{
|
|
//Force back to spawners location
|
|
BN.Agent.Warp(Home);
|
|
}
|
|
}
|
|
//Heal logic
|
|
if (NPC.health < NPC.MaxHealth() / 2 && !Healing && settings.canheal)
|
|
{
|
|
if (plugin.DebugInfo) plugin.Puts("Bot is Healing");
|
|
//Store current held gun if not medical item
|
|
if (NPC.GetHeldEntity() is MedicalTool)
|
|
{
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
//Store gun
|
|
Gun = NPC.GetHeldEntity().GetItem();
|
|
}
|
|
catch
|
|
{
|
|
//Failed to store gun so end function
|
|
return;
|
|
}
|
|
//Check bot for med items
|
|
foreach (var item in NPC.inventory.containerBelt.itemList.ToList())
|
|
{
|
|
if (item.GetHeldEntity() is MedicalTool)
|
|
{
|
|
//Switch to the meds
|
|
NPC.UpdateActiveItem(item.uid);
|
|
NPC.inventory.UpdatedVisibleHolsteredItems();
|
|
//use meds
|
|
MedicalTool meds = NPC.GetHeldEntity() as MedicalTool;
|
|
if (meds != null)
|
|
{
|
|
Healing = true;
|
|
meds.ServerUse();
|
|
}
|
|
//Switch back to gun
|
|
Invoke("SwitchGun", 4f);
|
|
break;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
//Find Targeted Player
|
|
BasePlayer AttackPlayer = GetBestTarget();
|
|
//Fallback funding players in CQ slower method if nothing found
|
|
if (AttackPlayer == null)
|
|
{
|
|
List<BasePlayer> PlayerScan = new List<BasePlayer>();
|
|
Vis.Entities<BasePlayer>(bp.transform.position, 10, PlayerScan);
|
|
foreach (BasePlayer entity in PlayerScan)
|
|
{
|
|
//Stops shooting though walls/doors
|
|
if (BasePlayer.activePlayerList.Contains(entity) && bp.IsVisibleAndCanSee(entity.eyes.position))
|
|
{
|
|
//Found a player to attack
|
|
AttackPlayer = entity;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
//No players with in range do nothing.
|
|
if (AttackPlayer == null)
|
|
{
|
|
HomeChecks();
|
|
return;
|
|
}
|
|
//Reset Home Checking
|
|
FailedHomes = 0;
|
|
LastInteraction = 0;
|
|
|
|
//Make sure its not another bot
|
|
if (!AttackPlayer.IsNpc)
|
|
{
|
|
//Peacekeeper bypass attack code.
|
|
if (settings.peacekeeper && !AttackPlayer.IsHostile())
|
|
{
|
|
return;
|
|
}
|
|
//Sleeper Protection
|
|
if (plugin.SleepProtect && AttackPlayer.IsSleeping())
|
|
{
|
|
return;
|
|
}
|
|
|
|
//Adjust Chance faces the right way.
|
|
//BN.SetFacingDirectionOverride(AttackPlayer.transform.position += new Vector3(-UnityEngine.Random.Range(0.0f, 1.0f), -UnityEngine.Random.Range(0.0f , 1.0f), -UnityEngine.Random.Range(0.0f, 1.0f))); ;
|
|
//Turn to the player thats the target
|
|
if (UnityEngine.Random.Range(1, 101) <= settings.accuracy)
|
|
{
|
|
BN.SetFacingDirectionEntity(AttackPlayer);
|
|
if (plugin.IgnoredShots.Contains(bp.userID))
|
|
plugin.IgnoredShots.Remove(bp.userID);
|
|
}
|
|
else
|
|
{
|
|
if (!plugin.IgnoredShots.Contains(bp.userID))
|
|
plugin.IgnoredShots.Add(bp.userID);
|
|
}
|
|
|
|
|
|
|
|
//Attack
|
|
AttackLogic(AttackPlayer);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnServerInitialized(bool initial)
|
|
{
|
|
plugin = this;
|
|
//Delay startup if fresh server boot. Helps hooking on slow servers
|
|
if (initial)
|
|
{
|
|
Fstartup();
|
|
return;
|
|
}
|
|
//Startup plugin
|
|
Startup();
|
|
}
|
|
private void Fstartup()
|
|
{
|
|
//Waits for fully loaded before running script to help first startup performance.
|
|
timer.Once(10f, () =>
|
|
{
|
|
try
|
|
{
|
|
if (Rust.Application.isLoading)
|
|
{
|
|
//Still starting so run a timer again in 10 sec to check.
|
|
Fstartup();
|
|
return;
|
|
}
|
|
}
|
|
catch { }
|
|
//Starup script now.
|
|
Startup();
|
|
});
|
|
}
|
|
|
|
private void Startup()
|
|
{
|
|
//Clears clocksettings incase its triggered reload.
|
|
NPC_Spawners.Clear();
|
|
uint placeholderid = StringPool.Get(prefabplaceholder);
|
|
//Find All NPCSpawners in the map
|
|
for (int i = World.Serialization.world.prefabs.Count - 1; i >= 0; i--)
|
|
{
|
|
PrefabData prefabdata = World.Serialization.world.prefabs[i];
|
|
//Check the prefab datas category since thats where customprefabs names are stored
|
|
if (prefabdata.id == placeholderid && prefabdata.category.Contains("BMGBOT"))
|
|
{
|
|
//Pull settings from prefab name
|
|
string settings = prefabdata.category.Split(':')[1].Replace("\\", "");
|
|
//Check if it is already in dict
|
|
if (!NPC_Spawners.ContainsKey(prefabdata.position))
|
|
{
|
|
BotsSettings bs = new BotsSettings();
|
|
if (settings != null)
|
|
{
|
|
//Settings are seperated by a fullstop
|
|
string[] ParsedSettings = settings.Split('.');
|
|
foreach (string keyword in ParsedSettings)
|
|
{
|
|
if (keyword.ToLower().Contains("stationary")) { bs.stationary = true; }
|
|
else if (keyword.ToLower().Contains("name")) { try { bs.name = keyword.Split('=')[1]; } catch { bs.name = ""; } }
|
|
else if (keyword.ToLower().Contains("kit")) { try { bs.kitname = keyword.Split('=')[1]; } catch { bs.kitname = ""; } }
|
|
else if (keyword.ToLower().Contains("parachute")) { bs.parachute = true; }
|
|
else if (keyword.ToLower().Contains("replace")) { bs.replaceitems = true; }
|
|
else if (keyword.ToLower().Contains("health")) { try { bs.health = int.Parse(keyword.Split('=')[1]); } catch { bs.health = 0; } }
|
|
else if (keyword.ToLower().Contains("attack")) { try { bs.AttackRange = int.Parse(keyword.Split('=')[1]); } catch { bs.AttackRange = 30; } }
|
|
else if (keyword.ToLower().Contains("roam")) { try { bs.roamrange = int.Parse(keyword.Split('=')[1]); } catch { bs.roamrange = 30; } }
|
|
else if (keyword.ToLower().Contains("cooldown")) { try { bs.cooldown = int.Parse(keyword.Split('=')[1]); } catch { bs.cooldown = 15; } }
|
|
else if (keyword.ToLower().Contains("peacekeeper")) { bs.peacekeeper = true; }
|
|
else if (keyword.ToLower().Contains("radiooff")) { bs.radiooff = true; }
|
|
else if (keyword.ToLower().Contains("taunt")) { bs.taunts = true; }
|
|
else if (keyword.ToLower().Contains("height")) { try { bs.height = int.Parse(keyword.Split('=')[1]); } catch { bs.height = 0; } }
|
|
else if (keyword.ToLower().Contains("steamicon")) { try { bs.steamicon = ulong.Parse(keyword.Split('=')[1]); } catch { bs.steamicon = 0; } }
|
|
else if (keyword.ToLower().Contains("speed")) { try { bs.speed = int.Parse(keyword.Split('=')[1]); } catch { bs.speed = 0; } }
|
|
else if (keyword.ToLower().Contains("accuracy")) { try { bs.accuracy = int.Parse(keyword.Split('=')[1]); } catch { bs.accuracy = 50; } }
|
|
else if (keyword.ToLower().Contains("damageScale")) { try { bs.damageScale = int.Parse(keyword.Split('=')[1]); } catch { bs.damageScale = 100; } }
|
|
else if (keyword.ToLower().Contains("strip")) { bs.strip = true; }
|
|
else if (keyword.ToLower().Contains("mount")) { bs.mount = true; }
|
|
else if (keyword.ToLower().Contains("killnotice")) { bs.killnotice = true; }
|
|
else if (keyword.ToLower().Contains("apcattack")) { bs.apcattack = true; }
|
|
else if (keyword.ToLower().Contains("canheal")) { bs.canheal = true; }
|
|
}
|
|
}
|
|
//Create Dictornary reference
|
|
if (!NPC_Spawners.ContainsKey(prefabdata.position))
|
|
{
|
|
NPC_Spawners.Add(prefabdata.position, bs);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//Set plugin as fully ready
|
|
HasLoadedSpawner = true;
|
|
//Clean up place holders
|
|
RemoveAllPlaceHolders();
|
|
//Outputs debug info
|
|
Puts("Found " + NPC_Spawners.Count.ToString() + " NPC Spawners");
|
|
}
|
|
|
|
private void Unload()
|
|
{
|
|
plugin = null;
|
|
//removes the script
|
|
foreach (var script in GameObject.FindObjectsOfType<NPCPlayer>())
|
|
{
|
|
script.EnsureDismounted();
|
|
foreach (var af in script.GetComponentsInChildren<BMGBOT>())
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(af);
|
|
//Kill bot since wont get re hooked if its moved from its spawner
|
|
script.Kill();
|
|
}
|
|
}
|
|
}
|
|
|
|
void OnWorldPrefabSpawned(GameObject gameObject, string str)
|
|
{
|
|
//Creates a list of prefab used as placeholders to make the prefab group.
|
|
if (gameObject.name == prefabplaceholder)
|
|
{
|
|
PlaceHolders.Add(gameObject.GetComponent<BaseEntity>());
|
|
}
|
|
//Fix invalid prefab stopping server starting
|
|
BaseEntity component = gameObject.GetComponent<BaseEntity>();
|
|
if (component != null)
|
|
{
|
|
if (component.OwnerID == 0 && gameObject.transform.position == MapLockLocation && MapLockLocation != new Vector3(0, 0, 0))
|
|
{
|
|
if (DebugInfo) Puts("Removed Invalid Prefab @ " + MapLockLocation);
|
|
//Remove Invalid Prefab thats used as somewhat protection.
|
|
component.Kill();
|
|
return;
|
|
}
|
|
|
|
//Door collider shrink but not destroy to stop bots shooting though then.
|
|
if (FixDoors)
|
|
{
|
|
Door doorfix = component.GetComponent<Door>();
|
|
if (doorfix != null)
|
|
{
|
|
if (DebugInfo) Puts("Door Fixed @ " + doorfix.transform.position.ToString());
|
|
//Scales door X to 0.1 so it still has some thickness to stop bot bullet and vison but not so thick player hits on it.
|
|
doorfix.transform.localScale = new Vector3(0.1f, doorfix.transform.localScale.y, doorfix.transform.localScale.z);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
|
|
{
|
|
if (info != null && info.InitiatorPlayer != null && entity != null)
|
|
{
|
|
BasePlayer player = info.InitiatorPlayer;
|
|
if (IgnoredShots.Contains(player.userID))
|
|
{
|
|
info.damageTypes.ScaleAll(0.1f);
|
|
IgnoredShots.Remove(player.userID);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
void OnEntityDeath(BaseEntity entity, HitInfo info)
|
|
{
|
|
try
|
|
{
|
|
if (entity == null || info == null) return;
|
|
//Dismount bots on seat break
|
|
BaseMountable seat = entity.GetComponent<BaseMountable>();
|
|
if (seat != null)
|
|
{
|
|
if (seat.GetMounted().IsNpc)
|
|
{
|
|
seat.DismountAllPlayers();
|
|
}
|
|
}
|
|
//Get player
|
|
BasePlayer player = null;
|
|
player = entity.ToPlayer();
|
|
if (player == null) return;
|
|
//Get who was the attacker
|
|
BasePlayer attacker = null;
|
|
attacker = player.lastAttacker.ToPlayer();
|
|
if (attacker == null) return;
|
|
//Failsafe so while loop cant get stuck going on for ever
|
|
int failsafe = 0;
|
|
|
|
//Taunt bot kills
|
|
if (NPC_Bots.ContainsKey(attacker.userID))
|
|
{
|
|
if (NPC_Spawners.ContainsKey(NPC_Bots[attacker.userID]))
|
|
{
|
|
if (NPC_Spawners[NPC_Bots[attacker.userID]].taunts)
|
|
{
|
|
//Pick a taunt at randm making sure not to be a repeat
|
|
if (Killed == null) return;
|
|
int seed = 0;
|
|
seed = Random.Range(0, Killed.Length - 1);
|
|
while (seed == lasttaunt && failsafe < 10)
|
|
{
|
|
failsafe++;
|
|
seed = Random.Range(0, Killed.Length - 1);
|
|
}
|
|
lasttaunt = seed;
|
|
//Send taunt to chat.
|
|
CreateTaunt(Killed[seed].Replace("{N}", player.displayName), attacker, NPC_Spawners[NPC_Bots[attacker.userID]].steamicon);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
//taunt bot dies
|
|
if (NPC_Bots.ContainsKey(player.userID))
|
|
{
|
|
if (NPC_Spawners.ContainsKey(NPC_Bots[player.userID]))
|
|
{
|
|
//Public bot death annoucements
|
|
if (NPC_Spawners[NPC_Bots[player.userID]].killnotice)
|
|
{
|
|
//Send kill announcement to chat.
|
|
CreateAnouncment(attacker.displayName + " Killed " + player.name + " With " + info.damageTypes.GetMajorityDamageType());
|
|
}
|
|
if (NPC_Spawners[NPC_Bots[player.userID]].taunts)
|
|
{
|
|
//Pick a taunt at randm making sure not to be a repeat
|
|
int seed = Random.Range(0, Dead.Length - 1);
|
|
while (seed == lasttaunt && failsafe < 10)
|
|
{
|
|
failsafe++;
|
|
seed = Random.Range(0, Dead.Length - 1);
|
|
}
|
|
lasttaunt = seed;
|
|
//Send taunt to chat.
|
|
CreateTaunt(Dead[seed].Replace("{N}", attacker.displayName), player.ToPlayer(), NPC_Spawners[NPC_Bots[player.userID]].steamicon);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
void OnEntitySpawned(BaseNetworkable entity)
|
|
{
|
|
//Fix console spam
|
|
BaseNavigator baseNavigator = entity.GetComponent<BaseNavigator>();
|
|
//Checks if has navigator AI
|
|
if (baseNavigator != null)
|
|
{
|
|
//temp position
|
|
Vector3 pos;
|
|
//Checks if its within the default navmesh scan settings
|
|
if (!baseNavigator.GetNearestNavmeshPosition(entity.transform.position + (Vector3.one * 2f), out pos, (baseNavigator.IsSwimming() ? 30f : 6f)))
|
|
{
|
|
//Sets as stationary
|
|
if (DebugInfo) Puts("No Navmesh found below Bot @ " + entity.transform.position.ToString() + " bot will be frozen in place");
|
|
baseNavigator.CanUseNavMesh = false;
|
|
}
|
|
}
|
|
//Cast as a NPCPlayer
|
|
NPCPlayer bot = entity as NPCPlayer;
|
|
//Check if NPC
|
|
if (bot != null)
|
|
{
|
|
//Checks if NPC_Spawner list has been filled
|
|
if (!HasLoadedSpawner)
|
|
Startup();
|
|
//Checks bots against NPC_Spawner
|
|
NextFrame(() => { CheckBot(bot); });
|
|
}
|
|
//Check corpses
|
|
if (entity.ShortPrefabName == "frankensteinpet_corpse")
|
|
{
|
|
//Delay until next frame to allow it to be spawned
|
|
NextFrame(() =>
|
|
{
|
|
LootableCorpse corpse = entity as LootableCorpse;
|
|
if (corpse != null)
|
|
{
|
|
UpdateCorpse(corpse);
|
|
corpse.SendNetworkUpdateImmediate();
|
|
}
|
|
});
|
|
}
|
|
else if (entity.ShortPrefabName == "scientist_corpse")
|
|
{
|
|
//Delay until next frame to allow it to be spawned
|
|
NextFrame(() =>
|
|
{
|
|
LootableCorpse corpse = entity as LootableCorpse;
|
|
if (corpse != null)
|
|
{
|
|
UpdateCorpse(corpse);
|
|
corpse.SendNetworkUpdateImmediate();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void UpdateCorpse(LootableCorpse corpse)
|
|
{
|
|
//Checks if its a NPC
|
|
if (NPC_Bots.ContainsKey(corpse.playerSteamID))
|
|
{
|
|
//Checks if settings from the spawner
|
|
if (NPC_Spawners.ContainsKey(NPC_Bots[corpse.playerSteamID]))
|
|
{
|
|
//If its set to replace items or strip them
|
|
if (NPC_Spawners[NPC_Bots[corpse.playerSteamID]].replaceitems || NPC_Spawners[NPC_Bots[corpse.playerSteamID]].strip)
|
|
{
|
|
//Empty default items off corpse
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
corpse.containers[i].Kill();
|
|
}
|
|
//Only remove items
|
|
if (NPC_Spawners[NPC_Bots[corpse.playerSteamID]].strip) return;
|
|
|
|
//Checks list of saved items from kit
|
|
if (NPC_Items.ContainsKey(corpse.playerSteamID))
|
|
{
|
|
if (DebugInfo) Puts("Moving Items");
|
|
foreach (Botsinfo items in NPC_Items[corpse.playerSteamID])
|
|
{
|
|
corpse.containers[0].AddItem(items.idef, items.item_ammount, items.item_skin);
|
|
}
|
|
//Emptys kit item list
|
|
NPC_Items.Remove(corpse.playerSteamID);
|
|
}
|
|
//Wait 1 sec since RE and other plugins might be adding stuff on the next frame already.
|
|
timer.Once(1f, () =>
|
|
{
|
|
if (corpse == null)
|
|
{
|
|
return;
|
|
}
|
|
//Remove the delayed outfits rust edit adds to frankenstine and scientist corpses
|
|
foreach (ItemContainer ic in corpse.containers)
|
|
{
|
|
foreach (Item it in ic.itemList.ToArray())
|
|
{
|
|
if (it.info.shortname.Contains("halloween.mummysuit") || it.info.shortname.Contains("scarecrow.suit") || it.info.shortname.Contains("hazmatsuit_scientist"))
|
|
{
|
|
it.Remove();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
//Update corpses name
|
|
corpse.playerName = NPC_Spawners[NPC_Bots[corpse.playerSteamID]].name;
|
|
//Remove stored bot since its dead.
|
|
NPC_Bots.Remove(corpse.playerSteamID);
|
|
}
|
|
}
|
|
}
|
|
|
|
void RemoveAllPlaceHolders()
|
|
{
|
|
//Go though each placeholder and make sure its with in range of the NPC Spawner before removing it.
|
|
foreach (BaseEntity PH in PlaceHolders)
|
|
{
|
|
if (PH != null)
|
|
{
|
|
foreach (KeyValuePair<Vector3, BotsSettings> Spawners in NPC_Spawners)
|
|
{
|
|
if (Vector3.Distance(PH.transform.position, Spawners.Key) < ScanDistance)
|
|
{
|
|
try { PH.Kill(); } catch { }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Sends message to all active players under a steamID
|
|
void CreateAnouncment(string msg)
|
|
{
|
|
foreach (BasePlayer current in BasePlayer.activePlayerList.ToArray())
|
|
{
|
|
if (current.IsConnected)
|
|
{
|
|
rust.SendChatMessage(current, "<color=" + color + ">Death</color>", msg, AnnouncementIcon.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
//Sends chat message to all active players.
|
|
void CreateTaunt(string msg, BasePlayer player, ulong steamid)
|
|
{
|
|
foreach (BasePlayer current in BasePlayer.activePlayerList.ToArray())
|
|
{
|
|
if (current.IsConnected)
|
|
{
|
|
rust.SendChatMessage(current, "<color=#55aaff>" + player.displayName + "</color>:", msg, steamid.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
void CheckBot(NPCPlayer bot)
|
|
{
|
|
if (bot == null) return;
|
|
//Loop all the NPC_Spawners that have been hooked.
|
|
for (int i = 0; i < NPC_Spawners.Count; i++)
|
|
{
|
|
//Checks with in 5f range of spawner since bot could of moved slightly.
|
|
if (Vector3.Distance(NPC_Spawners.ElementAt(i).Key, bot.transform.position) < 5f)
|
|
{
|
|
if (!NPC_Bots.ContainsKey(bot.userID))
|
|
{
|
|
NPC_Bots.Add(bot.userID, NPC_Spawners.ElementAt(i).Key);
|
|
//Attach script if its not already attached
|
|
if (bot.gameObject.GetComponent<BMGBOT>() == null)
|
|
{
|
|
bot.gameObject.AddComponent<BMGBOT>();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//Murdurer Fix Delay so it can spawn chainsaw then start it so it will do damage.
|
|
timer.Once(2f, () =>
|
|
{
|
|
//checks if it has a chainsaw
|
|
Chainsaw cs = bot.GetHeldEntity() as Chainsaw;
|
|
if (cs != null)
|
|
{
|
|
//Turn on chainsaw
|
|
cs.SetFlag(Chainsaw.Flags.On, true);
|
|
cs.SendNetworkUpdateImmediate();
|
|
}
|
|
});
|
|
}
|
|
|
|
void ParachuteSuiside(BasePlayer bot)
|
|
{
|
|
//Destroys parachute after 20 secs incase bot gets stuck in trees/buildings
|
|
timer.Once(ChuteSider, () =>
|
|
{
|
|
bool Destroyed = false;
|
|
foreach (var child in bot.children.Where(child => child.name.Contains("parachute")))
|
|
{
|
|
child.SetParent(null);
|
|
child.Kill();
|
|
Destroyed = true;
|
|
break;
|
|
}
|
|
if (Destroyed)
|
|
{
|
|
Effect.server.Run("assets/bundled/prefabs/fx/player/groundfall.prefab", bot.transform.position);
|
|
}
|
|
});
|
|
}
|
|
|
|
//Do attack damage and sfx slightly delayed to match with time swing takes to happen
|
|
void BotMeleeAttack(NPCPlayer bot, BasePlayer AttackPlayer, BaseEntity weapon, Rust.DamageType damagetype, float Damage, string sfx = "assets/bundled/prefabs/fx/headshot.prefab", int HeadshotPercentage = 10)
|
|
{
|
|
if (bot == null || AttackPlayer == null || weapon == null)
|
|
return;
|
|
|
|
//Create a delay for the animation
|
|
float delay = 0.2f;
|
|
try { delay = (weapon as BaseMelee).aiStrikeDelay; } catch { try { delay = (weapon as AttackEntity).animationDelay; } catch { } }
|
|
timer.Once(delay, () =>
|
|
{
|
|
float range = 0.8f;
|
|
try { range = (weapon as BaseMelee).maxDistance; } catch { try { range = (weapon as AttackEntity).effectiveRange; } catch { } }
|
|
//Check weapons reach.
|
|
if (Vector3.Distance(bot.transform.position, AttackPlayer.transform.position) < range)
|
|
{
|
|
//Apply damage and play SFX
|
|
//AttackPlayer.Hurt(Damage, damagetype, bot, true);
|
|
if (UnityEngine.Random.Range(1, 101) <= HeadshotPercentage)
|
|
Effect.server.Run(sfx, AttackPlayer.transform.position);
|
|
}
|
|
});
|
|
}
|
|
|
|
//Blocks shooting when under attack range on setup bots except heavy and junkpile for some reason.
|
|
private object OnNpcTarget(NPCPlayer npc, BaseEntity entity)
|
|
{
|
|
if (entity == null || npc == null) return null;
|
|
if (NPC_Bots.ContainsKey(npc.userID))
|
|
{
|
|
if (NPC_Spawners.ContainsKey(NPC_Bots[npc.userID]))
|
|
{
|
|
if (Vector3.Distance(npc.transform.position, entity.transform.position) >= NPC_Spawners[NPC_Bots[npc.userID]].AttackRange + 10)
|
|
{
|
|
return true;
|
|
}
|
|
//Cast target to baseplayer to check some conditions
|
|
BasePlayer TargetedPlayer = entity.ToPlayer();
|
|
if (TargetedPlayer == null)
|
|
{
|
|
return null;
|
|
}
|
|
//Block targeting of non-hostile players from peacekeeper bots.
|
|
if (NPC_Spawners[NPC_Bots[npc.userID]].peacekeeper && !TargetedPlayer.IsHostile())
|
|
{
|
|
return true;
|
|
}
|
|
//Protects sleepers
|
|
if (SleepProtect && TargetedPlayer.IsSleeping())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
//Hook to disable radio
|
|
private object OnNpcRadioChatter(ScientistNPC npc)
|
|
{
|
|
//Check if a custom bot
|
|
if (NPC_Bots.ContainsKey(npc.userID))
|
|
{
|
|
//check if has setting
|
|
if (NPC_Spawners.ContainsKey(NPC_Bots[npc.userID]))
|
|
{
|
|
//check radio status and disable radio if set
|
|
if (NPC_Spawners[NPC_Bots[npc.userID]].radiooff)
|
|
{
|
|
//no chatter
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
//Normal bahavour
|
|
return null;
|
|
}
|
|
|
|
//Fix for bradly going nuts on NPCs
|
|
private object CanBradleyApcTarget(BradleyAPC apc, BaseEntity baseentity)
|
|
{
|
|
if (baseentity == null) return null;
|
|
BasePlayer player = baseentity.ToPlayer();
|
|
if (apc == null && player == null) return null;
|
|
if (NPC_Bots.ContainsKey(player.userID))
|
|
{
|
|
if (NPC_Spawners.ContainsKey(NPC_Bots[player.userID]))
|
|
{
|
|
//Checks if doesnt have flag to allow NPC to attack
|
|
if (!NPC_Spawners[NPC_Bots[player.userID]].apcattack) return false;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
//Fix for NPCs with rocket launchers just firing at there feet.
|
|
private void EmulatedFire(BaseProjectile _ProectileWeapon, NPCPlayer player, BasePlayer targetplayer)
|
|
{
|
|
if (_ProectileWeapon == null || _ProectileWeapon.HasAttackCooldown()) return;
|
|
//Get distance from bot to player to work out rocket arch
|
|
float AimDistance = Vector3.Distance(player.transform.position, targetplayer.transform.position);
|
|
//Get direction of launcher
|
|
Vector3 vector3 = _ProectileWeapon.MuzzlePoint.transform.forward;
|
|
//Get height of launcher
|
|
Vector3 position = _ProectileWeapon.MuzzlePoint.transform.position + (Vector3.up * 1.6f);
|
|
//Create a new rocket
|
|
BaseEntity entity = null;
|
|
switch (_ProectileWeapon.ShortPrefabName)
|
|
{
|
|
case "rocket_launcher.entity":
|
|
entity = GameManager.server.CreateEntity("assets/prefabs/ammo/rocket/rocket_basic.prefab", position, player.eyes.GetLookRotation());
|
|
break;
|
|
|
|
case "mgl.entity":
|
|
entity = GameManager.server.CreateEntity("assets/prefabs/ammo/40mmgrenade/40mm_grenade_he.prefab", position, player.eyes.GetLookRotation());
|
|
AimDistance += 5f;
|
|
break;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
if (entity == null) return;
|
|
//Get rockets settings
|
|
var proj = entity.GetComponent<ServerProjectile>();
|
|
if (proj == null) return;
|
|
//Set creator of rocket to bot
|
|
entity.creatorEntity = player;
|
|
//Set fly angle
|
|
proj.InitializeVelocity(Quaternion.Euler(vector3) * entity.transform.forward * AimDistance);
|
|
//Adjust explosive delay
|
|
TimedExplosive rocketExplosion = entity.GetComponent<TimedExplosive>();
|
|
if (rocketExplosion != null)
|
|
{
|
|
rocketExplosion.timerAmountMin = 1;
|
|
rocketExplosion.timerAmountMax = 15;
|
|
}
|
|
//Make shot happen.
|
|
entity.Spawn();
|
|
}
|
|
|
|
public void FireWaterGun(BasePlayer ownerPlayer, BaseEntity wep)
|
|
{
|
|
//Get as item
|
|
global::Item item = wep.GetItem();
|
|
if (item == null)
|
|
{
|
|
return;
|
|
}
|
|
//Creates water
|
|
ItemDefinition itemDefinition = ItemManager.FindItemDefinition("water");
|
|
//Get direction
|
|
Ray ray = ownerPlayer.eyes.BodyRay();
|
|
//Draw water line
|
|
Debug.DrawLine(ray.origin, ray.origin + ray.direction * 10f, Color.blue, 1f);
|
|
//Do damage
|
|
RaycastHit hitInfo;
|
|
Physics.Raycast(ray, out hitInfo, 10f, 1218652417);
|
|
List<Rust.DamageTypeEntry> damage = new List<Rust.DamageTypeEntry>();
|
|
WaterBall.DoSplash(hitInfo.point, 2f, itemDefinition, 10);
|
|
DamageUtil.RadiusDamage(ownerPlayer, wep.LookupPrefab(), hitInfo.point, 0.15f, 0.15f, damage, 131072, true);
|
|
}
|
|
|
|
//Make NPCs able to throw
|
|
public void ServerThrow(Vector3 targetPosition, ThrownWeapon wep, NPCPlayer player)
|
|
{
|
|
if (wep == null || wep.HasAttackCooldown()) return;
|
|
//Get aim direction
|
|
Vector3 position = player.eyes.position;
|
|
Vector3 vector3 = player.eyes.BodyForward();
|
|
float AimDistance = 1f;
|
|
//Trigger throw server code
|
|
wep.SignalBroadcast(BaseEntity.Signal.Throw, string.Empty);
|
|
//Create the nade
|
|
BaseEntity entity = GameManager.server.CreateEntity(wep.prefabToThrow.resourcePath, position, Quaternion.LookRotation(wep.overrideAngle == Vector3.zero ? -vector3 : wep.overrideAngle));
|
|
if ((UnityEngine.Object)entity == (UnityEngine.Object)null)
|
|
return;
|
|
//Set owner
|
|
entity.creatorEntity = (BaseEntity)player;
|
|
//Get throw arch
|
|
Vector3 aimDir = vector3 + Quaternion.AngleAxis(10f, Vector3.right) * Vector3.up;
|
|
float f = 6f;
|
|
if (float.IsNaN(f))
|
|
{
|
|
aimDir = vector3 + Quaternion.AngleAxis(20f, Vector3.right) * Vector3.up;
|
|
f = 6f;
|
|
if (float.IsNaN(f))
|
|
f = 5f;
|
|
}
|
|
entity.SetVelocity(aimDir * f * AimDistance);
|
|
if ((double)wep.tumbleVelocity > 0.0)
|
|
entity.SetAngularVelocity(new Vector3(UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f)) * wep.tumbleVelocity);
|
|
//Make sure doesnt dud
|
|
DudTimedExplosive dud = entity.GetComponent<DudTimedExplosive>();
|
|
if (dud != null)
|
|
dud.dudChance = 0f;
|
|
entity.Spawn();
|
|
}
|
|
|
|
public static bool IsFemale(ulong userID)
|
|
{
|
|
//Save current random state
|
|
UnityEngine.Random.State state = UnityEngine.Random.state;
|
|
//initilise in a known state so we already know the outcome of the random generator
|
|
//Feed userid as the seed
|
|
UnityEngine.Random.InitState((int)((uint)4332 + userID));
|
|
//Determin gender
|
|
bool Gender = (UnityEngine.Random.Range(0f, 1f) > 0.5f);
|
|
//Reset state back to a random unknown state we saved.
|
|
UnityEngine.Random.state = state;
|
|
return Gender;
|
|
}
|
|
|
|
//Scans around the bot for seats, Put the bot in the closest seat to it.
|
|
public void MountBot(BasePlayer bot)
|
|
{
|
|
List<BaseMountable> Seats = new List<BaseMountable>();
|
|
Vis.Entities<BaseMountable>(bot.transform.position, MountScan, Seats);
|
|
BaseMountable closest_seat = null;
|
|
//Finds closest seat thats not already mounted
|
|
foreach (BaseMountable seat in Seats)
|
|
{
|
|
if (seat.HasFlag(BaseEntity.Flags.Busy)) continue;
|
|
if (closest_seat == null) closest_seat = seat;
|
|
if (Vector3.Distance(bot.transform.position, seat.transform.position) <= Vector3.Distance(bot.transform.position, closest_seat.transform.position))
|
|
closest_seat = seat;
|
|
}
|
|
//Trys to mount seat
|
|
if (closest_seat != null)
|
|
{
|
|
closest_seat.GetComponent<BaseMountable>().AttemptMount(bot);
|
|
closest_seat.SendNetworkUpdateImmediate();
|
|
bot.SendNetworkUpdateImmediate();
|
|
if (plugin.DebugInfo) plugin.Puts(bot.displayName + " Forced into Seat @ " + closest_seat.transform.position.ToString());
|
|
}
|
|
}
|
|
|
|
|
|
private bool IsKit(string kit)
|
|
{
|
|
//Call kit plugin to check if its valid kit
|
|
var success = Kits?.Call("isKit", kit);
|
|
if (success == null || !(success is bool))
|
|
{
|
|
return false;
|
|
}
|
|
return (bool)success;
|
|
}
|
|
|
|
void BotSkin(NPCPlayer bot, string Skin, bool replacement)
|
|
{
|
|
//Remove Default kit
|
|
ItemManager.DoRemoves();
|
|
foreach (Item i in bot.inventory.AllItems())
|
|
{
|
|
i.Remove();
|
|
}
|
|
ItemManager.DoRemoves();
|
|
|
|
//Try apply the kit
|
|
Kits?.Call("GiveKit", bot, Skin);
|
|
//Trys to equip stuff after a delay for kits plugin to of ran
|
|
Item projectileItem = null;
|
|
//Find first gun
|
|
timer.Once(0.5f, () =>
|
|
{
|
|
foreach (var item in bot.inventory.containerBelt.itemList.ToList())
|
|
{
|
|
if (item.GetHeldEntity() is BaseProjectile)
|
|
{
|
|
projectileItem = item;
|
|
break;
|
|
}
|
|
//Move medial items out of hot bar
|
|
if (item.GetHeldEntity() is MedicalTool)
|
|
{
|
|
if (bot.inventory.containerBelt.GetSlot(5) != null)
|
|
{
|
|
bot.inventory.containerBelt.GetSlot(5).MoveToContainer(bot.inventory.containerMain);
|
|
}
|
|
item.MoveToContainer(bot.inventory.containerBelt, 5);
|
|
continue;
|
|
}
|
|
|
|
}
|
|
if (projectileItem != null)
|
|
{
|
|
//pull out gun.
|
|
bot.UpdateActiveItem(projectileItem.uid);
|
|
bot.inventory.UpdatedVisibleHolsteredItems();
|
|
}
|
|
else
|
|
{
|
|
//Find a melee weapon in the belt
|
|
foreach (var item in bot.inventory.containerBelt.itemList.ToList())
|
|
{
|
|
//Move medial items out of hot bar
|
|
if (item.GetHeldEntity() is MedicalTool)
|
|
{
|
|
if (bot.inventory.containerBelt.GetSlot(5) != null)
|
|
{
|
|
bot.inventory.containerBelt.GetSlot(5).MoveToContainer(bot.inventory.containerMain);
|
|
}
|
|
item.MoveToContainer(bot.inventory.containerBelt, 5);
|
|
continue;
|
|
}
|
|
if (item.GetHeldEntity() is BaseMelee)
|
|
{
|
|
projectileItem = item;
|
|
break;
|
|
}
|
|
}
|
|
//Try pull out active weapon
|
|
try
|
|
{
|
|
bot.UpdateActiveItem(projectileItem.uid);
|
|
bot.inventory.UpdatedVisibleHolsteredItems();
|
|
}
|
|
catch { }
|
|
}
|
|
//Try get gun ready.
|
|
try
|
|
{
|
|
timer.Once(1f, () =>
|
|
{
|
|
(bot as NPCPlayer).AttemptReload();
|
|
});
|
|
}
|
|
catch { }
|
|
//Only do this if bot wants items replaced on the corpse
|
|
if (replacement)
|
|
{
|
|
timer.Once(0.5f, () =>
|
|
{
|
|
//Update bot item list for corpse use
|
|
List<Botsinfo> items = new List<Botsinfo>();
|
|
foreach (Item item in bot.inventory.containerBelt.itemList)
|
|
{
|
|
if (item.info != null)
|
|
{
|
|
Botsinfo bi = ProcessKitItem(item);
|
|
if (!items.Contains(bi))
|
|
items.Add(bi);
|
|
}
|
|
|
|
}
|
|
foreach (Item item in bot.inventory.containerMain.itemList)
|
|
{
|
|
if (item.info != null)
|
|
{
|
|
Botsinfo bi = ProcessKitItem(item);
|
|
if (!items.Contains(bi))
|
|
items.Add(bi);
|
|
}
|
|
}
|
|
foreach (Item item in bot.inventory.containerWear.itemList)
|
|
{
|
|
if (item.info != null)
|
|
{
|
|
Botsinfo bi = ProcessKitItem(item);
|
|
if (!items.Contains(bi))
|
|
items.Add(bi);
|
|
}
|
|
}
|
|
if (NPC_Items.ContainsKey(bot.userID))
|
|
{
|
|
NPC_Items[bot.userID] = items;
|
|
}
|
|
else
|
|
{
|
|
NPC_Items.Add(bot.userID, items);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|