//reference System.Core.dll using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using MCGalaxy.Bots; using MCGalaxy.Events.PlayerEvents; using MCGalaxy.Maths; using MCGalaxy.Network; using MCGalaxy.Tasks; using BlockID = System.UInt16; namespace MCGalaxy { public sealed class MobAI : Plugin { public override string name { get { return "MobAI"; } } public override string MCGalaxy_Version { get { return "1.9.3.1"; } } public override string creator { get { return "Venk"; } } BotInstruction drop; BotInstruction hostile; BotInstruction roam; BotInstruction run; BotInstruction smart; BotInstruction smarthunt; BotInstruction spleef; BotInstruction tnt; public override void Load(bool startup) { // Initialize bot AIs drop = new DropInstruction(); hostile = new HostileInstruction(); roam = new RoamInstruction(); run = new RunInstruction(); smart = new SmartInstruction(); smarthunt = new SmartHuntInstruction(); spleef = new SpleefInstruction(); tnt = new TNTInstruction(); // Create bot AIs BotInstruction.Instructions.Add(drop); BotInstruction.Instructions.Add(hostile); BotInstruction.Instructions.Add(roam); BotInstruction.Instructions.Add(run); BotInstruction.Instructions.Add(smart); BotInstruction.Instructions.Add(smarthunt); BotInstruction.Instructions.Add(spleef); BotInstruction.Instructions.Add(tnt); } public override void Unload(bool shutdown) { // Delete bot AIs BotInstruction.Instructions.Remove(drop); BotInstruction.Instructions.Remove(hostile); BotInstruction.Instructions.Remove(roam); BotInstruction.Instructions.Remove(run); BotInstruction.Instructions.Remove(smart); BotInstruction.Instructions.Remove(smarthunt); BotInstruction.Instructions.Remove(spleef); BotInstruction.Instructions.Remove(tnt); } /// /// Determines the axis to check for blocks around the bot with. /// /// North = Z -> 0 /// East = 0 -> X /// South = 0 -> Z /// West = X -> 0 /// public static string axis = "none"; /// /// Calculates cardinal direction based on bot's yaw value. /// /// /// public static string CalculateCardinal(PlayerBot bot) { if (Orientation.PackedToDegrees(bot.Rot.RotY) >= 0 && Orientation.PackedToDegrees(bot.Rot.RotY) < 45) return "North"; if (Orientation.PackedToDegrees(bot.Rot.RotY) >= 45 && Orientation.PackedToDegrees(bot.Rot.RotY) < 90) return "Northeast"; if (Orientation.PackedToDegrees(bot.Rot.RotY) >= 90 && Orientation.PackedToDegrees(bot.Rot.RotY) < 135) return "East"; if (Orientation.PackedToDegrees(bot.Rot.RotY) >= 135 && Orientation.PackedToDegrees(bot.Rot.RotY) < 180) return "Southeast"; if (Orientation.PackedToDegrees(bot.Rot.RotY) >= 180 && Orientation.PackedToDegrees(bot.Rot.RotY) < 225) return "South"; if (Orientation.PackedToDegrees(bot.Rot.RotY) >= 225 && Orientation.PackedToDegrees(bot.Rot.RotY) < 270) return "Southwest"; if (Orientation.PackedToDegrees(bot.Rot.RotY) >= 270 && Orientation.PackedToDegrees(bot.Rot.RotY) < 315) return "West"; if (Orientation.PackedToDegrees(bot.Rot.RotY) >= 315 && Orientation.PackedToDegrees(bot.Rot.RotY) < 361) return "Northwest"; return ""; } /// /// Since bots travel at ~1.4x (40%) faster speeds on diagonals, we need to make the bot's speed slower. /// /// public static void SetDirectionalSpeeds(PlayerBot bot) { if (CalculateCardinal(bot) == "Northeast" || CalculateCardinal(bot) == "Northwest" || CalculateCardinal(bot) == "Southeast" || CalculateCardinal(bot) == "Southwest") { bot.movementSpeed = (int)Math.Round(3m * 60 / 100m); // 40% slower on diagonals } else { if (CalculateCardinal(bot) == "North" || CalculateCardinal(bot) == "South") axis = "Z"; if (CalculateCardinal(bot) == "East" || CalculateCardinal(bot) == "West") axis = "X"; bot.movementSpeed = (int)Math.Round(3m * 100 / 100m); // Regular speed on non-diagonals } } /// /// The player closest to the bot. /// /// /// /// internal static Player ClosestPlayer(PlayerBot bot, int search) { int maxDist = search * 32; Player[] players = PlayerInfo.Online.Items; Player closest = null; foreach (Player p in players) { if (p.level != bot.level || p.invincible || p.hidden) continue; int dx = p.Pos.X - bot.Pos.X, dy = p.Pos.Y - bot.Pos.Y, dz = p.Pos.Z - bot.Pos.Z; int playerDist = Math.Abs(dx) + Math.Abs(dy) + Math.Abs(dz); if (playerDist >= maxDist) continue; closest = p; maxDist = playerDist; } return closest; } /// /// The bot closest to the bot. /// /// /// /// internal static PlayerBot ClosestBot(PlayerBot bot, int search) { int maxDist = search * 32; Player[] players = PlayerInfo.Online.Items; PlayerBot closest = null; PlayerBot[] bots = bot.level.Bots.Items; foreach (PlayerBot b in bots) { if (b == bot) continue; int dx = b.Pos.X - bot.Pos.X, dy = b.Pos.Y - bot.Pos.Y, dz = b.Pos.Z - bot.Pos.Z; int botDist = Math.Abs(dx) + Math.Abs(dy) + Math.Abs(dz); if (botDist >= maxDist) continue; closest = b; maxDist = botDist; } return closest; } /// /// The zombie closest to the bot. /// /// /// /// internal static PlayerBot ClosestZombie(PlayerBot bot, int search) { int maxDist = search * 32; Player[] players = PlayerInfo.Online.Items; PlayerBot closest = null; PlayerBot[] bots = bot.level.Bots.Items; foreach (PlayerBot b in bots) { if (b == bot) continue; if (b.Model != "zombie") continue; int dx = b.Pos.X - bot.Pos.X, dy = b.Pos.Y - bot.Pos.Y, dz = b.Pos.Z - bot.Pos.Z; int botDist = Math.Abs(dx) + Math.Abs(dy) + Math.Abs(dz); if (botDist >= maxDist) continue; closest = b; maxDist = botDist; } return closest; } /// /// Face bot towards target position. /// /// public static void FaceTowards(PlayerBot bot) { int dstHeight = ModelInfo.CalcEyeHeight(bot); int dx = bot.TargetPos.X - bot.Pos.X; int dy = (bot.TargetPos.Y + 16) - (bot.Pos.Y + dstHeight); int dz = bot.TargetPos.Z - bot.Pos.Z; Vec3F32 dir = new Vec3F32(dx, dy, dz); dir = Vec3F32.Normalise(dir); Orientation rot = bot.Rot; DirUtils.GetYawPitch(dir, out rot.RotY, out rot.HeadX); bot.Rot = rot; } /// /// Whether or not the bot is "inside" the player. Note the hitbox is 0.875 so ping will affect how far/close players are from the bot. /// /// /// /// /// public static bool InRange(Player a, PlayerBot b, int dist) { int dx = Math.Abs(a.Pos.X - b.Pos.X); int dy = Math.Abs(a.Pos.Y - b.Pos.Y); int dz = Math.Abs(a.Pos.Z - b.Pos.Z); return dx <= dist && dy <= dist && dz <= dist; } } public sealed class Metadata { public int waitTime; public int walkTime; public int lookTime; public int bowCooldown; public int explodeTime; public int jumpHeight; public int search; public int randomTick; public int tickDelay; public int velocityX; public int velocityY; public int velocityZ; public bool flanking; public Player chasing; } class Location { public int X; public int Z; public int F; public int G; public int H; public Location Parent; } #region Drop AI /* Current AI behaviour: - Spin - Fall until block hits the ground */ sealed class DropInstruction : BotInstruction { public DropInstruction() { Name = "drop"; } public override bool Execute(PlayerBot bot, InstructionData data) { if (bot.Pos.BlockY < 0) PlayerBot.Remove(bot); BlockID block = bot.level.GetBlock((ushort)bot.Pos.BlockX, ((ushort)(bot.Pos.BlockY - 2)), (ushort)bot.Pos.BlockZ); //Console.WriteLine(block + " block"); if (block != Block.Air) return true; Metadata meta = (Metadata)data.Metadata; //Console.WriteLine(bot.Pos.X + " x " + bot.Pos.Y + " y" + bot.Pos.Z + " z" ); bot.Pos = new Position(bot.Pos.X, (bot.Pos.Y - 16), bot.Pos.Z); return true; } public override InstructionData Parse(string[] args) { InstructionData data = default(InstructionData); data.Metadata = new Metadata(); return data; } public void Output(Player p, string[] args, StreamWriter w) { if (args.Length > 3) { w.WriteLine(Name + " " + ushort.Parse(args[3])); } else { w.WriteLine(Name); } } public override string[] Help { get { return help; } } static string[] help = new string[] { "%T/BotAI add [name] drop", "%HCauses the bot to fall.", }; } #endregion #region Hostile AI /* Current AI behaviour: - Chase player if within 12 block range - Hit player if too close - Assign movement speed based on mob model - Explode if mob is a creeper - 50% chance to stand still (moving when 0-2, still when 3-5) - If not moving, wait for waitTime duration before executing next task - Choose random coord within 8x8 block radius of player and try to go to it - Do action for walkTime duration */ sealed class HostileInstruction : BotInstruction { public HostileInstruction() { Name = "hostile"; } static bool MoveTowards(PlayerBot bot, Player p, Metadata meta) { if (p == null) return false; int dx = p.Pos.X - bot.Pos.X, dy = p.Pos.Y - bot.Pos.Y, dz = p.Pos.Z - bot.Pos.Z; bot.TargetPos = p.Pos; Vec3F32 dir = new Vec3F32(dx, dy, dz); if (dir.Length > 0) dir = Vec3F32.Normalise(dir); Orientation rot = bot.Rot; DirUtils.GetYawPitch(dir, out rot.RotY, out rot.HeadX); // Apply rotation before movement/speed logic so the mob faces where it is actually going. bot.Rot = rot; MobAI.SetDirectionalSpeeds(bot); bot.movement = true; dx = Math.Abs(dx); dy = Math.Abs(dy); dz = Math.Abs(dz); if (bot.Model == "creeper") { if (dx < (3 * 32) && dz < (3 * 32)) { if (meta.explodeTime == 0) { meta.explodeTime = 10; } } else { meta.explodeTime = 0; } } else { // Check to see if positions collide AABB playerBB = p.ModelBB.OffsetPosition(p.Pos); AABB botBB = bot.ModelBB.OffsetPosition(bot.Pos); int dist = (int)(0.875f * 32); bool inRange = ((long)dx * dx + (long)dz * dz <= dist * dist) && (botBB.Min.Y <= playerBB.Max.Y && playerBB.Min.Y <= botBB.Max.Y); if (inRange) HitPlayer(bot, p, rot); } return dx <= 8 && dy <= 16 && dz <= 8; } public static void HitPlayer(PlayerBot bot, Player p, Orientation rot) { // Send player backwards if hit // Code "borrowed" from PvP plugin // If we are very close to a player, switch from trying to look // at them to just facing the opposite direction to them rot.RotY = (byte)(p.Rot.RotY + 128); bot.Rot = rot; int srcHeight = ModelInfo.CalcEyeHeight(bot); int dstHeight = ModelInfo.CalcEyeHeight(p); int dx2 = bot.Pos.X - p.Pos.X, dy2 = (bot.Pos.Y + srcHeight) - (p.Pos.Y + dstHeight), dz2 = bot.Pos.Z - p.Pos.Z; Vec3F32 dir2 = new Vec3F32(dx2, dy2, dz2); if (dir2.Length > 0) dir2 = Vec3F32.Normalise(dir2); float mult = 1 / ModelInfo.GetRawScale(p.Model); float plScale = ModelInfo.GetRawScale(p.Model); float VelocityY = 1.0117f * mult; if (dir2.Length <= 0) VelocityY = 0; if (p.Supports(CpeExt.VelocityControl)) { // Intensity of force is in part determined by model scale p.Send(Packet.VelocityControl((-dir2.X * mult) * 0.57f, VelocityY, (-dir2.Z * mult) * 0.57f, 0, 1, 0)); } int damage = 0; if (bot.Model == "bee") damage = 2; if (bot.Model == "blaze") damage = 4; if (bot.Model == "creeper") damage = 23; // 22.5 but either way, still kills the player if (bot.Model == "enderman") damage = 5; // 4.5 if (bot.Model == "panda") damage = 4; if (bot.Model == "skeleton") damage = 2; // 3-5 damage when shot with bow if (bot.Model == "spider") damage = 2; if (bot.Model == "witherskeleton") damage = 5; if (bot.Model == "wither") damage = 15; // 34 with explosion if (bot.Model == "zombie") damage = 3; // 2.5 // Update player's health p.Extras["SURVIVAL_HEALTH"] = p.Extras.GetInt("SURVIVAL_HEALTH") - damage; if (p.Extras.GetInt("SURVIVAL_HEALTH") <= 0) p.HandleDeath(Block.Orange); } private readonly Random _random = new Random(); public int RandomNumber(int min, int max) { return _random.Next(min, max); } public void DoStuff(PlayerBot bot, Metadata meta) { int stillChance = RandomNumber(0, 5); // Chance for the NPC to stand still int walkTime = RandomNumber(4, 8) * 5; // Time in milliseconds to execute a task int waitTime = RandomNumber(2, 5) * 5; // Time in milliseconds to wait before executing the next task int dx = RandomNumber(bot.Pos.X - (8 * 32), bot.Pos.X + (8 * 32)); // Random X location on the map within a 8x8 radius of the bot for the it to walk towards. int dz = RandomNumber(bot.Pos.Z - (8 * 32), bot.Pos.Z + (8 * 32)); // Random Z location on the map within a 8x8 radius of the bot for the it to walk towards. if (stillChance > 2) { meta.walkTime = walkTime; } else { Coords target; target.X = dx; target.Y = bot.Pos.Y; target.Z = dz; target.RotX = bot.Rot.RotX; target.RotY = bot.Rot.RotY; bot.TargetPos = new Position(target.X, target.Y, target.Z); bot.movement = true; if (bot.Pos.BlockX == bot.TargetPos.BlockX && bot.Pos.BlockZ == bot.TargetPos.BlockZ) { bot.SetYawPitch(target.RotX, target.RotY); bot.movement = false; } MobAI.FaceTowards(bot); meta.walkTime = walkTime; bot.movement = false; meta.waitTime = waitTime; } } public override bool Execute(PlayerBot bot, InstructionData data) { Metadata meta = (Metadata)data.Metadata; if (bot.Model == "skeleton" || bot.Model == "creeper") bot.movementSpeed = (int)Math.Round(3m * (short)97 / 100m); if (bot.Model == "zombie") bot.movementSpeed = (int)Math.Round(3m * (short)94 / 100m); if (bot.movementSpeed == 0) bot.movementSpeed = 1; int search = 16; // If specified, use user specified search distance instead of default if (meta.search != 16 && meta.search > 0) search = meta.search; else { if (bot.Model == "zombie") search = 35; } Player closest = MobAI.ClosestPlayer(bot, search); if (closest == null) { if (bot.Model == "creeper") { meta.explodeTime = 0; } if (meta.walkTime > 0) { meta.walkTime--; bot.movement = true; return true; } if (meta.waitTime > 0) { meta.waitTime--; return true; } DoStuff(bot, meta); bot.movement = false; bot.NextInstruction(); } else { if (bot.Model == "creeper") { if (meta.explodeTime > 0) { // While primed, stop sliding sideways/backwards and keep facing the target. bot.TargetPos = bot.Pos; bot.movement = false; MobAI.FaceTowards(bot); meta.explodeTime--; if (meta.explodeTime <= 1) { if (closest.level.physics > 1 && closest.level.physics != 5) closest.level.MakeExplosion((ushort)(bot.Pos.X / 32), (ushort)(bot.Pos.Y / 32), (ushort)(bot.Pos.Z / 32), 0); Command.Find("Effect").Use(closest, "explosion " + (bot.Pos.X / 32) + " " + (bot.Pos.Y / 32) + " " + (bot.Pos.Z / 32) + " 0 0 0 true"); int distanceX = closest.Pos.X - bot.Pos.X, distanceY = closest.Pos.Y - bot.Pos.Y, distanceZ = closest.Pos.Z - bot.Pos.Z; int distance = (distanceX + distanceZ) / 32; // Do damage to the player if player is within a 3 block radius if (distance < 3) { closest.Extras["SURVIVAL_HEALTH"] = closest.Extras.GetInt("SURVIVAL_HEALTH") - 23; if (closest.Extras.GetInt("SURVIVAL_HEALTH") <= 0) { closest.HandleDeath(Block.Orange); } else { Orientation rot = bot.Rot; HitPlayer(bot, closest, rot); } } meta.explodeTime = 0; PlayerBot.Remove(bot); return true; } return true; } } if (bot.Model == "skeleton") { if (meta.bowCooldown >= 1) { meta.bowCooldown--; } if (meta.bowCooldown == 0) { meta.bowCooldown = 33; Bow.Enable(bot, closest); } bot.movement = true; } } bool overlapsPlayer = MoveTowards(bot, closest, meta); if (overlapsPlayer && closest != null) { bot.NextInstruction(); return false; } return true; } public override InstructionData Parse(string[] args) { InstructionData data = default(InstructionData); data.Metadata = new Metadata(); Metadata meta = (Metadata)data.Metadata; if (args.Length > 1) { meta.search = int.Parse(args[1]); } return data; } public void Output(Player p, string[] args, StreamWriter w) { if (args.Length > 3) { w.WriteLine(Name + " " + ushort.Parse(args[3])); } else { w.WriteLine(Name); } } struct Coords { public int X, Y, Z; public byte RotX, RotY; } public override string[] Help { get { return help; } } static string[] help = new string[] { "%T/BotAI add [name] hostile", "%HCauses the bot behave as a hostile mob.", }; } #endregion #region Roam AI /* Current AI behaviour: - 50% chance to stand still (moving when 0-2, still when 3-5) - If not moving, wait for waitTime duration before executing next task - Choose random coord within 8x8 block radius of player and try to go to it - Do action for walkTime duration */ sealed class RoamInstruction : BotInstruction { public RoamInstruction() { Name = "roam"; } private readonly Random _random = new Random(); public int RandomNumber(int min, int max) { return _random.Next(min, max); } public void DoStuff(PlayerBot bot, Metadata meta) { int stillChance = RandomNumber(0, 5); // Chance for the NPC to stand still int walkTime = RandomNumber(4, 8) * 5; // Time in milliseconds to execute a task int waitTime = RandomNumber(2, 4) * 5; // Time in milliseconds to wait before executing the next task int lookChance = RandomNumber(0, 2); // Chance for the NPC to look at the player int lookTime = RandomNumber(2, 5) * 5; // Time in milliseconds to look at the player for int eatGrassChance = RandomNumber(0, 100); // Chance for the NPC to eat grass int dx = RandomNumber(bot.Pos.X - (8 * 32), bot.Pos.X + (8 * 32)); // Random X location on the map within a 8x8 radius of the bot for the it to walk towards. int dz = RandomNumber(bot.Pos.Z - (8 * 32), bot.Pos.Z + (8 * 32)); // Random Z location on the map within a 8x8 radius of the bot for the it to walk towards. if (eatGrassChance <= 3) { // Only eat grass if the block underneath the NPC is grass BlockID floor = bot.level.GetBlock((ushort)(bot.Pos.X / 32), (ushort)((bot.Pos.Y / 32) - 2), (ushort)(bot.Pos.Z / 32)); if (floor == Block.Grass) { bot.level.UpdateBlock(Player.Console, (ushort)(bot.Pos.X / 32), (ushort)((bot.Pos.Y / 32) - 2), (ushort)(bot.Pos.Z / 32), Block.Dirt); if (bot.Model.CaselessEq("sheep_nofur")) { bot.UpdateModel("sheep"); BotsFile.Save(bot.level); } } } if (stillChance > 2) { meta.walkTime = walkTime; Player p = MobAI.ClosestPlayer(bot, 5); if (p != null && lookChance == 1) { int srcHeight = ModelInfo.CalcEyeHeight(p); int dstHeight = ModelInfo.CalcEyeHeight(bot); // Look at player int lx = p.Pos.X - bot.Pos.X, ly = (p.Pos.Y + srcHeight) - (bot.Pos.Y + dstHeight), lz = p.Pos.Z - bot.Pos.Z; Vec3F32 dir = new Vec3F32(lx, ly, lz); dir = Vec3F32.Normalise(dir); Orientation rot = bot.Rot; DirUtils.GetYawPitch(dir, out rot.RotY, out rot.HeadX); bot.Rot = rot; meta.lookTime = lookTime; meta.chasing = p; //p.Message("looking at " + p.truename + " for " + lookTime); } else { Coords target; target.X = dx; target.Y = bot.Pos.Y; target.Z = dz; target.RotX = bot.Rot.RotX; target.RotY = bot.Rot.RotY; bot.TargetPos = new Position(target.X, target.Y, target.Z); bot.movement = true; if (bot.Pos.BlockX == bot.TargetPos.BlockX && bot.Pos.BlockZ == bot.TargetPos.BlockZ) { bot.SetYawPitch(target.RotX, bot.Rot.RotY); bot.movement = false; } MobAI.FaceTowards(bot); meta.walkTime = walkTime; bot.movement = false; meta.waitTime = waitTime; } } } public override bool Execute(PlayerBot bot, InstructionData data) { Metadata meta = (Metadata)data.Metadata; if (meta.walkTime > 0) { meta.walkTime--; bot.movement = true; return true; } if (meta.waitTime > 0) { Player p = MobAI.ClosestPlayer(bot, 5); if (p != null) { int lookChance = RandomNumber(0, 2); // Chance for the NPC to look at the player int lookTime = RandomNumber(2, 5) * 5; // Time in milliseconds to look at the player for if (lookChance == 1) { int srcHeight = ModelInfo.CalcEyeHeight(p); int dstHeight = ModelInfo.CalcEyeHeight(bot); // Look at player int lx = p.Pos.X - bot.Pos.X, ly = (p.Pos.Y + srcHeight) - (bot.Pos.Y + dstHeight), lz = p.Pos.Z - bot.Pos.Z; Vec3F32 dir = new Vec3F32(lx, ly, lz); dir = Vec3F32.Normalise(dir); Orientation rot = bot.Rot; DirUtils.GetYawPitch(dir, out rot.RotY, out rot.HeadX); bot.Rot = rot; meta.lookTime = lookTime; meta.chasing = p; //p.Message("interrupted, looking at " + p.truename + " for " + lookTime); } meta.waitTime = 0; return true; } meta.waitTime--; return true; } if (meta.lookTime > 0) { if (meta.chasing != null) { Player p = meta.chasing; int srcHeight = ModelInfo.CalcEyeHeight(p); int dstHeight = ModelInfo.CalcEyeHeight(bot); // Look at player int lx = p.Pos.X - bot.Pos.X, ly = (p.Pos.Y + srcHeight) - (bot.Pos.Y + dstHeight), lz = p.Pos.Z - bot.Pos.Z; Vec3F32 dir = new Vec3F32(lx, ly, lz); dir = Vec3F32.Normalise(dir); Orientation rot = bot.Rot; DirUtils.GetYawPitch(dir, out rot.RotY, out rot.HeadX); bot.Rot = rot; //p.Message("still looking at " + p.truename + " for " + meta.lookTime); } meta.lookTime--; if (meta.lookTime <= 0) { if (meta.chasing != null) //meta.chasing.Message("finished looking"); bot.NextInstruction(); } else return true; } DoStuff(bot, meta); bot.movement = false; bot.NextInstruction(); return true; } public override InstructionData Parse(string[] args) { InstructionData data = default(InstructionData); data.Metadata = new Metadata(); return data; } public void Output(Player p, string[] args, StreamWriter w) { if (args.Length > 3) { w.WriteLine(Name + " " + ushort.Parse(args[3])); } else { w.WriteLine(Name); } } struct Coords { public int X, Y, Z; public byte RotX, RotY; } public override string[] Help { get { return help; } } static string[] help = new string[] { "%T/BotAI add [name] roam", "%HCauses the bot behave freely.", }; } #endregion #region Run AI /* Current AI behaviour: - Chase player if within 10 block range */ sealed class RunInstruction : BotInstruction { public RunInstruction() { Name = "run"; } static int lastY = 0; static bool MoveTowards(PlayerBot bot, Player p, Metadata meta) { if (p == null) return false; //int dist = (int)(0.875 * 32); int dx = p.Pos.X - bot.Pos.X, dy = p.Pos.Y - bot.Pos.Y, dz = p.Pos.Z - bot.Pos.Z; MobAI.SetDirectionalSpeeds(bot); Vec3F32 pDir = DirUtils.GetDirVector(p.Rot.RotY, 0); bot.TargetPos.X = bot.Pos.X + (int)(pDir.X * 100); bot.TargetPos.Z = bot.Pos.Z + (int)(pDir.Z * 100); bot.movement = true; Vec3F32 dir = new Vec3F32(dx, dy, dz); dir = Vec3F32.Normalise(dir); Orientation rot = bot.Rot; DirUtils.GetYawPitch(dir, out rot.RotY, out rot.HeadX); dx = Math.Abs(dx); dy = Math.Abs(dy); dz = Math.Abs(dz); //if (InRange(p, bot, dist)) p.Message("%cInfect"); bot.Rot = rot; return dx <= 4 && dy <= 8 && dz <= 4; } private readonly Random _random = new Random(); public int RandomNumber(int min, int max) { return _random.Next(min, max); } public override bool Execute(PlayerBot bot, InstructionData data) { Metadata meta = (Metadata)data.Metadata; Player closest = MobAI.ClosestPlayer(bot, 20); if (closest == null) { bot.movement = false; bot.NextInstruction(); } bool overlapsPlayer = MoveTowards(bot, closest, meta); if (overlapsPlayer && closest != null) { bot.NextInstruction(); return false; } return true; } public override InstructionData Parse(string[] args) { InstructionData data = default(InstructionData); data.Metadata = new Metadata(); return data; } public void Output(Player p, string[] args, StreamWriter w) { if (args.Length > 3) { w.WriteLine(Name + " " + ushort.Parse(args[3])); } else { w.WriteLine(Name); } } struct Coords { public int X, Y, Z; public byte RotX, RotY; } public override string[] Help { get { return help; } } static string[] help = new string[] { "%T/BotAI add [name] run", "%HCauses the bot to try and spleef you.", }; } #endregion #region Smart AI /* Current AI behaviour: - Chase player if within 12 block range - Let player know if they are within the hitbox (simulative of New Blood's 0.875 hitbox) - Adjust speed to be ~40% slower when running on diagonals to simulate player speed (bot speed is 1.4 on diagonals since cardinally lacking) */ sealed class SmartInstruction : BotInstruction { public SmartInstruction() { Name = "smart"; } public static bool InRange(Player a, PlayerBot b, int dist) { int dx = Math.Abs(a.Pos.X - b.Pos.X); int dy = Math.Abs(a.Pos.Y - b.Pos.Y); int dz = Math.Abs(a.Pos.Z - b.Pos.Z); // Actually checking if positions collide AABB aliveBB = a.ModelBB.OffsetPosition(a.Pos); AABB killerBB = b.ModelBB.OffsetPosition(b.Pos); killerBB.Max.Y -= 8; // Adjust because zombie head is a bit lower bool inRange = ((long)dx * dx + (long)dz * dz <= dist * dist) && (killerBB.Min.Y <= aliveBB.Max.Y && aliveBB.Min.Y <= killerBB.Max.Y); return inRange; } static bool MoveTowards(PlayerBot bot, Player p, Metadata meta) { if (p == null) return false; int dist = (int)(0.875 * 32); int dx = p.Pos.X - bot.Pos.X, dy = p.Pos.Y - bot.Pos.Y, dz = p.Pos.Z - bot.Pos.Z; // Update target pos every x seconds to simulate ping // 10 ticks per second (10 : 1000 or 1 : 100) meta.tickDelay++; if (meta.tickDelay == meta.randomTick) { p.Message("%dUpdate target"); bot.TargetPos = p.Pos; meta.tickDelay = 0; } bot.movement = true; MobAI.SetDirectionalSpeeds(bot); Vec3F32 dir = new Vec3F32(dx, dy, dz); dir = Vec3F32.Normalise(dir); Orientation rot = bot.Rot; DirUtils.GetYawPitch(dir, out rot.RotY, out rot.HeadX); bot.Rot = rot; if (InRange(p, bot, dist)) p.Message("%cInfect"); return dx <= 8 && dy <= 16 && dz <= 8; } private readonly Random _random = new Random(); public int RandomNumber(int min, int max) { return _random.Next(min, max); } public override bool Execute(PlayerBot bot, InstructionData data) { Metadata meta = (Metadata)data.Metadata; Player closest = MobAI.ClosestPlayer(bot, 30); if (closest == null) { bot.movement = false; bot.NextInstruction(); } bool overlapsPlayer = MoveTowards(bot, closest, meta); if (overlapsPlayer && closest != null) { bot.NextInstruction(); return false; } return true; } public override InstructionData Parse(string[] args) { InstructionData data = default(InstructionData); data.Metadata = new Metadata(); return data; } public void Output(Player p, string[] args, StreamWriter w) { if (args.Length > 3) { w.WriteLine(Name + " " + ushort.Parse(args[3])); } else { w.WriteLine(Name); } } struct Coords { public int X, Y, Z; public byte RotX, RotY; } public override string[] Help { get { return help; } } static string[] help = new string[] { "%T/BotAI add [name] smart", "%HCauses the bot behave as a smart mob.", }; } #endregion #region Hostile AI /* Current AI behaviour: - Chase player if within 12 block range - Hit player if too close - Assign movement speed based on mob model - Explode if mob is a creeper - 50% chance to stand still (moving when 0-2, still when 3-5) - If not moving, wait for waitTime duration before executing next task - Choose random coord within 8x8 block radius of player and try to go to it - Do action for walkTime duration */ sealed class SmartHuntInstruction : BotInstruction { public SmartHuntInstruction() { Name = "smarthunt"; } static bool MoveTowards(PlayerBot bot, Player p, Metadata meta) { if (p == null) return false; int dx = p.Pos.X - bot.Pos.X, dy = p.Pos.Y - bot.Pos.Y, dz = p.Pos.Z - bot.Pos.Z; Vec3F32 dir = new Vec3F32(dx, dy, dz); dir = Vec3F32.Normalise(dir); Orientation rot = bot.Rot; DirUtils.GetYawPitch(dir, out rot.RotY, out rot.HeadX); PathFind(bot, p); bot.Rot = rot; MobAI.SetDirectionalSpeeds(bot); dx = Math.Abs(dx); dy = Math.Abs(dy); dz = Math.Abs(dz); // Check to see if positions collide AABB playerBB = p.ModelBB.OffsetPosition(p.Pos); AABB botBB = bot.ModelBB.OffsetPosition(bot.Pos); int dist = (int)(0.875f * 32); bool inRange = ((long)dx * dx + (long)dz * dz <= dist * dist) && (botBB.Min.Y <= playerBB.Max.Y && playerBB.Min.Y <= botBB.Max.Y); //if (inRange) HitPlayer(bot, p, rot); return dx <= 8 && dy <= 16 && dz <= 8; } static void PathFind(PlayerBot bot, Player p) { int delay = 0; var start = new Location { X = p.Pos.X / 32, Z = p.Pos.Z / 32 }; var target = new Location { X = bot.Pos.X / 32, Z = bot.Pos.Z / 32 }; // Algorithm Location current = null; var openList = new List(); var closedList = new List(); int g = 0; // Start by adding the original position to the open list openList.Add(start); while (openList.Count > 0) { // Get the square with the lowest F score var lowest = openList.Min(l => l.F); current = openList.First(l => l.F == lowest); // Add the current square to the closed list closedList.Add(current); p.SendBlockchange((ushort)current.X, 0, (ushort)current.Z, Block.Yellow); Thread.Sleep(10); // Remove it from the open list openList.Remove(current); // If we added the destination to the closed list, we've found a path if (closedList.FirstOrDefault(l => l.X == target.X && l.Z == target.Z) != null) break; var adjacentSquares = GetWalkableAdjacentSquares(p, current.X, current.Z, openList); g = current.G + 1; foreach (var adjacentSquare in adjacentSquares) { // If this adjacent square is already in the closed list, ignore it if (closedList.FirstOrDefault(l => l.X == adjacentSquare.X && l.Z == adjacentSquare.Z) != null) continue; // If it's not in the open list... if (openList.FirstOrDefault(l => l.X == adjacentSquare.X && l.Z == adjacentSquare.Z) == null) { // Compute its score, set the parent adjacentSquare.G = g; adjacentSquare.H = ComputeHScore(adjacentSquare.X, adjacentSquare.Z, target.X, target.Z); adjacentSquare.F = adjacentSquare.G + adjacentSquare.H; adjacentSquare.Parent = current; // And add it to the open list openList.Insert(0, adjacentSquare); } else { // Test if using the current G score makes the adjacent square's F score // Lower, if yes update the parent because it means it's a better path if (g + adjacentSquare.H < adjacentSquare.F) { adjacentSquare.G = g; adjacentSquare.F = adjacentSquare.G + adjacentSquare.H; adjacentSquare.Parent = current; } } } } Location end = current; // Assume path was found; let's show it while (current != null) { p.SendBlockchange((ushort)current.X, 0, (ushort)current.Z, Block.Green); current = current.Parent; Thread.Sleep(100); bot.TargetPos = new Position(current.X * 32, p.Pos.Y, current.Z * 32); bot.movement = true; } if (end != null) { //Console.WriteLine("Path : {0}", end.G); } } static List GetWalkableAdjacentSquares(Player p, int x, int z, List openList) { List list = new List(); BlockID cur = p.level.GetBlock((ushort)x, 0, (ushort)(z - 1)); if (cur == Block.Air || cur == Block.Water || cur == Block.Lava) { Location node = openList.Find(l => l.X == x && l.Z == z - 1); if (node == null) list.Add(new Location() { X = x, Z = z - 1 }); else list.Add(node); } cur = p.level.GetBlock((ushort)x, 0, (ushort)(z + 1)); if (cur == Block.Air || cur == Block.Water || cur == Block.Lava) { Location node = openList.Find(l => l.X == x && l.Z == z + 1); if (node == null) list.Add(new Location() { X = x, Z = z + 1 }); else list.Add(node); } cur = p.level.GetBlock((ushort)(x - 1), 0, (ushort)z); if (cur == Block.Air || cur == Block.Water || cur == Block.Lava) { Location node = openList.Find(l => l.X == x - 1 && l.Z == z); if (node == null) list.Add(new Location() { X = x - 1, Z = z }); else list.Add(node); } cur = p.level.GetBlock((ushort)(x + 1), 0, (ushort)z); if (cur == Block.Air || cur == Block.Water || cur == Block.Lava) { Location node = openList.Find(l => l.X == x + 1 && l.Z == z); if (node == null) list.Add(new Location() { X = x + 1, Z = z }); else list.Add(node); } return list; } static int ComputeHScore(int x, int z, int targetX, int targetZ) { return Math.Abs(targetX - x) + Math.Abs(targetZ - z); } public override bool Execute(PlayerBot bot, InstructionData data) { Metadata meta = (Metadata)data.Metadata; if (bot.Model == "skeleton" || bot.Model == "creeper") bot.movementSpeed = (int)Math.Round(3m * (short)97 / 100m); if (bot.Model == "zombie") bot.movementSpeed = (int)Math.Round(3m * (short)94 / 100m); if (bot.movementSpeed == 0) bot.movementSpeed = 1; int search = 16; // If user specified a search distance, use that instead of default if (meta.search != 16 && meta.search > 0) search = meta.search; Player closest = MobAI.ClosestPlayer(bot, search); if (closest == null) { bot.movement = false; bot.NextInstruction(); } bool overlapsPlayer = MoveTowards(bot, closest, meta); if (overlapsPlayer && closest != null) { bot.NextInstruction(); return false; } return true; } public override InstructionData Parse(string[] args) { InstructionData data = default(InstructionData); data.Metadata = new Metadata(); Metadata meta = (Metadata)data.Metadata; if (args.Length > 1) { meta.search = int.Parse(args[1]); } return data; } public void Output(Player p, string[] args, StreamWriter w) { if (args.Length > 3) { w.WriteLine(Name + " " + ushort.Parse(args[3])); } else { w.WriteLine(Name); } } struct Coords { public int X, Y, Z; public byte RotX, RotY; } public override string[] Help { get { return help; } } static string[] help = new string[] { "%T/BotAI add [name] smarthunt", "%HCauses the bot to use 2D A* pathfinding when hunting players.", }; } #endregion #region Spleef AI /* Current AI behaviour: - Chase player if within 10 block range - Delete block below the player if within 5 block range (default reach distance) - 33% chance to delete blocks (simulates click speed since nobody clicks at consistent speeds) Planned behaviour: - Nobody consistently breaks 5 blocks away, let's make it choose from 3-5 */ sealed class SpleefInstruction : BotInstruction { public SpleefInstruction() { Name = "spleef"; } static int lastY = 0; static bool MoveTowards(PlayerBot bot, Player p, Metadata meta) { if (p == null) return false; //int dist = (int)(0.875 * 32); int dx = p.Pos.X - bot.Pos.X, dy = p.Pos.Y - bot.Pos.Y, dz = p.Pos.Z - bot.Pos.Z; bot.TargetPos = p.Pos; bot.movement = true; Vec3F32 dir = new Vec3F32(dx, dy, dz); dir = Vec3F32.Normalise(dir); Orientation rot = bot.Rot; DirUtils.GetYawPitch(dir, out rot.RotY, out rot.HeadX); MobAI.SetDirectionalSpeeds(bot); dx = Math.Abs(dx); dy = Math.Abs(dy); dz = Math.Abs(dz); //if (InRange(p, bot, dist)) p.Message("%cInfect"); bot.Rot = rot; if (dx < (6 * 32) && dz < (6 * 32)) // Check if player is 6 blocks away from player { Random rnd = new Random(); // This code serves as a sort of 'CPS mechanism' to ensure that the bot does not perfectly delete every single block int chance = rnd.Next(3); // 80% chance of deleting the block // 1-3 blocks away from the player since nobody consistently deletes 5 blocks away int rangeX = rnd.Next(1, 3); int rangeZ = rnd.Next(1, 3); int distanceX = p.Pos.X - bot.Pos.X, distanceY = p.Pos.Y - bot.Pos.Y, distanceZ = p.Pos.Z - bot.Pos.Z; int distance = (distanceX + distanceZ) / 32; int speed = rnd.Next(1, 3); p.Message("dist " + distance + " sp " + speed); if (distance < 0) { // Subtract bot.level.UpdateBlock(Player.Console, (ushort)((p.Pos.X / 32) - rangeX), (ushort)((p.Pos.Y / 32) - 2), (ushort)((p.Pos.Z / 32) - rangeZ), Block.Air); if ((p.Pos.Y / 32) > lastY) bot.level.UpdateBlock(Player.Console, (ushort)((p.Pos.X / 32) - rangeX), (ushort)((p.Pos.Y / 32) - 3), (ushort)((p.Pos.Z / 32) - rangeZ), Block.Air); if (speed > 1) { bot.level.UpdateBlock(Player.Console, (ushort)((p.Pos.X / 32) - rangeX - 1), (ushort)((p.Pos.Y / 32) - 2), (ushort)((p.Pos.Z / 32) - rangeZ - 1), Block.Air); if (speed > 2) bot.level.UpdateBlock(Player.Console, (ushort)((p.Pos.X / 32) - rangeX - 2), (ushort)((p.Pos.Y / 32) - 2), (ushort)((p.Pos.Z / 32) - rangeZ - 2), Block.Air); } } else { // Add bot.level.UpdateBlock(Player.Console, (ushort)((p.Pos.X / 32) + rangeX), (ushort)((p.Pos.Y / 32) - 2), (ushort)((p.Pos.Z / 32) + rangeZ), Block.Air); if (speed > 1) { bot.level.UpdateBlock(Player.Console, (ushort)((p.Pos.X / 32) + rangeX + 1), (ushort)((p.Pos.Y / 32) - 2), (ushort)((p.Pos.Z / 32) + rangeZ + 1), Block.Air); if (speed > 2) bot.level.UpdateBlock(Player.Console, (ushort)((p.Pos.X / 32) + rangeX + 2), (ushort)((p.Pos.Y / 32) - 2), (ushort)((p.Pos.Z / 32) + rangeZ + 2), Block.Air); } if ((p.Pos.Y / 32) > lastY) bot.level.UpdateBlock(Player.Console, (ushort)((p.Pos.X / 32) + rangeX), (ushort)((p.Pos.Y / 32) - 3), (ushort)((p.Pos.Z / 32) + rangeZ), Block.Air); } lastY = (p.Pos.Y / 32); } return dx <= 8 && dy <= 16 && dz <= 8; } private readonly Random _random = new Random(); public int RandomNumber(int min, int max) { return _random.Next(min, max); } public override bool Execute(PlayerBot bot, InstructionData data) { Metadata meta = (Metadata)data.Metadata; Player closest = MobAI.ClosestPlayer(bot, 20); if (closest == null) { bot.movement = false; bot.NextInstruction(); } bool overlapsPlayer = MoveTowards(bot, closest, meta); if (overlapsPlayer && closest != null) { bot.NextInstruction(); return false; } return true; } public override InstructionData Parse(string[] args) { InstructionData data = default(InstructionData); data.Metadata = new Metadata(); return data; } public void Output(Player p, string[] args, StreamWriter w) { if (args.Length > 3) { w.WriteLine(Name + " " + ushort.Parse(args[3])); } else { w.WriteLine(Name); } } struct Coords { public int X, Y, Z; public byte RotX, RotY; } public override string[] Help { get { return help; } } static string[] help = new string[] { "%T/BotAI add [name] spleef", "%HCauses the bot to try and spleef you.", }; } #endregion #region TNT AI /* Current AI behaviour: - Chase player if within 10 block range - Delete block below the player if within 5 block range (default reach distance) - 33% chance to delete blocks (simulates click speed since nobody clicks at consistent speeds) Planned behaviour: - Nobody consistently breaks 5 blocks away, let's make it choose from 3-5 */ sealed class TNTInstruction : BotInstruction { public TNTInstruction() { Name = "tnt"; } static int lastY = 0; static bool MoveTowards(PlayerBot bot, Player p, Metadata meta) { if (p == null) return false; //int dist = (int)(0.875 * 32); int dx = p.Pos.X - bot.Pos.X, dy = p.Pos.Y - bot.Pos.Y, dz = p.Pos.Z - bot.Pos.Z; bot.TargetPos = p.Pos; bot.movement = true; Vec3F32 dir = new Vec3F32(dx, dy, dz); dir = Vec3F32.Normalise(dir); Orientation rot = bot.Rot; DirUtils.GetYawPitch(dir, out rot.RotY, out rot.HeadX); MobAI.SetDirectionalSpeeds(bot); dx = Math.Abs(dx); dy = Math.Abs(dy); dz = Math.Abs(dz); //if (InRange(p, bot, dist)) p.Message("%cInfect"); bot.Rot = rot; if (dx < (5 * 32) && dz < (5 * 32)) // 5 block reach { Random rnd = new Random(); // This code serves as a sort of 'CPS mechanism' to ensure that the bot does not perfectly delete every single block int chance = rnd.Next(0, 4); // 33% chance of deleting the block if (chance < 3) { bot.level.UpdateBlock(Player.Console, (ushort)(p.Pos.X / 32), (ushort)((p.Pos.Y / 32) - 2), (ushort)(p.Pos.Z / 32), Block.Air); if ((p.Pos.Y / 32) > lastY) bot.level.UpdateBlock(Player.Console, (ushort)(p.Pos.X / 32), (ushort)((p.Pos.Y / 32) - 3), (ushort)(p.Pos.Z / 32), Block.Air); } lastY = (p.Pos.Y / 32); } return dx <= 8 && dy <= 16 && dz <= 8; } private readonly Random _random = new Random(); public int RandomNumber(int min, int max) { return _random.Next(min, max); } public override bool Execute(PlayerBot bot, InstructionData data) { Metadata meta = (Metadata)data.Metadata; Player closest = MobAI.ClosestPlayer(bot, 10); if (closest == null) { bot.movement = false; bot.NextInstruction(); } bool overlapsPlayer = MoveTowards(bot, closest, meta); if (overlapsPlayer && closest != null) { bot.NextInstruction(); return false; } return true; } public override InstructionData Parse(string[] args) { InstructionData data = default(InstructionData); data.Metadata = new Metadata(); return data; } public void Output(Player p, string[] args, StreamWriter w) { if (args.Length > 3) { w.WriteLine(Name + " " + ushort.Parse(args[3])); } else { w.WriteLine(Name); } } struct Coords { public int X, Y, Z; public byte RotX, RotY; } public override string[] Help { get { return help; } } static string[] help = new string[] { "%T/BotAI add [name] spleef", "%HCauses the bot to try and spleef you.", }; } #endregion #region Bow item public abstract class Bow { public string Name { get { return "Bow"; } } public class BowData { public BlockID block; public Vec3F32 pos, vel; public Vec3U16 last, next; public Vec3F32 drag; public float power; public float gravity; } public static PlayerBot bot; public static bool shooting = false; public static SchedulerTask task; /// Called when the bot fires this weapon. public static void OnActivated(PlayerBot bot, Vec3F32 dir) { BowData data = MakeArgs(bot, dir, Block.Red); UpdateNext(bot, data); Server.MainScheduler.QueueRepeat(BowCallback, data, TimeSpan.FromMilliseconds(50)); } /// Applies this weapon to the given bot, and sets up necessary state. public static void Enable(PlayerBot bot, Player pl) { if (shooting) return; shooting = true; Bow.bot = bot; int dx = bot.Pos.X - pl.Pos.X, dy = bot.Pos.Y - pl.Pos.Y, dz = bot.Pos.Z - pl.Pos.Z; Vec3F32 dir = new Vec3F32(-dx, -dy, -dz); dir = Vec3F32.Normalise(dir); OnActivated(bot, dir); } static Vec3U16 Round(Vec3F32 v) { unchecked { return new Vec3U16((ushort)Math.Round(v.X), (ushort)Math.Round(v.Y), (ushort)Math.Round(v.Z)); } } static BowData MakeArgs(PlayerBot bot, Vec3F32 dir, BlockID block) { BowData args = new BowData(); args.block = Block.FromRaw(725); args.drag = new Vec3F32(0.95f, 0.98f, 0.95f); args.gravity = 0.08f; args.pos = bot.Pos.BlockCoords; args.last = Round(args.pos); args.next = Round(args.pos); float push = 3 * 0.755f; args.vel = new Vec3F32(dir.X * push, dir.Y * push, dir.Z * push); return args; } static void RevertLast(PlayerBot bot, BowData data) { bot.level.BroadcastRevert(data.last.X, data.last.Y, data.last.Z); } static void UpdateNext(PlayerBot bot, BowData data) { bot.level.BroadcastChange(data.next.X, data.next.Y, data.next.Z, data.block); } static void OnHitPlayer(PlayerBot bot, BowData args, Player pl) { if (pl == null) return; if (pl.invincible || pl.Game.Referee) return; PushPlayer(bot, pl); } static void PushPlayer(PlayerBot bot, Player pl) { if (pl.level.Config.MOTD.ToLower().Contains("-damage")) return; int srcHeight = ModelInfo.CalcEyeHeight(bot); int dstHeight = ModelInfo.CalcEyeHeight(pl); int dx = bot.Pos.X - pl.Pos.X, dy = (bot.Pos.Y + srcHeight) - (pl.Pos.Y + dstHeight), dz = bot.Pos.Z - pl.Pos.Z; Vec3F32 dir = new Vec3F32(dx, dy, dz); if (dir.Length > 0) dir = Vec3F32.Normalise(dir); float mult = 1 / ModelInfo.GetRawScale(pl.Model); float plScale = ModelInfo.GetRawScale(pl.Model); if (pl.Supports(CpeExt.VelocityControl)) { // Intensity of force is in part determined by model scale // Also incremented by power of bow float push = 3 * 0.77f; pl.Send(Packet.VelocityControl((-dir.X * mult) * push, 1.0117f * mult, (-dir.Z * mult) * push, 0, 1, 0)); } } static void BowCallback(SchedulerTask task) { Bow.task = task; BowData data = (BowData)task.State; if (TickBow(bot, data)) return; // Done RevertLast(bot, data); task.Repeating = false; } static Player PlayerAt(PlayerBot bot, Vec3U16 pos) { Player[] players = PlayerInfo.Online.Items; foreach (Player pl in players) { if (pl.level != bot.level) continue; if (Math.Abs(pl.Pos.BlockX - pos.X) <= 1 && Math.Abs(pl.Pos.BlockY - pos.Y) <= 1 && Math.Abs(pl.Pos.BlockZ - pos.Z) <= 1) { return pl; } } return null; } protected static PlayerBot BotAt(PlayerBot bot, Vec3U16 pos) { PlayerBot[] bots = bot.level.Bots.Items; foreach (PlayerBot b in bots) { if (b == null) continue; if (b == bot) continue; Vec3F32 scale = ModelInfo.CalcScale(b); float scaleX = scale.X == 0f ? 1f : scale.X; float scaleY = scale.Y == 0f ? 1f : scale.Y; float scaleZ = scale.Z == 0f ? 1f : scale.Z; scaleX = scaleX / 4f; if (scaleY > 1f) scaleY = scaleY * 2f; scaleZ = scaleZ / 4f; if (Math.Abs(b.Pos.BlockX - pos.X) <= scaleX && Math.Abs(b.Pos.BlockY - pos.Y) <= scaleY && Math.Abs(b.Pos.BlockZ - pos.Z) <= scaleZ) { return b; } } return null; } static void OnHitBlock(PlayerBot bot, BowData args, Vec3U16 pos, BlockID block) { } static void OnHitBot(PlayerBot bot, BowData args, PlayerBot hit) { if (hit == null) return; if (bot == hit) return; int number; bool isNumber = int.TryParse(hit.Owner, out number); if (hit.Owner == null || !isNumber) return; int damage = 3; HurtBot(damage, hit, bot); } public static void HurtBot(int damage, PlayerBot hit, PlayerBot bot) { int srcHeight = ModelInfo.CalcEyeHeight(hit); int dstHeight = ModelInfo.CalcEyeHeight(bot); int dx = bot.Pos.X - hit.Pos.X, dy = (bot.Pos.Y + srcHeight) - (hit.Pos.Y + dstHeight), dz = bot.Pos.Z - hit.Pos.Z; Vec3F32 dir = new Vec3F32(dx, dy, dz); if (dir.Length > 0) dir = Vec3F32.Normalise(dir); float mult = 1 / ModelInfo.GetRawScale(hit.Model); float plScale = ModelInfo.GetRawScale(hit.Model); Position newPos; newPos.X = hit.Pos.X + (int)(hit.Pos.X - bot.Pos.X); newPos.Y = hit.Pos.Y; newPos.Z = hit.Pos.Z + (int)(hit.Pos.Z - bot.Pos.Z); Position newMidPos; newMidPos.X = hit.Pos.X + (int)((hit.Pos.X - bot.Pos.X) / 2); newMidPos.Y = hit.Pos.Y; newMidPos.Z = hit.Pos.Z + (int)((hit.Pos.Z - bot.Pos.Z) / 2); if (hit.level.IsAirAt((ushort)newPos.BlockX, (ushort)newPos.BlockY, (ushort)newPos.BlockZ) && hit.level.IsAirAt((ushort)newPos.BlockX, (ushort)(newPos.BlockY - 1), (ushort)newPos.BlockZ) && hit.level.IsAirAt((ushort)newMidPos.BlockX, (ushort)newMidPos.BlockY, (ushort)newMidPos.BlockZ) && hit.level.IsAirAt((ushort)newMidPos.BlockX, (ushort)(newMidPos.BlockY - 1), (ushort)newMidPos.BlockZ)) { hit.Pos = new Position(newPos.X, newPos.Y, newPos.Z); } int hp; bool isNumber = int.TryParse(hit.Owner, out hp); if (hit.Owner == null || !isNumber) return; int newHp = hp - damage; hit.Owner = newHp.ToString(); if (newHp <= 0) { // Despawn the bot that actually got hit PlayerBot.Remove(hit); } } static bool TickBow(PlayerBot bot, BowData data) { Vec3U16 pos = data.next; BlockID cur = bot.level.GetBlock(pos.X, pos.Y, pos.Z); // Hit a block if (cur == Block.Invalid) { shooting = false; return false; } //BlockDefinition def = p.level.GetBlockDef(cur); if (cur != Block.Air) { OnHitBlock(bot, data, pos, cur); shooting = false; return false; } // Hit a bot PlayerBot hit = BotAt(bot, pos); if (hit != null) { OnHitBot(bot, data, hit); shooting = false; return false; } // Hit a victim Player pl = PlayerAt(bot, pos); if (pl != null) { OnHitPlayer(bot, data, pl); shooting = false; return false; } // Apply physics data.pos += data.vel; data.vel.X *= data.drag.X; data.vel.Y *= data.drag.Y; data.vel.Z *= data.drag.Z; data.vel.Y -= data.gravity; data.next = Round(data.pos); if (data.last == data.next) { shooting = false; return true; } // Moved a block, update in world RevertLast(bot, data); UpdateNext(bot, data); data.last = data.next; shooting = false; return true; } } public class AmmunitionData { public BlockID block; public Vec3U16 start; public Vec3F32 dir; public bool moving = true; // Positions of all currently visible "trailing" blocks public List visible = new List(); // Position of all blocks this ammunition has touched/gone through public List all = new List(); public int iterations; public Vec3U16 PosAt(int i) { Vec3U16 target; target.X = (ushort)Math.Round(start.X + (double)(dir.X * i)); target.Y = (ushort)Math.Round(start.Y + (double)(dir.Y * i)); target.Z = (ushort)Math.Round(start.Z + (double)(dir.Z * i)); return target; } } #endregion }