From a3bfbe6ce572f78ab10e6162f32957006ab48505 Mon Sep 17 00:00:00 2001 From: Fernando Arzola <17498701+Arufonsu@users.noreply.github.com> Date: Tue, 2 Jul 2024 21:36:47 -0400 Subject: [PATCH] projectile refactoring: fixes, docs, enhancements (#2295) * 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. * weylon's review (I) --- .../Entities/Projectiles/Projectile.cs | 856 +++++++++--------- .../Entities/Projectiles/ProjectileSpawns.cs | 8 +- Intersect.Server.Core/Entities/Projectile.cs | 562 +++++++----- .../Entities/ProjectileSpawn.cs | 48 +- 4 files changed, 788 insertions(+), 686 deletions(-) diff --git a/Intersect.Client/Entities/Projectiles/Projectile.cs b/Intersect.Client/Entities/Projectiles/Projectile.cs index 48207fab4f..ebf0a610c5 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,63 +60,67 @@ 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++) { - 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++) { - 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) + if (!_isDisposing) { - lock (mLock) + lock (_lock) { - mDisposing = true; - if (mSpawnedAmount == 0) + _isDisposing = true; + + // Perform a final update if no projectiles have been spawned. + if (_spawnedAmount == 0) { Update(); } - if (Spawns != null) + 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(); } } + + GC.SuppressFinalize(this); } } } @@ -132,15 +134,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 +154,201 @@ 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); 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++) { - if (mMyBase.SpawnLocations[x, y].Directions[d] == true) + // 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), - (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 + (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)) + _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. + /// 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) { - 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. + /// 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) { - 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) + var spawn = _spawns[s]; + + if (spawn != null && Maps.MapInstance.Get(spawn.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]); + spawn.OffsetX = GetProjectileLerping(spawn, true); + spawn.OffsetY = GetProjectileLerping(spawn, false); + SetProjectileRotation(spawn); - 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]); + spawn.OffsetX = GetProjectileLerping(spawn, true); + spawn.OffsetY = GetProjectileLerping(spawn, false); + SetProjectileRotation(spawn); } 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); + 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(); } } } @@ -368,311 +360,370 @@ 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) + /// The projectile spawn information. + /// True for horizontal (X-axis), false for vertical (Y-axis) calculation. + /// The calculated offset value. + private float GetProjectileOffset(ProjectileSpawns spawn, bool isXAxis) { - if (mLastTargetMapId != Guid.Empty && mLastTargetMapId != spawn.SpawnMapId) + if (_lastTargetMapId == Guid.Empty || _lastTargetMapId == spawn.SpawnMapId || !Maps.MapInstance.TryGet(spawn.SpawnMapId, out var map)) + { + return isXAxis ? _lastTargetX - spawn.SpawnX : _lastTargetY - spawn.SpawnY; + } + + for (var y = map.GridY - 1; y <= map.GridY + 1; y++) { - 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++) { - 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] != Guid.Empty && Globals.MapGrid[x, y] == mLastTargetMapId) + if (rightSide) { - var leftSide = x == map.GridX - 1; - var rightSide = x == map.GridX + 1; - - if (leftSide) - { - return mLastTargetX - 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++) + else // Vertical (Y) calculation { - if (x < 0 || x >= Globals.MapGrid.GetLength(0) || y < 0 || y >= Globals.MapGrid.GetLength(1)) + var topSide = y == map.GridY + 1; + var bottomSide = y == map.GridY - 1; + + if (topSide) { - continue; + return _lastTargetY + Options.MapHeight - spawn.SpawnY; } - if (Globals.MapGrid[x, y] != Guid.Empty && Globals.MapGrid[x, y] == mLastTargetMapId) + if (bottomSide) { - var upSide = y == map.GridY + 1; - var downSide = y == map.GridY - 1; - - if (upSide) - { - return mLastTargetY + Options.MapHeight - spawn.SpawnY; - } - - if (downSide) - { - 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 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. + /// 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 (directionX, directionY) = (GetProjectileOffset(spawn, true), GetProjectileOffset(spawn, false)); + var distance = MathF.Sqrt(directionX * directionX + directionY * directionY); + if (distance == 0) return 0; - var length = (float)Math.Sqrt(directionX * directionX + directionY * directionY); - valueToLerp /= length; - - 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, directionY) = (GetProjectileOffset(spawn, true), GetProjectileOffset(spawn, false)); 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++) + 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) { - if (Spawns[i] != null && Timing.Global.Milliseconds > Spawns[i].TransmittionTimer) - { - var spawnMap = Maps.MapInstance.Get(Spawns[i].MapId); - if (spawnMap != null) - { - var newx = Spawns[i].X + (int)GetRangeX(Spawns[i].Dir, 1); - var newy = Spawns[i].Y + (int)GetRangeY(Spawns[i].Dir, 1); + continue; + } - if (mMyBase.HomingBehavior || mMyBase.DirectShotBehavior) - { - if (TargetId != Guid.Empty && TargetId != mOwner || mLastTargetX != -1 && mLastTargetY != -1 && mLastTargetMapId != Guid.Empty) - { - float deltaX = GetProjectileX(Spawns[i]); - float deltaY = GetProjectileY(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); - } - } + if (!Maps.MapInstance.TryGet(projectileSpawn.MapId, out var spawnMap)) + { + continue; + } - var newMapId = Spawns[i].MapId; - var killSpawn = false; + float newx = projectileSpawn.X; + float newy = projectileSpawn.Y; - Spawns[i].Distance++; + if (_myBase.HomingBehavior || _myBase.DirectShotBehavior) + { + if (_targetId != Guid.Empty && _targetId != _owner || _lastTargetX != -1 && _lastTargetY != -1 && _lastTargetMapId != Guid.Empty) + { + 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); + } - if (newx < 0) - { - 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; - } - } + AdjustPositionOnMapBoundaries(ref newx, ref newy, ref spawnMap); - 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; - } - } + 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); - 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; - } - } + var killSpawn = Collided(i) || projectileSpawn.Distance >= _myBase.Range || + newx < 0 || newx >= Options.MapWidth || newy < 0 || + newy >= Options.MapHeight; - 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; - } - } + // Check for map boundaries and remove the spawn if it goes out of bounds. + if (killSpawn) + { + TryRemoveSpawn(i); + _spawnCount--; - if (killSpawn) - { - TryRemoveSpawn(i); - mSpawnCount--; + continue; + } - continue; - } + // 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]; - Spawns[i].X = newx; - Spawns[i].Y = newy; - Spawns[i].MapId = newMapId; - var newMap = Maps.MapInstance.Get(newMapId); + if (attribute != null && attribute.Type == MapAttribute.ZDimension) + { + var zDimensionAttribute = (MapZDimensionAttribute)attribute; - //Check for Z-Dimension - if (newMap.Attributes[Spawns[i].X, Spawns[i].Y] != null) - { - if (newMap.Attributes[Spawns[i].X, Spawns[i].Y].Type == MapAttribute.ZDimension) - { - if (((MapZDimensionAttribute) newMap.Attributes[Spawns[i].X, Spawns[i].Y]) - .GatewayTo > - 0) - { - Spawns[i].Z = - ((MapZDimensionAttribute) newMap.Attributes[Spawns[i].X, Spawns[i].Y]) - .GatewayTo - - 1; - } - } - } + // 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 (killSpawn == false) - { - killSpawn = Collided(i); - } + // 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; + } + } + } - Spawns[i].TransmittionTimer = Timing.Global.Milliseconds + - (long) (mMyBase.Speed / (float) mMyBase.Range); + if (killSpawn) + { + TryRemoveSpawn(i); + _spawnCount--; + } + } + } - if (Spawns[i].Distance >= mMyBase.Range) - { - killSpawn = true; - } + /// + /// 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; + int MapHeight = Options.MapHeight; - if (killSpawn) - { - TryRemoveSpawn(i); - mSpawnCount--; - } - } - } + // 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 + else if (crossesRightBoundary && crossesTopBoundary) { - Globals.Entities[Id].Dispose(); + // 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 spawn = _spawns[i]; + + // 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 + 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) { - 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 +732,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 +744,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 +752,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..70f0b79422 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,46 @@ 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 + (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++; + if (CheckForCollision(s)) { s.Dead = true; @@ -121,49 +132,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. + /// 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) { - 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 +176,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. + /// 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) { - 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 +220,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 +242,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 +262,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 +278,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 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; } - var attribute = map.Attributes[(int)Math.Round(spawn.X), (int)Math.Round(spawn.Y)]; + + 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 +332,9 @@ 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) - ) + // 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))) { 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,222 +364,263 @@ 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 for the projectile spawn position based on the target map and spawn location. + /// + /// The projectile spawn information. + /// True for horizontal (X-axis), false for vertical (Y-axis) calculation. + /// The calculated offset value. + private float GetProjectileOffset(ProjectileSpawn spawn, bool isXAxis) { - 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); + return isXAxis ? _lastTargetX - spawn.X : _lastTargetY - spawn.Y; + } + + var grid = DbInterface.GetGrid(map.MapGrid); - //loop through surrounding maps - 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)) + { + continue; + } + + if (grid.MapIdGrid[x, y] != _lastTargetMapId || grid.MapIdGrid[x, y] == Guid.Empty) + { + continue; + } + + if (isXAxis) { - if (x < 0 || x >= grid.MapIdGrid.GetLength(0) || y < 0 || y >= grid.MapIdGrid.GetLength(1)) + 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] != Guid.Empty && grid.MapIdGrid[x, y] == mLastTargetMapId) + if (rightSide) { - var leftSide = x == map.MapGridX - 1; - var rightSide = x == map.MapGridX + 1; - - if (leftSide) - { - return mLastTargetX - 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++) + else { - if (x < 0 || x >= grid.MapIdGrid.GetLength(0) || y < 0 || y >= grid.MapIdGrid.GetLength(1)) + var topSide = y == map.MapGridY - 1; + var bottomSide = y == map.MapGridY + 1; + + if (topSide) { - continue; + return _lastTargetY - Options.MapHeight - spawn.Y; } - if (grid.MapIdGrid[x, y] != Guid.Empty && grid.MapIdGrid[x, y] == mLastTargetMapId) + if (bottomSide) { - var topSide = y == map.MapGridY - 1; - var bottomSide = y == map.MapGridY + 1; - - if (topSide) - { - return mLastTargetY - 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, directionY) = (GetProjectileOffset(spawn, true), GetProjectileOffset(spawn, false)); + 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); + // Last known target location logic: Moves the projectile towards the last known position of the target. + var (directionX, directionY) = (GetProjectileOffset(spawn, true), GetProjectileOffset(spawn, false)); + var distance = MathF.Sqrt(directionX * directionX + directionY * directionY); - newx += (float)(directionX / length); - newy += (float)(directionY / length); + // 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)), + }; } ///