// Special Zig-Zag movement for whirlwind/tempest /// <summary> /// Finds an optimal position for Barbarian Whirlwind, Monk Tempest Rush, or Demon Hunter Strafe /// </summary> /// <param name="origin"></param> /// <param name="ringDistance"></param> /// <param name="randomizeDistance"></param> /// <returns></returns> internal static Vector3 GetZigZagTarget(Vector3 origin, float ringDistance, bool randomizeDistance = false) { var minDistance = 9f; Vector3 myPos = PlayerStatus.CurrentPosition; float distanceToTarget = origin.Distance2D(myPos); Vector3 zigZagPoint = origin; using (new PerformanceLogger("FindZigZagTargetLocation")) { using (new PerformanceLogger("FindZigZagTargetLocation.CheckObjectCache")) { bool useTargetBasedZigZag = false; float maxDistance = 25f; int minTargets = 2; if (GilesTrinity.PlayerStatus.ActorClass == ActorClass.Monk) { maxDistance = 20f; minTargets = 3; useTargetBasedZigZag = (GilesTrinity.Settings.Combat.Monk.TargetBasedZigZag); } if (GilesTrinity.PlayerStatus.ActorClass == ActorClass.Barbarian) { useTargetBasedZigZag = (GilesTrinity.Settings.Combat.Barbarian.TargetBasedZigZag); } int eliteCount = ObjectCache.Count(u => u.Type == GObjectType.Unit && u.IsBossOrEliteRareUnique); bool shouldZigZagElites = ((GilesTrinity.CurrentTarget.IsBossOrEliteRareUnique && eliteCount > 1) || eliteCount == 0); if (useTargetBasedZigZag && shouldZigZagElites && !AnyTreasureGoblinsPresent && ObjectCache.Where(o => o.Type == GObjectType.Unit).Count() >= minTargets) { var clusterPoint = TargetUtil.GetBestClusterPoint(ringDistance, ringDistance, false); if (clusterPoint.Distance2D(PlayerStatus.CurrentPosition) >= minDistance) { DbHelper.Log(LogCategory.Movement, "Returning ZigZag: BestClusterPoint {0} r-dist={1} t-dist={2}", clusterPoint, ringDistance, clusterPoint.Distance2D(PlayerStatus.CurrentPosition)); return(clusterPoint); } IEnumerable <GilesObject> zigZagTargets = from u in ObjectCache where u.Type == GObjectType.Unit && u.RadiusDistance < maxDistance && !GilesTrinity.hashAvoidanceObstacleCache.Any(a => Vector3.Distance(u.Position, a.Location) < AvoidanceManager.GetAvoidanceRadiusBySNO(a.ActorSNO, a.Radius) && PlayerStatus.CurrentHealthPct <= AvoidanceManager.GetAvoidanceHealthBySNO(a.ActorSNO, 1)) select u; if (zigZagTargets.Count() >= minTargets) { zigZagPoint = zigZagTargets.OrderByDescending(u => u.CentreDistance).FirstOrDefault().Position; if (NavHelper.CanRayCast(zigZagPoint) && zigZagPoint.Distance2D(PlayerStatus.CurrentPosition) >= minDistance) { DbHelper.Log(LogCategory.Movement, "Returning ZigZag: TargetBased {0} r-dist={1} t-dist={2}", zigZagPoint, ringDistance, zigZagPoint.Distance2D(PlayerStatus.CurrentPosition)); return(zigZagPoint); } } } } Random rndNum = new Random(int.Parse(Guid.NewGuid().ToString().Substring(0, 8), NumberStyles.HexNumber)); using (new PerformanceLogger("FindZigZagTargetLocation.RandomZigZagPoint")) { float highestWeightFound = float.NegativeInfinity; Vector3 bestLocation = Vector3.Zero; // the unit circle always starts at 0 :) double min = 0; // the maximum size of a unit circle double max = 2 * Math.PI; // the number of times we will iterate around the circle to find points double piSlices = 16; // We will do several "passes" to make sure we can get a point that we can least zig-zag to // The total number of points tested will be piSlices * distancePasses.Count List <float> distancePasses = new List <float>(); distancePasses.Add(ringDistance * 1 / 2); // Do one loop at 1/2 distance distancePasses.Add(ringDistance * 3 / 4); // Do one loop at 3/4 distance distancePasses.Add(ringDistance); // Do one loop at exact distance foreach (float distance in distancePasses) { for (double direction = min; direction < max; direction += (Math.PI / piSlices)) { // Starting weight is 1 float pointWeight = 1f; // Find a new XY zigZagPoint = MathEx.GetPointAt(origin, distance, (float)direction); // Get the Z zigZagPoint.Z = GilesTrinity.gp.GetHeight(zigZagPoint.ToVector2()); // Make sure we're actually zig-zagging our target, except if we're kiting bool intersectsPath = MathUtil.IntersectsPath(CurrentTarget.Position, CurrentTarget.Radius, myPos, zigZagPoint); if (GilesTrinity.PlayerKiteDistance <= 0 && !intersectsPath) { continue; } // if we're kiting, lets not actualy run through monsters if (GilesTrinity.PlayerKiteDistance > 0 && GilesTrinity.hashMonsterObstacleCache.Any(m => m.Location.Distance(zigZagPoint) <= GilesTrinity.PlayerKiteDistance)) { continue; } // Ignore point if any AoE in this point position if (GilesTrinity.hashAvoidanceObstacleCache.Any(m => m.Location.Distance(zigZagPoint) <= m.Radius && PlayerStatus.CurrentHealthPct <= AvoidanceManager.GetAvoidanceHealthBySNO(m.ActorSNO, 1))) { continue; } // Make sure this point is in LoS/walkable (not around corners or into a wall) bool canRayCast = !Navigator.Raycast(PlayerStatus.CurrentPosition, zigZagPoint); if (!canRayCast) { continue; } float distanceToPoint = zigZagPoint.Distance2D(myPos); float distanceFromTargetToPoint = zigZagPoint.Distance2D(origin); // Lots of weight for points further away from us (e.g. behind our CurrentTarget) pointWeight *= distanceToPoint; // Add weight for any units in this point int monsterCount = ObjectCache.Count(u => u.Type == GObjectType.Unit && u.Position.Distance2D(zigZagPoint) <= Math.Max(u.Radius, 10f)); if (monsterCount > 0) { pointWeight *= monsterCount; } DbHelper.Log(LogCategory.Movement, "ZigZag Point: {0} distance={1:0} distaceFromTarget={2:0} intersectsPath={3} weight={4:0} monsterCount={5}", zigZagPoint, distanceToPoint, distanceFromTargetToPoint, intersectsPath, pointWeight, monsterCount); // Use this one if it's more weight, or we haven't even found one yet, or if same weight as another with a random chance if (pointWeight > highestWeightFound) { highestWeightFound = pointWeight; if (GilesTrinity.Settings.Combat.Misc.UseNavMeshTargeting) { bestLocation = new Vector3(zigZagPoint.X, zigZagPoint.Y, GilesTrinity.gp.GetHeight(zigZagPoint.ToVector2())); } else { bestLocation = new Vector3(zigZagPoint.X, zigZagPoint.Y, zigZagPoint.Z + 4); } } } } DbHelper.Log(LogCategory.Movement, "Returning ZigZag: RandomXY {0} r-dist={1} t-dist={2}", bestLocation, ringDistance, bestLocation.Distance2D(PlayerStatus.CurrentPosition)); return(bestLocation); } } }
internal static Vector3 newFindSafeZone(Vector3 origin, bool shouldKite = false, bool isStuck = false, IEnumerable <GilesObject> monsterList = null) { /* * generate 50x50 grid of 5x5 squares within max 100 distance from origin to edge of grid * * all squares start with 0 weight * * check if Center IsNavigable * check Z * check if avoidance is present * check if monsters are present * * final distance tile weight = (Max Dist - Dist)/Max Dist*Max Weight * * end result should be that only navigable squares where no avoidance, monsters, or obstacles are present */ float gridSquareSize = 5f; int maxDistance = 200; int maxWeight = 100; int maxZDiff = 14; int gridTotalSize = (int)(maxDistance / gridSquareSize) * 2; /* If maxDistance is the radius of a circle from the origin, then we want to get the hypotenuse of the radius (x) and tangent (y) as our search grid corner * anything outside of the circle will not be considered */ Vector2 topleft = new Vector2(origin.X - maxDistance, origin.Y - maxDistance); //Make a circle on the corners of the square double gridSquareRadius = Math.Sqrt((Math.Pow(gridSquareSize / 2, 2) + Math.Pow(gridSquareSize / 2, 2))); GridPoint bestPoint = new GridPoint(Vector3.Zero, 0, 0); int nodesNotNavigable = 0; int nodesZDiff = 0; int nodesGT45Raycast = 0; int nodesAvoidance = 0; int nodesMonsters = 0; int pathFailures = 0; for (int x = 0; x < gridTotalSize; x++) { for (int y = 0; y < gridTotalSize; y++) { Vector2 xy = new Vector2(topleft.X + (x * gridSquareSize), topleft.Y + (y * gridSquareSize)); Vector3 xyz = Vector3.Zero; Point p_xy = Point.Empty; if (GilesTrinity.Settings.Combat.Misc.UseNavMeshTargeting) { xyz = new Vector3(xy.X, xy.Y, gp.GetHeight(xy)); } else { xyz = new Vector3(xy.X, xy.Y, origin.Z + 4); } GridPoint gridPoint = new GridPoint(xyz, 0, origin.Distance(xyz)); //if (gridPoint.Distance > maxDistance + gridSquareRadius) // continue; if (GilesTrinity.Settings.Combat.Misc.UseNavMeshTargeting) { p_xy = gp.WorldToGrid(xy); if (!gp.CanStandAt(p_xy)) { nodesNotNavigable++; continue; } } else { // If ZDiff is way too different (up a cliff or wall) if (Math.Abs(gridPoint.Position.Z - origin.Z) > maxZDiff) { nodesZDiff++; continue; } } if (gridPoint.Distance > 45 && Navigator.Raycast(origin, xyz)) { nodesGT45Raycast++; continue; } if (isStuck && gridPoint.Distance > (PlayerMover.iTotalAntiStuckAttempts + 2) * 5) { continue; } /* * Check if a square is occupied already */ // Avoidance if (GilesTrinity.hashAvoidanceObstacleCache.Any(a => Vector3.Distance(xyz, a.Location) - a.Radius <= gridSquareRadius)) { nodesAvoidance++; continue; } // Obstacles if (GilesTrinity.hashNavigationObstacleCache.Any(a => Vector3.Distance(xyz, a.Location) - a.Radius <= gridSquareRadius)) { nodesMonsters++; continue; } // Monsters if (shouldKite) { // Any monster standing in this GridPoint if (GilesTrinity.hashMonsterObstacleCache.Any(a => Vector3.Distance(xyz, a.Location) - a.Radius <= (shouldKite ? gridSquareRadius : gridSquareSize + GilesTrinity.PlayerKiteDistance))) { nodesMonsters++; continue; } if (!hasEmergencyTeleportUp) { // Any monsters blocking in a straight line between origin and this GridPoint foreach (GilesObstacle monster in GilesTrinity.hashMonsterObstacleCache.Where(m => MathEx.IntersectsPath(new Vector3(m.Location.X, m.Location.Y, 0), m.Radius, new Vector3(origin.X, origin.Y, 0), new Vector3(gridPoint.Position.X, gridPoint.Position.Y, 0)) )) { nodesMonsters++; continue; } } int nearbyMonsters = (monsterList != null ? monsterList.Count() : 0); } if (isStuck && UsedStuckSpots.Any(p => Vector3.Distance(p.Position, gridPoint.Position) <= gridSquareRadius)) { continue; } if (!isStuck) { gridPoint.Weight = ((maxDistance - gridPoint.Distance) / maxDistance) * maxWeight; // Low weight for close range grid points if (shouldKite && gridPoint.Distance < GilesTrinity.PlayerKiteDistance) { gridPoint.Weight = (int)gridPoint.Distance; } } else { gridPoint.Weight = gridPoint.Distance; } // Boss Areas if (UnSafeZone.UnsafeKiteAreas.Any(a => a.WorldId == ZetaDia.CurrentWorldId && Vector3.Distance(a.Position, gridPoint.Position) <= a.Radius)) { continue; } if (shouldKite) { // make sure we can raycast to our target if (!NavHelper.CanRayCast(gridPoint.Position, GilesTrinity.LastPrimaryTargetPosition)) { continue; } /* * We want to down-weight any grid points where monsters are closer to it than we are */ foreach (GilesObstacle monster in GilesTrinity.hashMonsterObstacleCache) { float distFromMonster = gridPoint.Position.Distance2D(monster.Location); float distFromOrigin = gridPoint.Position.Distance2D(origin); float distFromOriginToAvoidance = origin.Distance2D(monster.Location); if (distFromOriginToAvoidance < distFromOrigin) { continue; } if (distFromMonster < distFromOrigin) { gridPoint.Weight -= distFromOrigin; } else if (distFromMonster > distFromOrigin) { gridPoint.Weight += distFromMonster; } } foreach (GilesObstacle avoidance in GilesTrinity.hashAvoidanceObstacleCache) { float distFromAvoidance = gridPoint.Position.Distance2D(avoidance.Location); float distFromOrigin = gridPoint.Position.Distance2D(origin); float distFromOriginToAvoidance = origin.Distance2D(avoidance.Location); float health = AvoidanceManager.GetAvoidanceHealthBySNO(avoidance.ActorSNO, 1f); float radius = AvoidanceManager.GetAvoidanceRadiusBySNO(avoidance.ActorSNO, 1f); // position is inside avoidance if (PlayerStatus.CurrentHealthPct < health && distFromAvoidance < radius) { continue; } // closer to avoidance than it is to player if (distFromOriginToAvoidance < distFromOrigin) { continue; } if (distFromAvoidance < distFromOrigin) { gridPoint.Weight -= distFromOrigin; } else if (distFromAvoidance > distFromOrigin) { gridPoint.Weight += distFromAvoidance; } } } else if (isStuck) { // give weight to points nearer to our destination gridPoint.Weight *= (maxDistance - PlayerMover.LastMoveToTarget.Distance2D(gridPoint.Position)) / maxDistance * maxWeight; } else if (!shouldKite && !isStuck) // melee avoidance use only { var monsterCount = GilesTrinity.GilesObjectCache.Count(u => u.Type == GObjectType.Unit && u.Position.Distance2D(gridPoint.Position) <= gridSquareRadius); if (monsterCount > 0) { gridPoint.Weight *= monsterCount; } } if (gridPoint.Weight > bestPoint.Weight && gridPoint.Distance > 1) { bestPoint = gridPoint; } //if (gridPoint.Weight > 0) // DbHelper.Log(TrinityLogLevel.Verbose, LogCategory.Moving, "Kiting grid point {0}, distance: {1:0}, weight: {2:0}", gridPoint.Position, gridPoint.Distance, gridPoint.Weight); } } if (isStuck) { UsedStuckSpots.Add(bestPoint); } DbHelper.Log(TrinityLogLevel.Verbose, LogCategory.Movement, "Kiting grid found {0}, distance: {1:0}, weight: {2:0}", bestPoint.Position, bestPoint.Distance, bestPoint.Weight); DbHelper.Log(TrinityLogLevel.Verbose, LogCategory.Movement, "Kiting grid stats NotNavigable {0} ZDiff {1} GT45Raycast {2} Avoidance {3} Monsters {4} pathFailures {5}", nodesNotNavigable, nodesZDiff, nodesGT45Raycast, nodesAvoidance, nodesMonsters, pathFailures); return(bestPoint.Position); }