From b7f6e69614fc006f04a31928f7df433c1346a064 Mon Sep 17 00:00:00 2001 From: Arufonsu <17498701+Arufonsu@users.noreply.github.com> Date: Sun, 30 Jun 2024 13:04:46 -0400 Subject: [PATCH 1/2] projectile refactoring: fixes, docs, enhancements - projectile related code has been refactored, documented and commented. - renamed variables to follow current Intersect's naming conventions. - fixed variable typos. - removed unused variables, libraries and imports. - a lot of methods have been merged into one (GetProjectileOffset, GetRange, FindProjectileRotation) for better organization. - created AdjustPositionOnMapBoundaries methods for better organization within CheckForCollision (server and client) - Suppress finalization to optimize garbage collection on projectile disposal (client), this is important for performance and to prevent the finalizer from running. - fixes server crash due to IndexOutOfRangeException when attacking across maps. - fixes projectiles not crossing map boundaries when stepping into more than one boundary (ie. diagonal crossing maps). - fixes server crash due to Z Dimension and Z block level issues with projectiles. - fixes projectile issues when stepping into Z block attributes, now they are destroyed when they are supposed to. - Linear interpolation improvements (client): we had time as a constant, this caused the two problems: non linear movement and the object not actually reaching the target position visually, so now, linear interpolation is dinamically based on the elapsed time since the projectile was spawned. Also, The interpolation factor is now clamped to ensure that the projectile does not overshoot its target position. - GetProjectileLerping considers Options.Instance.Processing.ProjectileUpdateInterval in order to visually adjust projectiles according to the server update intervals - Pattern Matching Enhancements: specifically the use of the switch expression with pattern matching introduced in C# 8.0 and enhanced in later versions. The use of the or keyword to combine multiple patterns in a single case is a feature of these enhancements, allowing for more concise and readable code when checking for multiple conditions. (GetRange, FindProjectileRotation. --- .../Entities/Projectiles/Projectile.cs | 785 ++++++++++-------- .../Entities/Projectiles/ProjectileSpawns.cs | 8 +- Intersect.Server.Core/Entities/Projectile.cs | 526 ++++++------ .../Entities/ProjectileSpawn.cs | 48 +- 4 files changed, 740 insertions(+), 627 deletions(-) diff --git a/Intersect.Client/Entities/Projectiles/Projectile.cs b/Intersect.Client/Entities/Projectiles/Projectile.cs index 48207fab4f..35d0a1ef9e 100644 --- a/Intersect.Client/Entities/Projectiles/Projectile.cs +++ b/Intersect.Client/Entities/Projectiles/Projectile.cs @@ -13,48 +13,46 @@ namespace Intersect.Client.Entities.Projectiles public partial class Projectile : Entity { - private bool mDisposing; + private bool _isDisposing; - private bool mLoaded; + private bool _isLoaded; - private object mLock = new object(); + private readonly object _lock = new object(); - private ProjectileBase mMyBase; + private ProjectileBase _myBase; - private Guid mOwner; + private Guid _owner; - private int mQuantity; + private int _quantity; - private int mSpawnCount; + private int _spawnCount; - private int mSpawnedAmount; + private int _spawnedAmount; - private long mSpawnTime; + private long _spawnTime; - private int mTotalSpawns; + private int _totalSpawns; - public Guid ProjectileId; + private Guid _projectileId; // Individual Spawns - public ProjectileSpawns[] Spawns; + private ProjectileSpawns[] _spawns; - public Guid TargetId; + private Guid _targetId; - public int mLastTargetX = -1; + private int _lastTargetX = -1; - public int mLastTargetY = -1; + private int _lastTargetY = -1; - public Guid mLastTargetMapId = Guid.Empty; - - public Dictionary mCurrentCoords = new Dictionary(); + private Guid _lastTargetMapId = Guid.Empty; /// - /// The constructor for the inherated projectile class + /// The constructor for the inherated projectile class /// public Projectile(Guid id, ProjectileEntityPacket packet) : base(id, packet, EntityType.Projectile) { - Vital[(int) Enums.Vital.Health] = 1; - MaxVital[(int) Enums.Vital.Health] = 1; + Vital[(int)Enums.Vital.Health] = 1; + MaxVital[(int)Enums.Vital.Health] = 1; HideName = true; Passable = true; IsMoving = true; @@ -62,19 +60,19 @@ public Projectile(Guid id, ProjectileEntityPacket packet) : base(id, packet, Ent public override void Load(EntityPacket packet) { - if (mLoaded) + if (_isLoaded) { return; } base.Load(packet); - var pkt = (ProjectileEntityPacket) packet; - ProjectileId = pkt.ProjectileId; + var pkt = (ProjectileEntityPacket)packet; + _projectileId = pkt.ProjectileId; Dir = (Direction)pkt.ProjectileDirection; - TargetId = pkt.TargetId; - mOwner = pkt.OwnerId; - mMyBase = ProjectileBase.Get(ProjectileId); - if (mMyBase != null) + _targetId = pkt.TargetId; + _owner = pkt.OwnerId; + _myBase = ProjectileBase.Get(_projectileId); + if (_myBase != null) { for (var x = 0; x < ProjectileBase.SPAWN_LOCATIONS_WIDTH; x++) { @@ -82,43 +80,52 @@ public override void Load(EntityPacket packet) { for (var d = 0; d < ProjectileBase.MAX_PROJECTILE_DIRECTIONS; d++) { - if (mMyBase.SpawnLocations[x, y].Directions[d] == true) + if (_myBase.SpawnLocations[x, y].Directions[d] == true) { - mTotalSpawns++; + _totalSpawns++; } } } } - mTotalSpawns *= mMyBase.Quantity; + _totalSpawns *= _myBase.Quantity; } - Spawns = new ProjectileSpawns[mTotalSpawns]; - mLoaded = true; + _spawns = new ProjectileSpawns[_totalSpawns]; + _isLoaded = true; } + /// + /// Disposes of the resources used by this instance, preventing any further use. + /// public override void Dispose() { - if (!mDisposing) + // Check if Dispose has already been called to avoid redundant calculations. + if (!_isDisposing) { - lock (mLock) + // Use a lock to ensure thread safety when disposing resources. + lock (_lock) { - mDisposing = true; - if (mSpawnedAmount == 0) + _isDisposing = true; // Mark as disposing to prevent concurrent disposal calls. + + // Perform a final update if no projectiles have been spawned. + if (_spawnedAmount == 0) { Update(); } - if (Spawns != null) + // Dispose of each spawned projectile's animation resources. + if (_spawns != null) { - foreach (var s in Spawns) + foreach (var s in _spawns) { - if (s != null && s.Anim != null) - { - s.Anim.DisposeNextDraw(); - } + s?.Anim?.DisposeNextDraw(); // Safely call DisposeNextDraw, if the animation exists. } } + + // Suppress finalization to optimize garbage collection. + // This is important for performance and to prevent the finalizer from running. + GC.SuppressFinalize(this); } } } @@ -132,15 +139,19 @@ public override bool CanBeAttacked } } - //Find out which animation data to load depending on what spawn wave we are on during projection. + /// + /// Determines the appropriate animation data index for the current spawn wave of the projectile. + /// This method iterates through the available animations defined in the projectile's base configuration, + /// selecting the one whose spawn range encompasses the current quantity of spawns. + /// + /// The index of the animation data to use for the current spawn wave. private int FindSpawnAnimationData() { var start = 0; - var end = 1; - for (var i = 0; i < mMyBase.Animations.Count; i++) + for (var i = 0; i < _myBase.Animations.Count; i++) { - end = mMyBase.Animations[i].SpawnRange; - if (mQuantity >= start && mQuantity < end) + var end = _myBase.Animations[i].SpawnRange; + if (_quantity >= start && _quantity < end) { return i; } @@ -148,215 +159,211 @@ private int FindSpawnAnimationData() start = end; } - //If reaches maximum and the developer(s) have fucked up the animation ranges on each spawn of projectiles somewhere, just assign it to the last animation state. - return mMyBase.Animations.Count - 1; + // If no suitable animation is found (e.g., developer(s) fucked up the animation ranges), default to the last animation. + // This serves as a fallback to prevent crashes or undefined behavior in case of misconfiguration. + return _myBase.Animations.Count - 1; } + /// + /// Adds projectile spawns based on predefined spawn locations and directions. + /// private void AddProjectileSpawns() { var spawn = FindSpawnAnimationData(); - var animBase = AnimationBase.Get(mMyBase.Animations[spawn].AnimationId); + var animBase = AnimationBase.Get(_myBase.Animations[spawn].AnimationId); + // Iterate over all possible spawn locations within the defined width and height for (var x = 0; x < ProjectileBase.SPAWN_LOCATIONS_WIDTH; x++) { for (var y = 0; y < ProjectileBase.SPAWN_LOCATIONS_WIDTH; y++) { + // Iterate over all possible directions a projectile can be spawned in for (var d = 0; d < ProjectileBase.MAX_PROJECTILE_DIRECTIONS; d++) { - if (mMyBase.SpawnLocations[x, y].Directions[d] == true) + // Check if the current direction is enabled for spawning at this location + // and if the maximum number of spawned projectiles has not been reached + if (_myBase.SpawnLocations[x, y].Directions[d] == true) { + // Calculate the spawn position and direction for the new projectile var s = new ProjectileSpawns( - FindProjectileRotationDir(Dir, (Direction)d), - (byte)(X + FindProjectileRotationX(Dir, x - 2, y - 2)), - (byte)(Y + FindProjectileRotationY(Dir, x - 2, y - 2)), Z, MapId, animBase, - mMyBase.Animations[spawn].AutoRotate, mMyBase, this + FindProjectileRotationDir(Dir, (Direction)d), // Calculate the rotation direction + (byte)(X + FindProjectileRotation(Dir, x - 2, y - 2, true)), + (byte)(Y + FindProjectileRotation(Dir, x - 2, y - 2, false)), Z, MapId, animBase, + _myBase.Animations[spawn].AutoRotate, _myBase, this ); - Spawns[mSpawnedAmount] = s; - mCurrentCoords.Add(s, (s.SpawnX, s.SpawnY)); - if (Collided(mSpawnedAmount)) + // Check for collision at the spawn location, mark as dead if collision is detected + _spawns[_spawnedAmount] = s; + if (Collided(_spawnedAmount)) { - TryRemoveSpawn(mSpawnedAmount); - mSpawnCount--; + TryRemoveSpawn(_spawnedAmount); + _spawnCount--; } - mSpawnedAmount++; - mSpawnCount++; + // Add the new spawn to the array and increment counters + _spawnedAmount++; + _spawnCount++; } } } } - mQuantity++; - mSpawnTime = Timing.Global.Milliseconds + mMyBase.Delay; + // Increment the quantity of projectiles spawned and update the spawn time based on the delay + _quantity++; + _spawnTime = Timing.Global.Milliseconds + _myBase.Delay; } - private static int FindProjectileRotationX(Direction direction, int x, int y) + /// + /// Finds the projectile rotation value based on the direction, position and the axis. + /// + /// The direction of the projectile. + /// The x-coordinate value. + /// The y-coordinate value. + /// Determines if the calculation is for the X axis (true) or Y axis (false). + /// The rotation value for the specified axis based on the direction. + private static int FindProjectileRotation(Direction direction, int x, int y, bool isXAxis) { - switch (direction) + if (isXAxis) { - case Direction.Up: - return x; - case Direction.Down: - return -x; - case Direction.Left: - case Direction.UpLeft: - case Direction.DownLeft: - return y; - case Direction.Right: - case Direction.UpRight: - case Direction.DownRight: - return -y; - default: - return x; + return direction switch + { + Direction.Up => x, + Direction.Down => -x, + Direction.Left or Direction.UpLeft or Direction.DownLeft => y, + Direction.Right or Direction.UpRight or Direction.DownRight => -y, + _ => x, + }; } - } - - private static int FindProjectileRotationY(Direction direction, int x, int y) - { - switch (direction) + else { - case Direction.Up: - return y; - case Direction.Down: - return -y; - case Direction.Left: - case Direction.UpLeft: - case Direction.DownLeft: - return -x; - case Direction.Right: - case Direction.UpRight: - case Direction.DownRight: - return x; - default: - return y; + return direction switch + { + Direction.Up => y, + Direction.Down => -y, + Direction.Left or Direction.UpLeft or Direction.DownLeft => -x, + Direction.Right or Direction.UpRight or Direction.DownRight => x, + _ => y, + }; } } private static Direction FindProjectileRotationDir(Direction entityDir, Direction projectionDir) => (Direction)ProjectileBase.ProjectileRotationDir[(int)entityDir * ProjectileBase.MAX_PROJECTILE_DIRECTIONS + (int)projectionDir]; - private static float GetRangeX(Direction direction, float range) + /// + /// Calculates the projectile range based on the given direction, range, and axis. + /// + /// The direction of the projectile. + /// The range of the projectile. + /// A boolean value indicating whether the axis is the X-axis. + /// The calculated range value. + private static float GetRange(Direction direction, float range, bool isXAxis) { - switch (direction) + if (isXAxis) { - case Direction.Left: - case Direction.UpLeft: - case Direction.DownLeft: - return -range; - case Direction.Right: - case Direction.UpRight: - case Direction.DownRight: - return range; - case Direction.Up: - case Direction.Down: - default: - return 0; + return direction switch + { + Direction.Left or Direction.UpLeft or Direction.DownLeft => -range, + Direction.Right or Direction.UpRight or Direction.DownRight => range, + _ => 0, + }; } - } - - private static float GetRangeY(Direction direction, float range) - { - switch (direction) + else { - case Direction.Up: - case Direction.UpLeft: - case Direction.UpRight: - return -range; - case Direction.Down: - case Direction.DownLeft: - case Direction.DownRight: - return range; - case Direction.Left: - case Direction.Right: - default: - return 0; + return direction switch + { + Direction.Up or Direction.UpLeft or Direction.UpRight => -range, + Direction.Down or Direction.DownLeft or Direction.DownRight => range, + _ => 0, + }; } } /// - /// Gets the displacement of the projectile during projection + /// Gets the displacement of the projectile during projection /// /// The displacement from the co-ordinates if placed on a Options.TileHeight grid. private float GetDisplacement(long spawnTime) { var elapsedTime = Timing.Global.Milliseconds - spawnTime; - var displacementPercent = elapsedTime / (float) mMyBase.Speed; + var displacementPercent = elapsedTime / (float)_myBase.Speed; + var calculatedDisplacement = displacementPercent * Options.TileHeight * _myBase.Range; - return displacementPercent * Options.TileHeight * mMyBase.Range; + // Ensure displacement does not exceed the maximum range of the projectile + var maxDisplacement = Options.TileHeight * _myBase.Range; + return Math.Min(calculatedDisplacement, maxDisplacement); } /// - /// Overwrite updating the offsets for projectile movement. + /// Overwrite updating the offsets for projectile movement. /// public override bool Update() { - if (mMyBase == null) + if (_myBase == null) { return false; } - lock (mLock) + lock (_lock) { - var tmpI = -1; var map = MapId; var y = Y; - if (!mDisposing && mQuantity < mMyBase.Quantity && mSpawnTime < Timing.Global.Milliseconds) + if (!_isDisposing && _quantity < _myBase.Quantity && _spawnTime < Timing.Global.Milliseconds) { AddProjectileSpawns(); } if (IsMoving) { - for (var s = 0; s < mSpawnedAmount; s++) + for (var s = 0; s < _spawnedAmount; s++) { - if (Spawns[s] != null && Maps.MapInstance.Get(Spawns[s].SpawnMapId) != null) + if (_spawns[s] != null && Maps.MapInstance.Get(_spawns[s].SpawnMapId) != null) { - if (TargetId != Guid.Empty && TargetId != mOwner && Globals.Entities.ContainsKey(TargetId) && (mMyBase.HomingBehavior || mMyBase.DirectShotBehavior)) + if (_targetId != Guid.Empty && _targetId != _owner && Globals.Entities.ContainsKey(_targetId) && (_myBase.HomingBehavior || _myBase.DirectShotBehavior)) { - var target = Globals.Entities[TargetId]; - mLastTargetX = target.X; - mLastTargetY = target.Y; - mLastTargetMapId = target.MapId; + var target = Globals.Entities[_targetId]; + _lastTargetX = target.X; + _lastTargetY = target.Y; + _lastTargetMapId = target.MapId; - Spawns[s].OffsetX = GetProjectileLerping(Spawns[s], true); - Spawns[s].OffsetY = GetProjectileLerping(Spawns[s], false); - SetProjectileRotation(Spawns[s]); + _spawns[s].OffsetX = GetProjectileLerping(_spawns[s], true); + _spawns[s].OffsetY = GetProjectileLerping(_spawns[s], false); + SetProjectileRotation(_spawns[s]); - if (mMyBase.DirectShotBehavior) + if (_myBase.DirectShotBehavior) { - TargetId = Guid.Empty; + _targetId = Guid.Empty; } } - else if(mLastTargetX != -1 && mLastTargetY != -1) + else if (_lastTargetX != -1 && _lastTargetY != -1) { - Spawns[s].OffsetX = GetProjectileLerping(Spawns[s], true); - Spawns[s].OffsetY = GetProjectileLerping(Spawns[s], false); - SetProjectileRotation(Spawns[s]); + _spawns[s].OffsetX = GetProjectileLerping(_spawns[s], true); + _spawns[s].OffsetY = GetProjectileLerping(_spawns[s], false); + SetProjectileRotation(_spawns[s]); } else { - Spawns[s].OffsetX = GetRangeX(Spawns[s].Dir, GetDisplacement(Spawns[s].SpawnTime)); - Spawns[s].OffsetY = GetRangeY(Spawns[s].Dir, GetDisplacement(Spawns[s].SpawnTime)); - Spawns[s].Anim.SetRotation(false); + _spawns[s].OffsetX = GetRange(_spawns[s].Dir, GetDisplacement(_spawns[s].SpawnTime), true); + _spawns[s].OffsetY = GetRange(_spawns[s].Dir, GetDisplacement(_spawns[s].SpawnTime), false); + _spawns[s].Anim.SetRotation(false); } - Spawns[s] + _spawns[s] .Anim.SetPosition( - Maps.MapInstance.Get(Spawns[s].SpawnMapId).GetX() + - Spawns[s].SpawnX * Options.TileWidth + - Spawns[s].OffsetX + + Maps.MapInstance.Get(_spawns[s].SpawnMapId).GetX() + + _spawns[s].SpawnX * Options.TileWidth + + _spawns[s].OffsetX + Options.TileWidth / 2, - Maps.MapInstance.Get(Spawns[s].SpawnMapId).GetY() + - Spawns[s].SpawnY * Options.TileHeight + - Spawns[s].OffsetY + + Maps.MapInstance.Get(_spawns[s].SpawnMapId).GetY() + + _spawns[s].SpawnY * Options.TileHeight + + _spawns[s].OffsetY + Options.TileHeight / 2, X, Y, MapId, - Spawns[s].AutoRotate ? Spawns[s].Dir : Direction.Up, - Spawns[s].Z + _spawns[s].AutoRotate ? _spawns[s].Dir : Direction.Up, + _spawns[s].Z ); - Spawns[s].Anim.Update(); + _spawns[s].Anim.Update(); } } } @@ -368,15 +375,15 @@ public override bool Update() } /// - /// Returns the distance of the projectile from the target in tiles in the X axis. + /// Calculates the offset for the projectile spawn position based on the target map and spawn location. /// - /// The spawn of the projectile. - /// The distance in tiles from the target in the X axis. - private float GetProjectileX(ProjectileSpawns spawn) + /// A boolean value indicating whether the calculation should be done horizontally (X-axis) or vertically (Y-axis). + /// The projectile spawn information. + /// The calculated offset value. + private float GetProjectileOffset(bool isXAxis, ProjectileSpawns spawn) { - if (mLastTargetMapId != Guid.Empty && mLastTargetMapId != spawn.SpawnMapId) + if (_lastTargetMapId != Guid.Empty && _lastTargetMapId != spawn.SpawnMapId && Maps.MapInstance.TryGet(spawn.SpawnMapId, out var map)) { - var map = Maps.MapInstance.Get(spawn.SpawnMapId); for (var y = map.GridY - 1; y <= map.GridY + 1; y++) { for (var x = map.GridX - 1; x <= map.GridX + 1; x++) @@ -386,231 +393,181 @@ private float GetProjectileX(ProjectileSpawns spawn) continue; } - if (Globals.MapGrid[x, y] != Guid.Empty && Globals.MapGrid[x, y] == mLastTargetMapId) + if (Globals.MapGrid[x, y] != _lastTargetMapId || Globals.MapGrid[x, y] == Guid.Empty) + { + continue; + } + + if (isXAxis) // Horizontal (X) calculation { var leftSide = x == map.GridX - 1; var rightSide = x == map.GridX + 1; if (leftSide) { - return mLastTargetX - Options.MapWidth - spawn.SpawnX; + return _lastTargetX - Options.MapWidth - spawn.SpawnX; } if (rightSide) { - return mLastTargetX + Options.MapWidth - spawn.SpawnX; + return _lastTargetX + Options.MapWidth - spawn.SpawnX; } } - } - } - } - - return mLastTargetX - spawn.SpawnX; - } - - /// - /// Returns the distance of the projectile from the target in tiles in the Y axis. - /// - /// The spawn of the projectile. - /// The distance in tiles from the target in the Y axis. - private float GetProjectileY(ProjectileSpawns spawn) - { - if (mLastTargetMapId != Guid.Empty && mLastTargetMapId != spawn.SpawnMapId) - { - var map = Maps.MapInstance.Get(spawn.SpawnMapId); - for (var y = map.GridY - 1; y <= map.GridY + 1; y++) - { - for (var x = map.GridX - 1; x <= map.GridX + 1; x++) - { - if (x < 0 || x >= Globals.MapGrid.GetLength(0) || y < 0 || y >= Globals.MapGrid.GetLength(1)) - { - continue; - } - - if (Globals.MapGrid[x, y] != Guid.Empty && Globals.MapGrid[x, y] == mLastTargetMapId) + else // Vertical (Y) calculation { - var upSide = y == map.GridY + 1; - var downSide = y == map.GridY - 1; + var topSide = y == map.GridY + 1; + var bottomSide = y == map.GridY - 1; - if (upSide) + if (topSide) { - return mLastTargetY + Options.MapHeight - spawn.SpawnY; + return _lastTargetY + Options.MapHeight - spawn.SpawnY; } - if (downSide) + if (bottomSide) { - return mLastTargetY - Options.MapHeight - spawn.SpawnY; + return _lastTargetY - Options.MapHeight - spawn.SpawnY; } } } } } - return mLastTargetY - spawn.SpawnY; + return isXAxis ? _lastTargetX - spawn.SpawnX : _lastTargetY - spawn.SpawnY; } + /// + /// Calculates the interpolated (lerped) position value for a projectile along the X or Y axis. + /// + /// The spawn information of the projectile, including its initial position and spawn time. + /// True to calculate the position along the X axis, false for the Y axis. + /// The interpolated position value for the projectile on the specified axis, taking into account the projectile's travel direction, speed, and elapsed time since spawning. + /// + /// This method determines the projectile's current position by interpolating between its initial offset and its desired position at the current time. + /// It calculates the direction and magnitude of the projectile's movement, normalizes this to obtain a unit vector in the direction of movement, + /// and then applies linear interpolation based on the elapsed time since the projectile was spawned. + /// The interpolation factor is clamped to ensure that the projectile does not overshoot its target position. + /// private float GetProjectileLerping(ProjectileSpawns spawn, bool isXAxis) { - var directionX = GetProjectileX(spawn); - var directionY = GetProjectileY(spawn); - var valueToLerp = isXAxis ? directionX : directionY; - - var length = (float)Math.Sqrt(directionX * directionX + directionY * directionY); - valueToLerp /= length; + var (directionX, directionY) = (GetProjectileOffset(true, spawn), GetProjectileOffset(false, spawn)); + var distance = MathF.Sqrt(directionX * directionX + directionY * directionY); + if (distance == 0) return 0; - var lerpFactor = 0.1f; + var valueToLerp = (isXAxis ? directionX : directionY) / distance; var offset = isXAxis ? spawn.OffsetX : spawn.OffsetY; - var desiredValue = GetDisplacement(spawn.SpawnTime) * valueToLerp; + var desiredValue = GetDisplacement(spawn.SpawnTime + Options.Instance.Processing.ProjectileUpdateInterval) * valueToLerp; + var totalDuration = (float)_myBase.Range * (_myBase.Speed / Options.TileHeight); + var elapsedTime = Timing.Global.Milliseconds - spawn.SpawnTime; + var lerpFactor = Utilities.MathHelper.Clamp(elapsedTime / totalDuration, 0f, 1f); - return offset + (desiredValue - offset) * lerpFactor; + // Dynamically calculated lerp factor + return (1 - lerpFactor) * offset + lerpFactor * desiredValue; } + /// + /// Sets the rotation of a projectile based on its spawn information. + /// + /// The spawn information of the projectile, including its current animation state and position. + /// + /// This method calculates the angle between the projectile's current position and its target, + /// converting the angle from radians to degrees and adjusting it by 90 degrees to align with the game's coordinate system. + /// The calculated angle is then applied to the projectile's animation state to visually orient the projectile towards its target. + /// private void SetProjectileRotation(ProjectileSpawns spawn) { - var directionX = GetProjectileX(spawn); - var directionY = GetProjectileY(spawn); + var directionX = GetProjectileOffset(true, spawn); + var directionY = GetProjectileOffset(false, spawn); var angle = (float)(Math.Atan2(directionY, directionX) * (180.0 / Math.PI) + 90); spawn.Anim.SetRotation(angle); } + /// + /// Checks for collision of the projectile with other entities or map boundaries. + /// public void CheckForCollision() { - if (mSpawnCount != 0 || mQuantity < mMyBase.Quantity) + if (_spawnCount != 0 || _quantity <= _myBase.Quantity) { - for (var i = 0; i < mSpawnedAmount; i++) + for (var i = 0; i < _spawnedAmount && i < _spawns.Length; i++) // Ensure i is within _spawns bounds { - if (Spawns[i] != null && Timing.Global.Milliseconds > Spawns[i].TransmittionTimer) + if (_spawns[i] != null && Timing.Global.Milliseconds > _spawns[i].TransmissionTimer) { - var spawnMap = Maps.MapInstance.Get(Spawns[i].MapId); - if (spawnMap != null) + if (Maps.MapInstance.TryGet(_spawns[i].MapId, out var spawnMap)) { - var newx = Spawns[i].X + (int)GetRangeX(Spawns[i].Dir, 1); - var newy = Spawns[i].Y + (int)GetRangeY(Spawns[i].Dir, 1); - - if (mMyBase.HomingBehavior || mMyBase.DirectShotBehavior) + float newx = _spawns[i].X; + float newy = _spawns[i].Y; + + if (_myBase.HomingBehavior || _myBase.DirectShotBehavior) { - if (TargetId != Guid.Empty && TargetId != mOwner || mLastTargetX != -1 && mLastTargetY != -1 && mLastTargetMapId != Guid.Empty) + if (_targetId != Guid.Empty && _targetId != _owner || _lastTargetX != -1 && _lastTargetY != -1 && _lastTargetMapId != Guid.Empty) { - float deltaX = GetProjectileX(Spawns[i]); - float deltaY = GetProjectileY(Spawns[i]); + float deltaX = GetProjectileOffset(true, _spawns[i]); + float deltaY = GetProjectileOffset(false, _spawns[i]); float distance = MathF.Sqrt(deltaX * deltaX + deltaY * deltaY); float xFactor = deltaX / distance; float yFactor = deltaY / distance; - - mCurrentCoords[Spawns[i]] = (mCurrentCoords[Spawns[i]].Item1 + xFactor, mCurrentCoords[Spawns[i]].Item2 + yFactor); - newx = (int)Math.Round(mCurrentCoords[Spawns[i]].Item1); - newy = (int)Math.Round(mCurrentCoords[Spawns[i]].Item2); + newx += xFactor; + newy += yFactor; } } - - var newMapId = Spawns[i].MapId; - var killSpawn = false; - - Spawns[i].Distance++; - - if (newx < 0) + else { - if (Maps.MapInstance.Get(spawnMap.Left) != null) - { - newMapId = spawnMap.Left; - newx = Options.MapWidth - 1; - mCurrentCoords[Spawns[i]] = (newx, mCurrentCoords[Spawns[i]].Item2); - } - else - { - killSpawn = true; - } + newx += GetRange(_spawns[i].Dir, 1, true); + newy += GetRange(_spawns[i].Dir, 1, false); } - if (newx > Options.MapWidth - 1) - { - if (Maps.MapInstance.Get(spawnMap.Right) != null) - { - newMapId = spawnMap.Right; - newx = 0; - mCurrentCoords[Spawns[i]] = (newx, mCurrentCoords[Spawns[i]].Item2); - } - else - { - killSpawn = true; - } - } + AdjustPositionOnMapBoundaries(ref newx, ref newy, ref spawnMap); - if (newy < 0) - { - if (Maps.MapInstance.Get(spawnMap.Up) != null) - { - newMapId = spawnMap.Up; - newy = Options.MapHeight - 1; - mCurrentCoords[Spawns[i]] = (mCurrentCoords[Spawns[i]].Item1, newy); - } - else - { - killSpawn = true; - } - } + _spawns[i].X = (int)newx; + _spawns[i].Y = (int)newy; + _spawns[i].MapId = spawnMap.Id; + _spawns[i].Distance++; + _spawns[i].TransmissionTimer = Timing.Global.MillisecondsOffset + (long)(_myBase.Speed / (float)_myBase.Range); - if (newy > Options.MapHeight - 1) - { - if (Maps.MapInstance.Get(spawnMap.Down) != null) - { - newMapId = spawnMap.Down; - newy = 0; - mCurrentCoords[Spawns[i]] = (mCurrentCoords[Spawns[i]].Item1, newy); - } - else - { - killSpawn = true; - } - } + var killSpawn = Collided(i) || _spawns[i].Distance >= _myBase.Range || + newx < 0 || newx >= Options.MapWidth || newy < 0 || + newy >= Options.MapHeight; + // Check for map boundaries and remove the spawn if it goes out of bounds. if (killSpawn) { TryRemoveSpawn(i); - mSpawnCount--; + _spawnCount--; continue; } - Spawns[i].X = newx; - Spawns[i].Y = newy; - Spawns[i].MapId = newMapId; - var newMap = Maps.MapInstance.Get(newMapId); - - //Check for Z-Dimension - if (newMap.Attributes[Spawns[i].X, Spawns[i].Y] != null) + // Check for Z-Dimension + if (!killSpawn && spawnMap.Attributes != null && + _spawns[i].X >= 0 && _spawns[i].Y >= 0 && _spawns[i].X < spawnMap.Attributes.GetLength(0) && + _spawns[i].Y < spawnMap.Attributes.GetLength(1)) { - if (newMap.Attributes[Spawns[i].X, Spawns[i].Y].Type == MapAttribute.ZDimension) + var attribute = spawnMap.Attributes[_spawns[i].X, _spawns[i].Y]; + + if (attribute != null && attribute.Type == MapAttribute.ZDimension) { - if (((MapZDimensionAttribute) newMap.Attributes[Spawns[i].X, Spawns[i].Y]) - .GatewayTo > - 0) + var zDimensionAttribute = (MapZDimensionAttribute)attribute; + + // If the Z dimension attribute specifies a blocked level that matches the projectile's current Z level, + // mark the projectile for destruction. + if (zDimensionAttribute.BlockedLevel > 0 && _spawns[i].Z == zDimensionAttribute.BlockedLevel - 1) { - Spawns[i].Z = - ((MapZDimensionAttribute) newMap.Attributes[Spawns[i].X, Spawns[i].Y]) - .GatewayTo - - 1; + killSpawn = true; } - } - } - if (killSpawn == false) - { - killSpawn = Collided(i); - } - - Spawns[i].TransmittionTimer = Timing.Global.Milliseconds + - (long) (mMyBase.Speed / (float) mMyBase.Range); - - if (Spawns[i].Distance >= mMyBase.Range) - { - killSpawn = true; + // If the Z dimension attribute specifies a gateway to another level, + // adjust the projectile's Z level to the specified gateway level. + if (zDimensionAttribute.GatewayTo > 0) + { + _spawns[i].Z = zDimensionAttribute.GatewayTo - 1; + } + } } if (killSpawn) { TryRemoveSpawn(i); - mSpawnCount--; + _spawnCount--; } } } @@ -622,57 +579,162 @@ public void CheckForCollision() } } + /// + /// Adjusts the position of a projectile on the map boundaries, potentially changing its map instance. + /// + /// The new x-coordinate of the projectile. + /// The new y-coordinate of the projectile. + /// The map instance where the projectile is spawned. This may be updated if the projectile crosses map boundaries. + /// + /// This method checks if the projectile crosses the boundaries of its current map. If it does, it attempts to move the projectile + /// to the adjacent map in the direction it crossed the boundary. This includes handling corner cases where the projectile crosses + /// at the corners of the map, potentially moving it diagonally to a new map. The new position of the projectile is adjusted to + /// visually reflect its entry point into the adjacent map. + /// + private static void AdjustPositionOnMapBoundaries(ref float newx, ref float newy, ref Maps.MapInstance spawnMap) + { + int MapWidth = Options.MapWidth; // The width of the map. + int MapHeight = Options.MapHeight; // The height of the map. + + // Determine if the projectile crosses any of the map boundaries. + bool crossesLeftBoundary = MathF.Floor(newx) < 0; + bool crossesRightBoundary = MathF.Ceiling(newx) > MapWidth - 1; + bool crossesTopBoundary = MathF.Floor(newy) < 0; + bool crossesBottomBoundary = MathF.Ceiling(newy) > MapHeight - 1; + + // Handle corner cases: crossing boundaries at the corners of the map. + if (crossesLeftBoundary && crossesTopBoundary) + { + // Move to the map diagonally up-left if possible. + if (Maps.MapInstance.TryGet(spawnMap.Up, out var upMap) && + Maps.MapInstance.TryGet(upMap.Left, out var upLeftMap)) + { + spawnMap = upLeftMap; + newx = MapWidth - 1; + newy = MapHeight - 1; + } + } + else if (crossesRightBoundary && crossesTopBoundary) + { + // Move to the map diagonally up-right if possible. + if (Maps.MapInstance.TryGet(spawnMap.Up, out var upMap) && + Maps.MapInstance.TryGet(upMap.Right, out var upRightMap)) + { + spawnMap = upRightMap; + newx = 0; + newy = MapHeight - 1; + } + } + else if (crossesLeftBoundary && crossesBottomBoundary) + { + // Move to the map diagonally down-left if possible. + if (Maps.MapInstance.TryGet(spawnMap.Down, out var downMap) && + Maps.MapInstance.TryGet(downMap.Left, out var downLeftMap)) + { + spawnMap = downLeftMap; + newx = MapWidth - 1; + newy = 0; + } + } + else if (crossesRightBoundary && crossesBottomBoundary) + { + // Move to the map diagonally down-right if possible. + if (Maps.MapInstance.TryGet(spawnMap.Down, out var downMap) && + Maps.MapInstance.TryGet(downMap.Right, out var downRightMap)) + { + spawnMap = downRightMap; + newx = 0; + newy = 0; + } + } + // Handle cases where the projectile crosses one boundary. + else if (crossesLeftBoundary) + { + // Move to the map on the left if possible. + if (Maps.MapInstance.TryGet(spawnMap.Left, out var leftMap)) + { + spawnMap = leftMap; + newx = MapWidth - 1; + } + } + else if (crossesRightBoundary) + { + // Move to the map on the right if possible. + if (Maps.MapInstance.TryGet(spawnMap.Right, out var rightMap)) + { + spawnMap = rightMap; + newx = 0; + } + } + else if (crossesTopBoundary) + { + // Move to the map above if possible. + if (Maps.MapInstance.TryGet(spawnMap.Up, out var upMap)) + { + spawnMap = upMap; + newy = MapHeight - 1; + } + } + else if (crossesBottomBoundary) + { + // Move to the map below if possible. + if (Maps.MapInstance.TryGet(spawnMap.Down, out var downMap)) + { + spawnMap = downMap; + newy = 0; + } + } + } + + /// + /// Determines if a projectile spawn has collided with an entity, resource, or map block. + /// + /// The index of the projectile spawn in the _spawns array. + /// True if the projectile spawn has collided and should be destroyed; otherwise, false. private bool Collided(int i) { - var killSpawn = false; - IEntity? blockedBy = default; - var spawn = Spawns[i]; + var killSpawn = false; // Flag to determine if the spawn should be killed due to collision + IEntity? blockedBy = default; // Entity, resource, or map block that the projectile has potentially collided with + var spawn = _spawns[i]; // Current projectile spawn being checked for collision + + // Check if the tile at the projectile's location is blocked var tileBlocked = Globals.Me.IsTileBlocked( - new Point(spawn.X, spawn.Y), - Z, - Spawns[i].MapId, - ref blockedBy, - spawn.ProjectileBase.IgnoreActiveResources, - spawn.ProjectileBase.IgnoreExhaustedResources, - true, - true + new Point(spawn.X, spawn.Y), // Projectile spawn location + Z, // Z-level of the projectile + spawn.MapId, // ID of the map where the projectile is located + ref blockedBy, // Reference to the entity blocking the projectile, if any + spawn.ProjectileBase.IgnoreActiveResources, // Whether active resources should be ignored + spawn.ProjectileBase.IgnoreExhaustedResources, // Whether exhausted resources should be ignored + true, // Always check for entities + true // Always check for map blocks ); switch (tileBlocked) { - case -1: + case -1: // No collision detected return killSpawn; - case -6 when - blockedBy != default && - blockedBy.Id != mOwner && - Globals.Entities.ContainsKey(blockedBy.Id): - { - if (blockedBy is Resource) + case -6: // Collision with an entity other than the owner + if (blockedBy != default && blockedBy.Id != _owner && Globals.Entities.ContainsKey(blockedBy.Id)) { - killSpawn = true; + if (blockedBy is Resource) + { + killSpawn = true; + } } - break; - } - case -2: - { - if (!Spawns[i].ProjectileBase.IgnoreMapBlocks) + case -2: // Collision with a map block + if (!spawn.ProjectileBase.IgnoreMapBlocks) { killSpawn = true; } - break; - } - case -3: - { - if (!Spawns[i].ProjectileBase.IgnoreZDimension) + case -3: // Collision with a Z-dimension block + if (!spawn.ProjectileBase.IgnoreZDimension) { killSpawn = true; } - break; - } - case -5: + case -5: // Collision with an unspecified block type or out of map bounds killSpawn = true; break; } @@ -681,7 +743,7 @@ private bool Collided(int i) } /// - /// Rendering all of the individual projectiles from a singular spawn to a map. + /// Rendering all of the individual projectiles from a singular spawn to a map. /// public override void Draw() { @@ -693,7 +755,7 @@ public override void Draw() public void SpawnDead(int spawnIndex) { - if (spawnIndex < mSpawnedAmount && Spawns[spawnIndex] != null) + if (spawnIndex < _spawnedAmount && _spawns[spawnIndex] != null) { TryRemoveSpawn(spawnIndex); } @@ -701,11 +763,10 @@ public void SpawnDead(int spawnIndex) private void TryRemoveSpawn(int spawnIndex) { - if (spawnIndex < mSpawnedAmount && Spawns[spawnIndex] != null) + if (spawnIndex < _spawnedAmount && _spawns[spawnIndex] != null) { - if (mCurrentCoords.ContainsKey(Spawns[spawnIndex])) mCurrentCoords.Remove(Spawns[spawnIndex]); - Spawns[spawnIndex].Dispose(); - Spawns[spawnIndex] = null; + _spawns[spawnIndex].Dispose(); + _spawns[spawnIndex] = null; } } diff --git a/Intersect.Client/Entities/Projectiles/ProjectileSpawns.cs b/Intersect.Client/Entities/Projectiles/ProjectileSpawns.cs index 9d9cf0709d..221fa1f4a6 100644 --- a/Intersect.Client/Entities/Projectiles/ProjectileSpawns.cs +++ b/Intersect.Client/Entities/Projectiles/ProjectileSpawns.cs @@ -1,5 +1,4 @@ -using System; -using Intersect.Enums; +using Intersect.Enums; using Intersect.GameObjects; using Intersect.Utilities; @@ -34,7 +33,7 @@ public partial class ProjectileSpawns public int SpawnY; - public long TransmittionTimer = Timing.Global.Milliseconds; + public long TransmissionTimer = Timing.Global.Milliseconds; public int X; @@ -65,8 +64,7 @@ Entity parent Anim = new Animation(animBase, true, autoRotate, Z, parent); AutoRotate = autoRotate; ProjectileBase = projectileBase; - TransmittionTimer = Timing.Global.Milliseconds + - (long) ((float) ProjectileBase.Speed / (float) ProjectileBase.Range); + TransmissionTimer = Timing.Global.Milliseconds + (ProjectileBase.Speed / ProjectileBase.Range); } public void Dispose() diff --git a/Intersect.Server.Core/Entities/Projectile.cs b/Intersect.Server.Core/Entities/Projectile.cs index c7bb4b3e6d..0be68ae351 100644 --- a/Intersect.Server.Core/Entities/Projectile.cs +++ b/Intersect.Server.Core/Entities/Projectile.cs @@ -20,15 +20,15 @@ public partial class Projectile : Entity public ItemBase Item; - private int mQuantity; + private int _quantity; - private int mSpawnCount; + private int _spawnCount; - private int mSpawnedAmount; + private int _spawnedAmount; - private long mSpawnTime; + private long _spawnTime; - private int mTotalSpawns; + private int _totalSpawns; public Entity Owner; @@ -37,13 +37,13 @@ public partial class Projectile : Entity public SpellBase Spell; - public Entity Target; + public new Entity Target; - private int mLastTargetX = -1; + private int _lastTargetX = -1; - private int mLastTargetY = -1; + private int _lastTargetY = -1; - private Guid mLastTargetMapId = Guid.Empty; + private Guid _lastTargetMapId = Guid.Empty; public Projectile( Entity owner, @@ -83,35 +83,47 @@ Entity target { if (Base.SpawnLocations[x, y].Directions[d] == true) { - mTotalSpawns++; + _totalSpawns++; } } } } - mTotalSpawns *= Base.Quantity; - Spawns = new ProjectileSpawn[mTotalSpawns]; + _totalSpawns *= Base.Quantity; + Spawns = new ProjectileSpawn[_totalSpawns]; } - private void AddProjectileSpawns(List> spawnDeaths) + /// + /// Adds projectile spawns based on predefined spawn locations and directions. + /// + private void AddProjectileSpawns() { + // Iterate over all possible spawn locations within the defined width and height for (byte x = 0; x < ProjectileBase.SPAWN_LOCATIONS_WIDTH; x++) { for (byte y = 0; y < ProjectileBase.SPAWN_LOCATIONS_HEIGHT; y++) { + // Iterate over all possible directions a projectile can be spawned in for (byte d = 0; d < ProjectileBase.MAX_PROJECTILE_DIRECTIONS; d++) { - if (Base.SpawnLocations[x, y].Directions[d] == true && mSpawnedAmount < Spawns.Length) + // Check if the current direction is enabled for spawning at this location + // and if the maximum number of spawned projectiles has not been reached + if (Base.SpawnLocations[x, y].Directions[d] && _spawnedAmount < Spawns.Length) { + // Calculate the spawn position and direction for the new projectile var s = new ProjectileSpawn( - FindProjectileRotationDir(Dir, (Direction)d), - (byte) (X + FindProjectileRotationX(Dir, x - 2, y - 2)), - (byte) (Y + FindProjectileRotationY(Dir, x - 2, y - 2)), (byte) Z, MapId, MapInstanceId, Base, this + FindProjectileRotationDir(Dir, (Direction)d), // Calculate the rotation direction + (byte)(X + FindProjectileRotation(Dir, x - 2, y - 2, true)), + (byte)(Y + FindProjectileRotation(Dir, x - 2, y - 2, false)), + (byte)Z, MapId, MapInstanceId, Base, this ); - Spawns[mSpawnedAmount] = s; - mSpawnedAmount++; - mSpawnCount++; + // Add the new spawn to the array and increment counters + Spawns[_spawnedAmount] = s; + _spawnedAmount++; + _spawnCount++; + + // Check for collision at the spawn location, mark as dead if collision is detected if (CheckForCollision(s)) { s.Dead = true; @@ -121,49 +133,42 @@ private void AddProjectileSpawns(List> spawnDeaths) } } - mQuantity++; - mSpawnTime = Timing.Global.Milliseconds + Base.Delay; + // Increment the quantity of projectiles spawned and update the spawn time based on the delay + _quantity++; + _spawnTime = Timing.Global.Milliseconds + Base.Delay; } - private static int FindProjectileRotationX(Direction direction, int x, int y) + /// + /// Finds the projectile rotation value based on the direction, position and the axis. + /// + /// The direction of the projectile. + /// The x-coordinate value. + /// The y-coordinate value. + /// Determines if the calculation is for the X axis (true) or Y axis (false). + /// The rotation value for the specified axis based on the direction. + private static int FindProjectileRotation(Direction direction, int x, int y, bool isXAxis) { - switch (direction) + if (isXAxis) { - case Direction.Up: - return x; - case Direction.Down: - return -x; - case Direction.Left: - case Direction.UpLeft: - case Direction.DownLeft: - return y; - case Direction.Right: - case Direction.UpRight: - case Direction.DownRight: - return -y; - default: - return x; + return direction switch + { + Direction.Up => x, + Direction.Down => -x, + Direction.Left or Direction.UpLeft or Direction.DownLeft => y, + Direction.Right or Direction.UpRight or Direction.DownRight => -y, + _ => x, + }; } - } - - private static int FindProjectileRotationY(Direction direction, int x, int y) - { - switch (direction) + else { - case Direction.Up: - return y; - case Direction.Down: - return -y; - case Direction.Left: - case Direction.UpLeft: - case Direction.DownLeft: - return -x; - case Direction.Right: - case Direction.UpRight: - case Direction.DownRight: - return x; - default: - return y; + return direction switch + { + Direction.Up => y, + Direction.Down => -y, + Direction.Left or Direction.UpLeft or Direction.DownLeft => -x, + Direction.Right or Direction.UpRight or Direction.DownRight => x, + _ => y, + }; } } @@ -172,49 +177,40 @@ private static Direction FindProjectileRotationDir(Direction entityDir, Directio public void Update(List projDeaths, List> spawnDeaths) { - if (mQuantity < Base.Quantity && Timing.Global.Milliseconds > mSpawnTime) + if (_quantity < Base.Quantity && Timing.Global.Milliseconds > _spawnTime) { - AddProjectileSpawns(spawnDeaths); + AddProjectileSpawns(); } ProcessFragments(projDeaths, spawnDeaths); } - private static float GetRangeX(Direction direction, float range) + /// + /// Calculates the projectile range based on the given direction, range, and axis. + /// + /// The direction of the projectile. + /// The range of the projectile. + /// A boolean value indicating whether the axis is the X-axis. + /// The calculated range value. + private static float GetRange(Direction direction, float range, bool isXAxis) { - switch (direction) + if (isXAxis) { - case Direction.Left: - case Direction.UpLeft: - case Direction.DownLeft: - return -range; - case Direction.Right: - case Direction.UpRight: - case Direction.DownRight: - return range; - case Direction.Up: - case Direction.Down: - default: - return 0; + return direction switch + { + Direction.Left or Direction.UpLeft or Direction.DownLeft => -range, + Direction.Right or Direction.UpRight or Direction.DownRight => range, + _ => 0, + }; } - } - - private static float GetRangeY(Direction direction, float range) - { - switch (direction) + else { - case Direction.Up: - case Direction.UpLeft: - case Direction.UpRight: - return -range; - case Direction.Down: - case Direction.DownLeft: - case Direction.DownRight: - return range; - case Direction.Left: - case Direction.Right: - default: - return 0; + return direction switch + { + Direction.Up or Direction.UpLeft or Direction.UpRight => -range, + Direction.Down or Direction.DownLeft or Direction.DownRight => range, + _ => 0, + }; } } @@ -225,14 +221,14 @@ public void ProcessFragments(List projDeaths, List return; } - if (mSpawnCount != 0 || mQuantity < Base.Quantity) + if (_spawnCount != 0 || _quantity < Base.Quantity) { - for (var i = 0; i < mSpawnedAmount; i++) + for (var i = 0; i < _spawnedAmount; i++) { var spawn = Spawns[i]; if (spawn != null) { - while (Timing.Global.Milliseconds > spawn.TransmittionTimer && Spawns[i] != null) + while (Timing.Global.Milliseconds > spawn.TransmissionTimer && Spawns[i] != null) { var x = spawn.X; var y = spawn.Y; @@ -247,11 +243,11 @@ public void ProcessFragments(List projDeaths, List } } - if (killSpawn || spawn.Dead) + if (killSpawn || spawn.Dead || CheckForCollision(spawn)) { spawnDeaths.Add(new KeyValuePair(Id, i)); Spawns[i] = null; - mSpawnCount--; + _spawnCount--; } } } @@ -267,9 +263,14 @@ public void ProcessFragments(List projDeaths, List } } + /// + /// Checks for collision between the projectile with other entities and attributes on the map. + /// + /// The projectile spawn information. + /// True if the projectile collides with an entity or attributes, false otherwise. public bool CheckForCollision(ProjectileSpawn spawn) { - if(spawn == null) + if (spawn == null) { return false; } @@ -278,24 +279,51 @@ public bool CheckForCollision(ProjectileSpawn spawn) //Check Map Entities For Hits var map = MapController.Get(spawn.MapId); - if (Math.Round(spawn.X) < 0 || Math.Round(spawn.X) >= Options.Instance.MapOpts.MapWidth || - Math.Round(spawn.Y) < 0 || Math.Round(spawn.Y) >= Options.Instance.MapOpts.MapHeight) + if (map == null) { return false; } - var attribute = map.Attributes[(int)Math.Round(spawn.X), (int)Math.Round(spawn.Y)]; + + var roundedX = MathF.Round(spawn.X); + var roundedY = MathF.Round(spawn.Y); + + //Checking if the coordinates are within the map boundaries + if (roundedX < 0 || roundedX >= Options.MapWidth || + roundedY < 0 || roundedY >= Options.MapHeight) + { + return false; + } + + GameObjects.Maps.MapAttribute attribute; + + // Before accessing map attributes, check if the coordinates are within the bounds of the map's attribute array. + if (roundedX >= 0 && roundedX < map.Attributes.GetLength(0) && + roundedY >= 0 && roundedY < map.Attributes.GetLength(1)) + { + attribute = map.Attributes[(int)roundedX, (int)roundedY]; + } + else + { + attribute = null; + } if (!killSpawn && attribute != null) { - //Check for Z-Dimension - if (!spawn.ProjectileBase.IgnoreZDimension) + // Check for Z-Dimension + if (!spawn.ProjectileBase.IgnoreZDimension && attribute is MapZDimensionAttribute zDimAttr && zDimAttr != null) { - if (attribute.Type == MapAttribute.ZDimension) + // If the Z dimension attribute specifies a blocked level that matches the projectile's current Z level, + // mark the projectile for destruction. + if (zDimAttr.BlockedLevel > 0 && spawn.Z == zDimAttr.BlockedLevel - 1) { - if (((MapZDimensionAttribute) attribute).GatewayTo > 0) - { - spawn.Z = (byte) (((MapZDimensionAttribute) attribute).GatewayTo - 1); - } + killSpawn = true; + } + + // If the Z dimension attribute specifies a gateway to another level, + // adjust the projectile's Z level to the specified gateway level. + if (zDimAttr.GatewayTo > 0) + { + spawn.Z = (byte)(zDimAttr.GatewayTo - 1); } } @@ -305,9 +333,8 @@ public bool CheckForCollision(ProjectileSpawn spawn) !spawn.Parent.HasGrappled && (spawn.X != Owner.X || spawn.Y != Owner.Y)) { - if (!spawn.ProjectileBase.HomingBehavior && !spawn.ProjectileBase.DirectShotBehavior && - (spawn.Dir <= Direction.Right || spawn.Dir != Direction.None && Options.Instance.MapOpts.EnableDiagonalMovement) - ) + if (!spawn.ProjectileBase.HomingBehavior && !spawn.ProjectileBase.DirectShotBehavior && + (spawn.Dir <= Direction.Right || (spawn.Dir != Direction.None && Options.Instance.MapOpts.EnableDiagonalMovement))) { spawn.Parent.HasGrappled = true; @@ -326,7 +353,7 @@ public bool CheckForCollision(ProjectileSpawn spawn) } if (!spawn.ProjectileBase.IgnoreMapBlocks && - (attribute.Type == MapAttribute.Blocked || attribute.Type == MapAttribute.Animation && ((MapAnimationAttribute)attribute).IsBlock)) + ((attribute.Type == MapAttribute.Blocked) || (attribute.Type == MapAttribute.Animation && ((MapAnimationAttribute)attribute).IsBlock))) { killSpawn = true; } @@ -337,41 +364,39 @@ public bool CheckForCollision(ProjectileSpawn spawn) var entities = mapInstance.GetEntities(); for (var z = 0; z < entities.Count; z++) { - if (entities[z] != null && entities[z] != spawn.Parent.Owner && entities[z].Z == spawn.Z && - (entities[z].X == Math.Round(spawn.X)) && - (entities[z].Y == Math.Round(spawn.Y)) && + var entity = entities[z]; + if (entity != null && entity != spawn.Parent.Owner && entity.Z == spawn.Z && + entity.X == roundedX && entity.Y == roundedY && (spawn.X != Owner.X || spawn.Y != Owner.Y)) { - killSpawn = spawn.HitEntity(entities[z]); + killSpawn = spawn.HitEntity(entity); if (killSpawn && !spawn.ProjectileBase.PierceTarget) { return killSpawn; } } - else + else if (z == entities.Count - 1 && spawn.Distance >= Base.Range) { - if (z == entities.Count - 1) - { - if (spawn.Distance >= Base.Range) - { - killSpawn = true; - } - } + killSpawn = true; } } } - return killSpawn; + return killSpawn || spawn?.Distance >= Base.Range; } - private float GetProjectileX(ProjectileSpawn spawn) + /// + /// Calculates the offset of a projectile based on the target's position and the spawn location. + /// + /// A boolean value indicating whether the calculation should be done horizontally (X-axis) or vertically (Y-axis). + /// The projectile spawn information. + /// The calculated offset value. + private float GetProjectileOffset(bool isXAxis, ProjectileSpawn spawn) { - if (mLastTargetMapId != Guid.Empty && mLastTargetMapId != spawn.MapId) + if (_lastTargetMapId != Guid.Empty && _lastTargetMapId != spawn.MapId && MapController.TryGet(spawn.MapId, out var map)) { - var map = MapController.Get(spawn.MapId); var grid = DbInterface.GetGrid(map.MapGrid); - //loop through surrounding maps for (var y = map.MapGridY - 1; y <= map.MapGridY + 1; y++) { for (var x = map.MapGridX - 1; x <= map.MapGridX + 1; x++) @@ -381,178 +406,221 @@ private float GetProjectileX(ProjectileSpawn spawn) continue; } - if (grid.MapIdGrid[x, y] != Guid.Empty && grid.MapIdGrid[x, y] == mLastTargetMapId) + if (grid.MapIdGrid[x, y] != _lastTargetMapId || grid.MapIdGrid[x, y] == Guid.Empty) + { + continue; + } + + if (isXAxis) // Horizontal (X) calculation { var leftSide = x == map.MapGridX - 1; var rightSide = x == map.MapGridX + 1; if (leftSide) { - return mLastTargetX - Options.MapWidth - spawn.X; + return _lastTargetX - Options.MapWidth - spawn.X; } if (rightSide) { - return mLastTargetX + Options.MapWidth - spawn.X; + return _lastTargetX + Options.MapWidth - spawn.X; } } - } - } - } - - return mLastTargetX - spawn.X; - } - - private float GetProjectileY(ProjectileSpawn spawn) - { - if (mLastTargetMapId != Guid.Empty && mLastTargetMapId != spawn.MapId) - { - var map = MapController.Get(spawn.MapId); - var grid = DbInterface.GetGrid(map.MapGrid); - - //loop through surrounding maps - for (var y = map.MapGridY - 1; y <= map.MapGridY + 1; y++) - { - for (var x = map.MapGridX - 1; x <= map.MapGridX + 1; x++) - { - if (x < 0 || x >= grid.MapIdGrid.GetLength(0) || y < 0 || y >= grid.MapIdGrid.GetLength(1)) - { - continue; - } - - if (grid.MapIdGrid[x, y] != Guid.Empty && grid.MapIdGrid[x, y] == mLastTargetMapId) + else // Vertical (Y) calculation { var topSide = y == map.MapGridY - 1; var bottomSide = y == map.MapGridY + 1; if (topSide) { - return mLastTargetY - Options.MapHeight - spawn.Y; + return _lastTargetY - Options.MapHeight - spawn.Y; } if (bottomSide) { - return mLastTargetY + Options.MapHeight - spawn.Y; + return _lastTargetY + Options.MapHeight - spawn.Y; } } } } } - return mLastTargetY - spawn.Y; + return isXAxis ? _lastTargetX - spawn.X : _lastTargetY - spawn.Y; } + /// + /// Moves the projectile fragment based on the specified spawn information. This method is responsible for + /// updating the position of the projectile, handling homing and direct shot behaviors, and checking for out-of-bounds conditions. + /// + /// The projectile spawn information, including current position, direction, and other relevant details. + /// Indicates whether the fragment should be moved. This can be used to pause movement if needed. Default is true. + /// True if the projectile goes out of bounds after moving, false otherwise. public bool MoveFragment(ProjectileSpawn spawn, bool move = true) { - float newx = spawn.X; - float newy = spawn.Y; - var newMapId = spawn.MapId; - if (move) { + // Increase the distance traveled by the projectile and update its transmission timer. spawn.Distance++; - spawn.TransmittionTimer += (long)(Base.Speed / (float)Base.Range); - + spawn.TransmissionTimer += (long)(Base.Speed / (float)Base.Range); + if (Target != default && Target.Id != Owner.Id && (Base.HomingBehavior || Base.DirectShotBehavior)) { - //homing logic - mLastTargetX = Target.X; - mLastTargetY = Target.Y; - mLastTargetMapId = Target.MapId; - var directionX = GetProjectileX(spawn); - var directionY = GetProjectileY(spawn); - var length = Math.Sqrt(directionX * directionX + directionY * directionY); - - newx += (float)(directionX / length); - newy += (float)(directionY / length); - + // Homing or direct shot logic: Adjusts the projectile's trajectory towards the target. + _lastTargetX = Target.X; + _lastTargetY = Target.Y; + _lastTargetMapId = Target.MapId; + var directionX = GetProjectileOffset(true, spawn); + var directionY = GetProjectileOffset(false, spawn); + var distance = MathF.Sqrt(directionX * directionX + directionY * directionY); + + // Normalize the direction and update the projectile's position. + spawn.X += directionX / distance; + spawn.Y += directionY / distance; + + // For direct shots, reset the target after moving towards it once. if (Base.DirectShotBehavior) { Target = default; } } - else if (mLastTargetX != -1 && mLastTargetY != -1) + else if (_lastTargetX != -1 && _lastTargetY != -1) { - //last target location logic - var directionX = GetProjectileX(spawn); - var directionY = GetProjectileY(spawn); - var length = Math.Sqrt(directionX * directionX + directionY * directionY); - - newx += (float)(directionX / length); - newy += (float)(directionY / length); + // Last known target location logic: Moves the projectile towards the last known position of the target. + var directionX = GetProjectileOffset(true, spawn); + var directionY = GetProjectileOffset(false, spawn); + var distance = MathF.Sqrt(directionX * directionX + directionY * directionY); + + // Normalize the direction and update the projectile's position. + spawn.X += directionX / distance; + spawn.Y += directionY / distance; } else { - // Default logic - newx = spawn.X + GetRangeX(spawn.Dir, 1); - newy = spawn.Y + GetRangeY(spawn.Dir, 1); + // Default movement logic: Moves the projectile in its current direction. + spawn.X += GetRange(spawn.Dir, 1, true); + spawn.Y += GetRange(spawn.Dir, 1, false); } } - var killSpawn = false; + // Adjust the projectile's position if it crosses map boundaries and potentially transitions it to a neighboring map. + AdjustPositionOnMapBoundaries(ref spawn.X, ref spawn.Y, ref spawn); + + // Check for map boundaries and remove the spawn if the projectile has gone out of bounds after moving. + return spawn.X < 0 || spawn.X >= Options.MapWidth || spawn.Y < 0 || spawn.Y >= Options.MapHeight; + } + + /// + /// Adjusts the position of a projectile when it crosses the boundaries of the current map. + /// This method checks if the projectile crosses the map boundaries and, if so, attempts to + /// transition the projectile to the adjacent map in the direction it is moving. The new position + /// on the adjacent map is calculated to maintain the continuity of the projectile's path. + /// + /// The new x-coordinate of the projectile, which may be adjusted if crossing map boundaries. + /// The new y-coordinate of the projectile, which may be adjusted if crossing map boundaries. + /// The projectile spawn information, including the current map ID, which may be updated to a new map. + private static void AdjustPositionOnMapBoundaries(ref float newx, ref float newy, ref ProjectileSpawn spawn) + { + // Retrieve the current map based on the projectile's spawn information. var map = MapController.Get(spawn.MapId); + int MapWidth = Options.MapWidth; + int MapHeight = Options.MapHeight; + + // Determine if the projectile crosses any of the map's boundaries. + bool crossesLeftBoundary = MathF.Floor(newx) < 0; + bool crossesRightBoundary = MathF.Ceiling(newx) > MapWidth - 1; + bool crossesTopBoundary = MathF.Floor(newy) < 0; + bool crossesBottomBoundary = MathF.Ceiling(newy) > MapHeight - 1; - if (Math.Floor(newx) < 0) + // Check for corner cases where the projectile crosses two boundaries. + if (crossesLeftBoundary && crossesTopBoundary) { - var leftMap = MapController.Get(map.Left); - if (leftMap != null) + // Projectile crosses the top-left corner of the map. + if (MapController.TryGet(map.Up, out var upMap) && + MapController.TryGet(upMap.Left, out var upLeftMap)) { - newMapId = leftMap.Id; - newx = Options.MapWidth - 1; + // Transition to the map diagonally up-left and adjust position to bottom-right corner. + spawn.MapId = upLeftMap.Id; + newx = MapWidth - 1; + newy = MapHeight - 1; } - else + } + else if (crossesRightBoundary && crossesTopBoundary) + { + // Projectile crosses the top-right corner of the map. + if (MapController.TryGet(map.Up, out var upMap) && + MapController.TryGet(upMap.Right, out var upRightMap)) { - killSpawn = true; + // Transition to the map diagonally up-right and adjust position to bottom-left corner. + spawn.MapId = upRightMap.Id; + newx = 0; + newy = MapHeight - 1; } } - - if (Math.Ceiling(newx) > Options.MapWidth) + else if (crossesLeftBoundary && crossesBottomBoundary) { - var rightMap = MapController.Get(map.Right); - if (rightMap != null) + // Projectile crosses the bottom-left corner of the map. + if (MapController.TryGet(map.Down, out var downMap) && + MapController.TryGet(downMap.Left, out var downLeftMap)) { - newMapId = rightMap.Id; - newx = 0; + // Transition to the map diagonally down-left and adjust position to top-right corner. + spawn.MapId = downLeftMap.Id; + newx = MapWidth - 1; + newy = 0; } - else + } + else if (crossesRightBoundary && crossesBottomBoundary) + { + // Projectile crosses the bottom-right corner of the map. + if (MapController.TryGet(map.Down, out var downMap) && + MapController.TryGet(downMap.Right, out var downRightMap)) { - killSpawn = true; + // Transition to the map diagonally down-right and adjust position to top-left corner. + spawn.MapId = downRightMap.Id; + newx = 0; + newy = 0; } } - - if (Math.Floor(newy) < 0) + // Check for single boundary crossings and adjust position accordingly. + else if (crossesLeftBoundary) { - var upMap = MapController.Get(map.Up); - if (upMap != null) + // Projectile crosses the left boundary of the map. + if (MapController.TryGet(map.Left, out var leftMap)) { - newMapId = upMap.Id; - newy = Options.MapHeight - 1; + // Transition to the map on the left and adjust position to the right edge. + spawn.MapId = leftMap.Id; + newx = MapWidth - 1; } - else + } + else if (crossesRightBoundary) + { + // Projectile crosses the right boundary of the map. + if (MapController.TryGet(map.Right, out var rightMap)) { - killSpawn = true; + // Transition to the map on the right and adjust position to the left edge. + spawn.MapId = rightMap.Id; + newx = 0; } } - - if (Math.Ceiling(newy) > Options.MapHeight) + else if (crossesTopBoundary) { - var downMap = MapController.Get(map.Down); - if (downMap != null) + // Projectile crosses the top boundary of the map. + if (MapController.TryGet(map.Up, out var upMap)) { - newMapId = downMap.Id; - newy = 0; + // Transition to the map above and adjust position to the bottom edge. + spawn.MapId = upMap.Id; + newy = MapHeight - 1; } - else + } + else if (crossesBottomBoundary) + { + // Projectile crosses the bottom boundary of the map. + if (MapController.TryGet(map.Down, out var downMap)) { - killSpawn = true; + // Transition to the map below and adjust position to the top edge. + spawn.MapId = downMap.Id; + newy = 0; } } - - spawn.X = newx; - spawn.Y = newy; - spawn.MapId = newMapId; - - return killSpawn; } public override void Die(bool dropItems = true, Entity killer = null) @@ -570,16 +638,12 @@ public override void Die(bool dropItems = true, Entity killer = null) public override EntityPacket EntityPacket(EntityPacket packet = null, Player forPlayer = null) { - if (packet == null) - { - packet = new ProjectileEntityPacket(); - } - + packet ??= new ProjectileEntityPacket(); packet = base.EntityPacket(packet, forPlayer); - var pkt = (ProjectileEntityPacket) packet; + var pkt = (ProjectileEntityPacket)packet; pkt.ProjectileId = Base.Id; - pkt.ProjectileDirection = (byte) Dir; + pkt.ProjectileDirection = (byte)Dir; pkt.TargetId = Owner.Target != default && Owner.Target.Id != Guid.Empty ? Owner.Target.Id : Guid.Empty; pkt.OwnerId = Owner?.Id ?? Guid.Empty; diff --git a/Intersect.Server.Core/Entities/ProjectileSpawn.cs b/Intersect.Server.Core/Entities/ProjectileSpawn.cs index 3e3e0bdf16..9cb1ab249e 100644 --- a/Intersect.Server.Core/Entities/ProjectileSpawn.cs +++ b/Intersect.Server.Core/Entities/ProjectileSpawn.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Intersect.Enums; using Intersect.GameObjects; using Intersect.Server.Entities.Combat; @@ -22,7 +20,7 @@ public partial class ProjectileSpawn public ProjectileBase ProjectileBase; - public long TransmittionTimer = Timing.Global.Milliseconds; + public long TransmissionTimer = Timing.Global.Milliseconds; public float X; @@ -34,7 +32,7 @@ public partial class ProjectileSpawn public Guid MapInstanceId; - private List mEntitiesCollided = new List(); + private List _entitiesCollided = new List(); public ProjectileSpawn( Direction dir, @@ -55,8 +53,8 @@ Projectile parent Dir = dir; ProjectileBase = projectileBase; Parent = parent; - TransmittionTimer = Timing.Global.Milliseconds + - (long) ((float) ProjectileBase.Speed / (float) ProjectileBase.Range); + TransmissionTimer = Timing.Global.Milliseconds + + (long)(ProjectileBase.Speed / (float)ProjectileBase.Range); } public bool IsAtLocation(Guid mapId, int x, int y, int z) @@ -76,13 +74,13 @@ public bool HitEntity(Entity targetEntity) if (targetEntity != null && targetEntity != Parent.Owner) { // Have we collided with this entity before? If so, cancel out. - if (mEntitiesCollided.Contains(targetEntity.Id)) + if (_entitiesCollided.Contains(targetEntity.Id)) { if (!Parent.Base.PierceTarget) { - if(targetPlayer != null) + if (targetPlayer != null) { - if(targetPlayer.Map.ZoneType == Enums.MapZone.Safe || + if (targetPlayer.Map.ZoneType == Enums.MapZone.Safe || Parent.Owner is Player plyr && plyr.InParty(targetPlayer)) { return false; @@ -96,7 +94,7 @@ public bool HitEntity(Entity targetEntity) return false; } } - mEntitiesCollided.Add(targetEntity.Id); + _entitiesCollided.Add(targetEntity.Id); if (targetPlayer != null) { @@ -123,9 +121,9 @@ public bool HitEntity(Entity targetEntity) } else if (targetEntity is Resource targetResource) { - if(targetResource.IsDead()) + if (targetResource.IsDead()) { - if(!ProjectileBase.IgnoreExhaustedResources) + if (!ProjectileBase.IgnoreExhaustedResources) { return true; } @@ -144,13 +142,12 @@ public bool HitEntity(Entity targetEntity) } else //Any other Parent.Target { - var ownerNpc = Parent.Owner as Npc; - if (ownerNpc == null || + if (Parent.Owner is not Npc ownerNpc || ownerNpc.CanNpcCombat(targetEntity, Parent.Spell != null && Parent.Spell.Combat.Friendly)) { Parent.Owner.TryAttack(targetEntity, Parent.Base, Parent.Spell, Parent.Item, Dir); - if (Dir <= Direction.Right && ShouldHook(targetEntity) && !Parent.HasGrappled) + if (Dir <= Direction.Right && ShouldHook(targetEntity) && !Parent.HasGrappled) { HookEntity(); } @@ -173,25 +170,18 @@ public bool HitEntity(Entity targetEntity) /// public bool ShouldHook(Entity en) { - if(en == null) + if (en == null) { return false; } - switch(en) + return en switch { - case Player _: - return ProjectileBase.GrappleHookOptions.Contains(Enums.GrappleOption.Player); - - case Npc _: - return ProjectileBase.GrappleHookOptions.Contains(Enums.GrappleOption.NPC); - - case Resource _: - return ProjectileBase.GrappleHookOptions.Contains(Enums.GrappleOption.Resource); - - default: - throw new ArgumentException($"Unsupported entity type {en.GetType().FullName}", nameof(en)); - } + Player => ProjectileBase.GrappleHookOptions.Contains(Enums.GrappleOption.Player), + Npc => ProjectileBase.GrappleHookOptions.Contains(Enums.GrappleOption.NPC), + Resource => ProjectileBase.GrappleHookOptions.Contains(Enums.GrappleOption.Resource), + _ => throw new ArgumentException($"Unsupported entity type {en.GetType().FullName}", nameof(en)), + }; } /// From bc76e887c3364d10d8eac75a5033aba3b4f2485f Mon Sep 17 00:00:00 2001 From: Arufonsu <17498701+Arufonsu@users.noreply.github.com> Date: Mon, 1 Jul 2024 22:17:45 -0400 Subject: [PATCH 2/2] weylon's review (I) --- .../Entities/Projectiles/Projectile.cs | 331 +++++++++--------- Intersect.Server.Core/Entities/Projectile.cs | 90 ++--- 2 files changed, 205 insertions(+), 216 deletions(-) diff --git a/Intersect.Client/Entities/Projectiles/Projectile.cs b/Intersect.Client/Entities/Projectiles/Projectile.cs index 35d0a1ef9e..ebf0a610c5 100644 --- a/Intersect.Client/Entities/Projectiles/Projectile.cs +++ b/Intersect.Client/Entities/Projectiles/Projectile.cs @@ -76,7 +76,7 @@ public override void Load(EntityPacket packet) { for (var x = 0; x < ProjectileBase.SPAWN_LOCATIONS_WIDTH; x++) { - for (var y = 0; y < ProjectileBase.SPAWN_LOCATIONS_WIDTH; y++) + for (var y = 0; y < ProjectileBase.SPAWN_LOCATIONS_HEIGHT; y++) { for (var d = 0; d < ProjectileBase.MAX_PROJECTILE_DIRECTIONS; d++) { @@ -100,13 +100,11 @@ public override void Load(EntityPacket packet) /// public override void Dispose() { - // Check if Dispose has already been called to avoid redundant calculations. if (!_isDisposing) { - // Use a lock to ensure thread safety when disposing resources. lock (_lock) { - _isDisposing = true; // Mark as disposing to prevent concurrent disposal calls. + _isDisposing = true; // Perform a final update if no projectiles have been spawned. if (_spawnedAmount == 0) @@ -114,17 +112,14 @@ public override void Dispose() Update(); } - // Dispose of each spawned projectile's animation resources. if (_spawns != null) { foreach (var s in _spawns) { - s?.Anim?.DisposeNextDraw(); // Safely call DisposeNextDraw, if the animation exists. + s?.Anim?.DisposeNextDraw(); } } - // Suppress finalization to optimize garbage collection. - // This is important for performance and to prevent the finalizer from running. GC.SuppressFinalize(this); } } @@ -172,27 +167,23 @@ private void AddProjectileSpawns() var spawn = FindSpawnAnimationData(); var animBase = AnimationBase.Get(_myBase.Animations[spawn].AnimationId); - // Iterate over all possible spawn locations within the defined width and height for (var x = 0; x < ProjectileBase.SPAWN_LOCATIONS_WIDTH; x++) { - for (var y = 0; y < ProjectileBase.SPAWN_LOCATIONS_WIDTH; y++) + for (var y = 0; y < ProjectileBase.SPAWN_LOCATIONS_HEIGHT; y++) { - // Iterate over all possible directions a projectile can be spawned in for (var d = 0; d < ProjectileBase.MAX_PROJECTILE_DIRECTIONS; d++) { - // Check if the current direction is enabled for spawning at this location - // and if the maximum number of spawned projectiles has not been reached + // Check if the current direction is enabled for spawning at this location. if (_myBase.SpawnLocations[x, y].Directions[d] == true) { // Calculate the spawn position and direction for the new projectile var s = new ProjectileSpawns( - FindProjectileRotationDir(Dir, (Direction)d), // Calculate the rotation direction + FindProjectileRotationDir(Dir, (Direction)d), (byte)(X + FindProjectileRotation(Dir, x - 2, y - 2, true)), (byte)(Y + FindProjectileRotation(Dir, x - 2, y - 2, false)), Z, MapId, animBase, _myBase.Animations[spawn].AutoRotate, _myBase, this ); - // Check for collision at the spawn location, mark as dead if collision is detected _spawns[_spawnedAmount] = s; if (Collided(_spawnedAmount)) { @@ -219,7 +210,7 @@ private void AddProjectileSpawns() /// The direction of the projectile. /// The x-coordinate value. /// The y-coordinate value. - /// Determines if the calculation is for the X axis (true) or Y axis (false). + /// True for horizontal (X-axis), false for vertical (Y-axis) calculation. /// The rotation value for the specified axis based on the direction. private static int FindProjectileRotation(Direction direction, int x, int y, bool isXAxis) { @@ -255,7 +246,7 @@ private static Direction FindProjectileRotationDir(Direction entityDir, Directio /// /// The direction of the projectile. /// The range of the projectile. - /// A boolean value indicating whether the axis is the X-axis. + /// True for horizontal (X-axis), false for vertical (Y-axis) calculation. /// The calculated range value. private static float GetRange(Direction direction, float range, bool isXAxis) { @@ -318,7 +309,9 @@ public override bool Update() { for (var s = 0; s < _spawnedAmount; s++) { - if (_spawns[s] != null && Maps.MapInstance.Get(_spawns[s].SpawnMapId) != null) + var spawn = _spawns[s]; + + if (spawn != null && Maps.MapInstance.Get(spawn.SpawnMapId) != null) { if (_targetId != Guid.Empty && _targetId != _owner && Globals.Entities.ContainsKey(_targetId) && (_myBase.HomingBehavior || _myBase.DirectShotBehavior)) { @@ -327,9 +320,9 @@ public override bool Update() _lastTargetY = target.Y; _lastTargetMapId = target.MapId; - _spawns[s].OffsetX = GetProjectileLerping(_spawns[s], true); - _spawns[s].OffsetY = GetProjectileLerping(_spawns[s], false); - SetProjectileRotation(_spawns[s]); + spawn.OffsetX = GetProjectileLerping(spawn, true); + spawn.OffsetY = GetProjectileLerping(spawn, false); + SetProjectileRotation(spawn); if (_myBase.DirectShotBehavior) { @@ -338,32 +331,24 @@ public override bool Update() } else if (_lastTargetX != -1 && _lastTargetY != -1) { - _spawns[s].OffsetX = GetProjectileLerping(_spawns[s], true); - _spawns[s].OffsetY = GetProjectileLerping(_spawns[s], false); - SetProjectileRotation(_spawns[s]); + spawn.OffsetX = GetProjectileLerping(spawn, true); + spawn.OffsetY = GetProjectileLerping(spawn, false); + SetProjectileRotation(spawn); } else { - _spawns[s].OffsetX = GetRange(_spawns[s].Dir, GetDisplacement(_spawns[s].SpawnTime), true); - _spawns[s].OffsetY = GetRange(_spawns[s].Dir, GetDisplacement(_spawns[s].SpawnTime), false); - _spawns[s].Anim.SetRotation(false); + spawn.OffsetX = GetRange(spawn.Dir, GetDisplacement(spawn.SpawnTime), true); + spawn.OffsetY = GetRange(spawn.Dir, GetDisplacement(spawn.SpawnTime), false); + spawn.Anim.SetRotation(false); } - _spawns[s] - .Anim.SetPosition( - Maps.MapInstance.Get(_spawns[s].SpawnMapId).GetX() + - _spawns[s].SpawnX * Options.TileWidth + - _spawns[s].OffsetX + - Options.TileWidth / 2, - Maps.MapInstance.Get(_spawns[s].SpawnMapId).GetY() + - _spawns[s].SpawnY * Options.TileHeight + - _spawns[s].OffsetY + - Options.TileHeight / 2, X, Y, MapId, - _spawns[s].AutoRotate ? _spawns[s].Dir : Direction.Up, - _spawns[s].Z - ); - - _spawns[s].Anim.Update(); + var spawnMapId = Maps.MapInstance.Get(spawn.SpawnMapId); + var spawnX = spawnMapId.GetX() + spawn.SpawnX * Options.TileWidth + spawn.OffsetX + Options.TileWidth / 2; + var spawnY = spawnMapId.GetY() + spawn.SpawnY * Options.TileHeight + spawn.OffsetY + Options.TileHeight / 2; + var spawnDirection = spawn.AutoRotate ? spawn.Dir : Direction.Up; + + spawn.Anim.SetPosition(spawnX, spawnY, X, Y, MapId, spawnDirection, spawn.Z); + spawn.Anim.Update(); } } } @@ -377,56 +362,58 @@ public override bool Update() /// /// Calculates the offset for the projectile spawn position based on the target map and spawn location. /// - /// A boolean value indicating whether the calculation should be done horizontally (X-axis) or vertically (Y-axis). /// The projectile spawn information. + /// True for horizontal (X-axis), false for vertical (Y-axis) calculation. /// The calculated offset value. - private float GetProjectileOffset(bool isXAxis, ProjectileSpawns spawn) + private float GetProjectileOffset(ProjectileSpawns spawn, bool isXAxis) { - if (_lastTargetMapId != Guid.Empty && _lastTargetMapId != spawn.SpawnMapId && Maps.MapInstance.TryGet(spawn.SpawnMapId, out var map)) + if (_lastTargetMapId == Guid.Empty || _lastTargetMapId == spawn.SpawnMapId || !Maps.MapInstance.TryGet(spawn.SpawnMapId, out var map)) { - for (var y = map.GridY - 1; y <= map.GridY + 1; y++) + return isXAxis ? _lastTargetX - spawn.SpawnX : _lastTargetY - spawn.SpawnY; + } + + for (var y = map.GridY - 1; y <= map.GridY + 1; y++) + { + for (var x = map.GridX - 1; x <= map.GridX + 1; x++) { - for (var x = map.GridX - 1; x <= map.GridX + 1; x++) + if (x < 0 || x >= Globals.MapGrid.GetLength(0) || y < 0 || y >= Globals.MapGrid.GetLength(1)) + { + continue; + } + + if (Globals.MapGrid[x, y] != _lastTargetMapId || Globals.MapGrid[x, y] == Guid.Empty) + { + continue; + } + + if (isXAxis) // Horizontal (X) calculation { - if (x < 0 || x >= Globals.MapGrid.GetLength(0) || y < 0 || y >= Globals.MapGrid.GetLength(1)) + var leftSide = x == map.GridX - 1; + var rightSide = x == map.GridX + 1; + + if (leftSide) { - continue; + return _lastTargetX - Options.MapWidth - spawn.SpawnX; } - if (Globals.MapGrid[x, y] != _lastTargetMapId || Globals.MapGrid[x, y] == Guid.Empty) + if (rightSide) { - continue; + return _lastTargetX + Options.MapWidth - spawn.SpawnX; } + } + else // Vertical (Y) calculation + { + var topSide = y == map.GridY + 1; + var bottomSide = y == map.GridY - 1; - if (isXAxis) // Horizontal (X) calculation + if (topSide) { - var leftSide = x == map.GridX - 1; - var rightSide = x == map.GridX + 1; - - if (leftSide) - { - return _lastTargetX - Options.MapWidth - spawn.SpawnX; - } - - if (rightSide) - { - return _lastTargetX + Options.MapWidth - spawn.SpawnX; - } + return _lastTargetY + Options.MapHeight - spawn.SpawnY; } - else // Vertical (Y) calculation - { - var topSide = y == map.GridY + 1; - var bottomSide = y == map.GridY - 1; - if (topSide) - { - return _lastTargetY + Options.MapHeight - spawn.SpawnY; - } - - if (bottomSide) - { - return _lastTargetY - Options.MapHeight - spawn.SpawnY; - } + if (bottomSide) + { + return _lastTargetY - Options.MapHeight - spawn.SpawnY; } } } @@ -439,7 +426,7 @@ private float GetProjectileOffset(bool isXAxis, ProjectileSpawns spawn) /// Calculates the interpolated (lerped) position value for a projectile along the X or Y axis. /// /// The spawn information of the projectile, including its initial position and spawn time. - /// True to calculate the position along the X axis, false for the Y axis. + /// True for horizontal (X-axis), false for vertical (Y-axis) calculation. /// The interpolated position value for the projectile on the specified axis, taking into account the projectile's travel direction, speed, and elapsed time since spawning. /// /// This method determines the projectile's current position by interpolating between its initial offset and its desired position at the current time. @@ -449,7 +436,7 @@ private float GetProjectileOffset(bool isXAxis, ProjectileSpawns spawn) /// private float GetProjectileLerping(ProjectileSpawns spawn, bool isXAxis) { - var (directionX, directionY) = (GetProjectileOffset(true, spawn), GetProjectileOffset(false, spawn)); + var (directionX, directionY) = (GetProjectileOffset(spawn, true), GetProjectileOffset(spawn, false)); var distance = MathF.Sqrt(directionX * directionX + directionY * directionY); if (distance == 0) return 0; @@ -475,8 +462,7 @@ private float GetProjectileLerping(ProjectileSpawns spawn, bool isXAxis) /// private void SetProjectileRotation(ProjectileSpawns spawn) { - var directionX = GetProjectileOffset(true, spawn); - var directionY = GetProjectileOffset(false, spawn); + var (directionX, directionY) = (GetProjectileOffset(spawn, true), GetProjectileOffset(spawn, false)); var angle = (float)(Math.Atan2(directionY, directionX) * (180.0 / Math.PI) + 90); spawn.Anim.SetRotation(angle); } @@ -486,96 +472,99 @@ private void SetProjectileRotation(ProjectileSpawns spawn) /// public void CheckForCollision() { - if (_spawnCount != 0 || _quantity <= _myBase.Quantity) + if (_spawnCount == 0 && _quantity > _myBase.Quantity) { - for (var i = 0; i < _spawnedAmount && i < _spawns.Length; i++) // Ensure i is within _spawns bounds + Globals.Entities[Id].Dispose(); + return; + } + + for (var i = 0; i < _spawnedAmount && i < _spawns.Length; i++) + { + var projectileSpawn = _spawns[i]; + if (projectileSpawn == null || Timing.Global.Milliseconds <= projectileSpawn.TransmissionTimer) + { + continue; + } + + if (!Maps.MapInstance.TryGet(projectileSpawn.MapId, out var spawnMap)) + { + continue; + } + + float newx = projectileSpawn.X; + float newy = projectileSpawn.Y; + + if (_myBase.HomingBehavior || _myBase.DirectShotBehavior) { - if (_spawns[i] != null && Timing.Global.Milliseconds > _spawns[i].TransmissionTimer) + if (_targetId != Guid.Empty && _targetId != _owner || _lastTargetX != -1 && _lastTargetY != -1 && _lastTargetMapId != Guid.Empty) { - if (Maps.MapInstance.TryGet(_spawns[i].MapId, out var spawnMap)) - { - float newx = _spawns[i].X; - float newy = _spawns[i].Y; - - if (_myBase.HomingBehavior || _myBase.DirectShotBehavior) - { - if (_targetId != Guid.Empty && _targetId != _owner || _lastTargetX != -1 && _lastTargetY != -1 && _lastTargetMapId != Guid.Empty) - { - float deltaX = GetProjectileOffset(true, _spawns[i]); - float deltaY = GetProjectileOffset(false, _spawns[i]); - float distance = MathF.Sqrt(deltaX * deltaX + deltaY * deltaY); - float xFactor = deltaX / distance; - float yFactor = deltaY / distance; - newx += xFactor; - newy += yFactor; - } - } - else - { - newx += GetRange(_spawns[i].Dir, 1, true); - newy += GetRange(_spawns[i].Dir, 1, false); - } + var (deltaX, deltaY) = (GetProjectileOffset(projectileSpawn, true), GetProjectileOffset(projectileSpawn, false)); + float distance = MathF.Sqrt(deltaX * deltaX + deltaY * deltaY); + float xFactor = deltaX / distance; + float yFactor = deltaY / distance; + newx += xFactor; + newy += yFactor; + } + } + else + { + newx += GetRange(projectileSpawn.Dir, 1, true); + newy += GetRange(projectileSpawn.Dir, 1, false); + } - AdjustPositionOnMapBoundaries(ref newx, ref newy, ref spawnMap); + AdjustPositionOnMapBoundaries(ref newx, ref newy, ref spawnMap); - _spawns[i].X = (int)newx; - _spawns[i].Y = (int)newy; - _spawns[i].MapId = spawnMap.Id; - _spawns[i].Distance++; - _spawns[i].TransmissionTimer = Timing.Global.MillisecondsOffset + (long)(_myBase.Speed / (float)_myBase.Range); + projectileSpawn.X = (int)newx; + projectileSpawn.Y = (int)newy; + projectileSpawn.MapId = spawnMap.Id; + projectileSpawn.Distance++; + projectileSpawn.TransmissionTimer = Timing.Global.MillisecondsOffset + (long)(_myBase.Speed / (float)_myBase.Range); - var killSpawn = Collided(i) || _spawns[i].Distance >= _myBase.Range || - newx < 0 || newx >= Options.MapWidth || newy < 0 || - newy >= Options.MapHeight; + var killSpawn = Collided(i) || projectileSpawn.Distance >= _myBase.Range || + newx < 0 || newx >= Options.MapWidth || newy < 0 || + newy >= Options.MapHeight; - // Check for map boundaries and remove the spawn if it goes out of bounds. - if (killSpawn) - { - TryRemoveSpawn(i); - _spawnCount--; + // Check for map boundaries and remove the spawn if it goes out of bounds. + if (killSpawn) + { + TryRemoveSpawn(i); + _spawnCount--; - continue; - } + continue; + } - // Check for Z-Dimension - if (!killSpawn && spawnMap.Attributes != null && - _spawns[i].X >= 0 && _spawns[i].Y >= 0 && _spawns[i].X < spawnMap.Attributes.GetLength(0) && - _spawns[i].Y < spawnMap.Attributes.GetLength(1)) - { - var attribute = spawnMap.Attributes[_spawns[i].X, _spawns[i].Y]; + // Check for Z-Dimension + if (!killSpawn && spawnMap.Attributes != null && + projectileSpawn.X >= 0 && projectileSpawn.Y >= 0 && projectileSpawn.X < spawnMap.Attributes.GetLength(0) && + projectileSpawn.Y < spawnMap.Attributes.GetLength(1)) + { + var attribute = spawnMap.Attributes[projectileSpawn.X, projectileSpawn.Y]; - if (attribute != null && attribute.Type == MapAttribute.ZDimension) - { - var zDimensionAttribute = (MapZDimensionAttribute)attribute; - - // If the Z dimension attribute specifies a blocked level that matches the projectile's current Z level, - // mark the projectile for destruction. - if (zDimensionAttribute.BlockedLevel > 0 && _spawns[i].Z == zDimensionAttribute.BlockedLevel - 1) - { - killSpawn = true; - } - - // If the Z dimension attribute specifies a gateway to another level, - // adjust the projectile's Z level to the specified gateway level. - if (zDimensionAttribute.GatewayTo > 0) - { - _spawns[i].Z = zDimensionAttribute.GatewayTo - 1; - } - } - } + if (attribute != null && attribute.Type == MapAttribute.ZDimension) + { + var zDimensionAttribute = (MapZDimensionAttribute)attribute; - if (killSpawn) - { - TryRemoveSpawn(i); - _spawnCount--; - } + // If the Z dimension attribute specifies a blocked level that matches the projectile's current Z level, + // mark the projectile for destruction. + if (zDimensionAttribute.BlockedLevel > 0 && projectileSpawn.Z == zDimensionAttribute.BlockedLevel - 1) + { + killSpawn = true; + } + + // If the Z dimension attribute specifies a gateway to another level, + // adjust the projectile's Z level to the specified gateway level. + if (zDimensionAttribute.GatewayTo > 0) + { + projectileSpawn.Z = zDimensionAttribute.GatewayTo - 1; } } } - } - else - { - Globals.Entities[Id].Dispose(); + + if (killSpawn) + { + TryRemoveSpawn(i); + _spawnCount--; + } } } @@ -593,8 +582,8 @@ public void CheckForCollision() /// private static void AdjustPositionOnMapBoundaries(ref float newx, ref float newy, ref Maps.MapInstance spawnMap) { - int MapWidth = Options.MapWidth; // The width of the map. - int MapHeight = Options.MapHeight; // The height of the map. + int MapWidth = Options.MapWidth; + int MapHeight = Options.MapHeight; // Determine if the projectile crosses any of the map boundaries. bool crossesLeftBoundary = MathF.Floor(newx) < 0; @@ -693,20 +682,20 @@ private static void AdjustPositionOnMapBoundaries(ref float newx, ref float newy /// True if the projectile spawn has collided and should be destroyed; otherwise, false. private bool Collided(int i) { - var killSpawn = false; // Flag to determine if the spawn should be killed due to collision - IEntity? blockedBy = default; // Entity, resource, or map block that the projectile has potentially collided with - var spawn = _spawns[i]; // Current projectile spawn being checked for collision + var killSpawn = false; + IEntity? blockedBy = default; + var spawn = _spawns[i]; - // Check if the tile at the projectile's location is blocked + // Check if the tile at the projectile's location is blocked. var tileBlocked = Globals.Me.IsTileBlocked( - new Point(spawn.X, spawn.Y), // Projectile spawn location - Z, // Z-level of the projectile - spawn.MapId, // ID of the map where the projectile is located - ref blockedBy, // Reference to the entity blocking the projectile, if any - spawn.ProjectileBase.IgnoreActiveResources, // Whether active resources should be ignored - spawn.ProjectileBase.IgnoreExhaustedResources, // Whether exhausted resources should be ignored - true, // Always check for entities - true // Always check for map blocks + delta: new Point(spawn.X, spawn.Y), + z: Z, + mapId: spawn.MapId, + blockedBy: ref blockedBy, + ignoreAliveResources: spawn.ProjectileBase.IgnoreActiveResources, + ignoreDeadResources: spawn.ProjectileBase.IgnoreExhaustedResources, + ignoreNpcAvoids: true, + projectileTrigger: true ); switch (tileBlocked) diff --git a/Intersect.Server.Core/Entities/Projectile.cs b/Intersect.Server.Core/Entities/Projectile.cs index 0be68ae351..70f0b79422 100644 --- a/Intersect.Server.Core/Entities/Projectile.cs +++ b/Intersect.Server.Core/Entities/Projectile.cs @@ -112,7 +112,7 @@ private void AddProjectileSpawns() { // Calculate the spawn position and direction for the new projectile var s = new ProjectileSpawn( - FindProjectileRotationDir(Dir, (Direction)d), // Calculate the rotation direction + FindProjectileRotationDir(Dir, (Direction)d), (byte)(X + FindProjectileRotation(Dir, x - 2, y - 2, true)), (byte)(Y + FindProjectileRotation(Dir, x - 2, y - 2, false)), (byte)Z, MapId, MapInstanceId, Base, this @@ -123,7 +123,6 @@ private void AddProjectileSpawns() _spawnedAmount++; _spawnCount++; - // Check for collision at the spawn location, mark as dead if collision is detected if (CheckForCollision(s)) { s.Dead = true; @@ -144,7 +143,7 @@ private void AddProjectileSpawns() /// The direction of the projectile. /// The x-coordinate value. /// The y-coordinate value. - /// Determines if the calculation is for the X axis (true) or Y axis (false). + /// True for horizontal (X-axis), false for vertical (Y-axis) calculation. /// The rotation value for the specified axis based on the direction. private static int FindProjectileRotation(Direction direction, int x, int y, bool isXAxis) { @@ -190,7 +189,7 @@ public void Update(List projDeaths, List> spawnDea /// /// The direction of the projectile. /// The range of the projectile. - /// A boolean value indicating whether the axis is the X-axis. + /// True for horizontal (X-axis), false for vertical (Y-axis) calculation. /// The calculated range value. private static float GetRange(Direction direction, float range, bool isXAxis) { @@ -333,6 +332,7 @@ public bool CheckForCollision(ProjectileSpawn spawn) !spawn.Parent.HasGrappled && (spawn.X != Owner.X || spawn.Y != Owner.Y)) { + // Grapple hooks are only allowed in the default projectile behavior if (!spawn.ProjectileBase.HomingBehavior && !spawn.ProjectileBase.DirectShotBehavior && (spawn.Dir <= Direction.Right || (spawn.Dir != Direction.None && Options.Instance.MapOpts.EnableDiagonalMovement))) { @@ -386,60 +386,62 @@ public bool CheckForCollision(ProjectileSpawn spawn) } /// - /// Calculates the offset of a projectile based on the target's position and the spawn location. + /// Calculates the offset for the projectile spawn position based on the target map and spawn location. /// - /// A boolean value indicating whether the calculation should be done horizontally (X-axis) or vertically (Y-axis). /// The projectile spawn information. + /// True for horizontal (X-axis), false for vertical (Y-axis) calculation. /// The calculated offset value. - private float GetProjectileOffset(bool isXAxis, ProjectileSpawn spawn) + private float GetProjectileOffset(ProjectileSpawn spawn, bool isXAxis) { - if (_lastTargetMapId != Guid.Empty && _lastTargetMapId != spawn.MapId && MapController.TryGet(spawn.MapId, out var map)) + if (_lastTargetMapId == Guid.Empty || _lastTargetMapId == spawn.MapId || !MapController.TryGet(spawn.MapId, out var map)) { - var grid = DbInterface.GetGrid(map.MapGrid); + return isXAxis ? _lastTargetX - spawn.X : _lastTargetY - spawn.Y; + } + + var grid = DbInterface.GetGrid(map.MapGrid); - for (var y = map.MapGridY - 1; y <= map.MapGridY + 1; y++) + for (var y = map.MapGridY - 1; y <= map.MapGridY + 1; y++) + { + for (var x = map.MapGridX - 1; x <= map.MapGridX + 1; x++) { - for (var x = map.MapGridX - 1; x <= map.MapGridX + 1; x++) + if (x < 0 || x >= grid.MapIdGrid.GetLength(0) || y < 0 || y >= grid.MapIdGrid.GetLength(1)) { - if (x < 0 || x >= grid.MapIdGrid.GetLength(0) || y < 0 || y >= grid.MapIdGrid.GetLength(1)) + continue; + } + + if (grid.MapIdGrid[x, y] != _lastTargetMapId || grid.MapIdGrid[x, y] == Guid.Empty) + { + continue; + } + + if (isXAxis) + { + var leftSide = x == map.MapGridX - 1; + var rightSide = x == map.MapGridX + 1; + + if (leftSide) { - continue; + return _lastTargetX - Options.MapWidth - spawn.X; } - if (grid.MapIdGrid[x, y] != _lastTargetMapId || grid.MapIdGrid[x, y] == Guid.Empty) + if (rightSide) { - continue; + return _lastTargetX + Options.MapWidth - spawn.X; } + } + else + { + var topSide = y == map.MapGridY - 1; + var bottomSide = y == map.MapGridY + 1; - if (isXAxis) // Horizontal (X) calculation + if (topSide) { - var leftSide = x == map.MapGridX - 1; - var rightSide = x == map.MapGridX + 1; - - if (leftSide) - { - return _lastTargetX - Options.MapWidth - spawn.X; - } - - if (rightSide) - { - return _lastTargetX + Options.MapWidth - spawn.X; - } + return _lastTargetY - Options.MapHeight - spawn.Y; } - else // Vertical (Y) calculation - { - var topSide = y == map.MapGridY - 1; - var bottomSide = y == map.MapGridY + 1; - if (topSide) - { - return _lastTargetY - Options.MapHeight - spawn.Y; - } - - if (bottomSide) - { - return _lastTargetY + Options.MapHeight - spawn.Y; - } + if (bottomSide) + { + return _lastTargetY + Options.MapHeight - spawn.Y; } } } @@ -469,8 +471,7 @@ public bool MoveFragment(ProjectileSpawn spawn, bool move = true) _lastTargetX = Target.X; _lastTargetY = Target.Y; _lastTargetMapId = Target.MapId; - var directionX = GetProjectileOffset(true, spawn); - var directionY = GetProjectileOffset(false, spawn); + var (directionX, directionY) = (GetProjectileOffset(spawn, true), GetProjectileOffset(spawn, false)); var distance = MathF.Sqrt(directionX * directionX + directionY * directionY); // Normalize the direction and update the projectile's position. @@ -486,8 +487,7 @@ public bool MoveFragment(ProjectileSpawn spawn, bool move = true) else if (_lastTargetX != -1 && _lastTargetY != -1) { // Last known target location logic: Moves the projectile towards the last known position of the target. - var directionX = GetProjectileOffset(true, spawn); - var directionY = GetProjectileOffset(false, spawn); + var (directionX, directionY) = (GetProjectileOffset(spawn, true), GetProjectileOffset(spawn, false)); var distance = MathF.Sqrt(directionX * directionX + directionY * directionY); // Normalize the direction and update the projectile's position.