Files
tiny_tackle_heroes/Assets/_Project/Scripts/Enemies/EnemyAttack.cs
T

1023 lines
34 KiB
C#

using System.Collections.Generic;
using BumpCombat.Constants;
using BumpCombat.Player;
using UnityEngine;
namespace BumpCombat.Enemies
{
[RequireComponent(typeof(EnemyController))]
public sealed class EnemyAttack : MonoBehaviour
{
private EnemyController controller;
private PlayerHealth playerHealth;
private AttackWarningVisual warningVisual;
private LancerThrustVisual lancerVisual;
private Vector2 lockedDirection;
private Vector2 targetPosition;
private Vector2 attackOrigin;
private Vector2 lancerPreviousTip;
private float lancerPreviousProgress;
private bool lancerAttackActive;
private bool lancerHitConfirmed;
private bool lancerTipNeedsRebase;
private bool necrofireRayActive;
private bool necrofireRayHitConfirmed;
private bool meleeAttackActive;
private bool meleeHitConfirmed;
private StageEnemyEffectVisual necrofireRayVisual;
private readonly List<StageEnemyEffectVisual> stageAoeVisuals = new();
private readonly List<AttackWarningVisual> stageMultiWarnings = new();
public bool IsLancerAttack => controller != null
&& controller.Definition != null
&& controller.Definition.Kind == EnemyKind.Lancer;
public Vector2 LockedDirection => lockedDirection;
public Vector2 LockedAttackOrigin => GetAttackOrigin(
GetAttackRootPosition(),
lockedDirection,
controller != null
? GetAuthoredOriginOffset()
: Vector2.zero,
controller != null
? controller.CurrentAttackPattern.DirectionMode
: EnemyAttackDirectionMode.LockedDirection);
private Vector2 GetAttackRootPosition()
{
return controller != null
&& controller.CurrentAttackPattern.DirectionMode
== EnemyAttackDirectionMode.HorizontalRoot
? controller.GroundAnchorPosition
: attackOrigin;
}
public static float GetRootScale(Transform root)
{
if (root == null)
{
return 1f;
}
Vector3 scale = root.lossyScale;
return Mathf.Max(
0.0001f,
Mathf.Max(Mathf.Abs(scale.x), Mathf.Abs(scale.y)));
}
private float GeometryScale => controller != null
&& controller.CurrentAttackPattern.GeometryScaleMode
== EnemyAttackGeometryScaleMode.RootScale
? GetRootScale(transform)
: 1f;
private Vector2 GetAuthoredOriginOffset()
{
return controller.CurrentAttackPattern.AttackOriginOffset
* GeometryScale;
}
private float GetAttackLength()
{
return controller.CurrentAttackPattern.GeometryScaleMode
== EnemyAttackGeometryScaleMode.RootScale
? controller.CurrentAttackPattern.AttackLength * GeometryScale
: controller.AttackLength;
}
private float GetAttackWidth()
{
return controller.CurrentAttackPattern.GeometryScaleMode
== EnemyAttackGeometryScaleMode.RootScale
? controller.CurrentAttackPattern.AttackWidth * GeometryScale
: controller.AttackWidth;
}
private float GetAttackRadius()
{
return controller.CurrentAttackPattern.GeometryScaleMode
== EnemyAttackGeometryScaleMode.RootScale
? controller.CurrentAttackPattern.AttackRadius * GeometryScale
: controller.AttackRadius;
}
public static Vector2 ResolveAttackDirection(
EnemyAttackDirectionMode directionMode,
Vector2 requestedDirection,
Vector2 fallbackDirection)
{
Vector2 fallback = fallbackDirection.sqrMagnitude > 0.0001f
? fallbackDirection.normalized
: Vector2.right;
if (directionMode == EnemyAttackDirectionMode.HorizontalRoot)
{
float x = Mathf.Abs(requestedDirection.x) > 0.0001f
? requestedDirection.x
: fallback.x;
return Vector2.right * (x < 0f ? -1f : 1f);
}
return requestedDirection.sqrMagnitude > 0.0001f
? requestedDirection.normalized
: fallback;
}
public static Vector2 GetAttackOrigin(
Vector2 rootPosition,
Vector2 direction,
Vector2 localOffset,
EnemyAttackDirectionMode directionMode =
EnemyAttackDirectionMode.LockedDirection)
{
Vector2 resolvedDirection = direction.sqrMagnitude > 0.0001f
? direction.normalized
: Vector2.right;
Vector2 side = directionMode == EnemyAttackDirectionMode.HorizontalRoot
? Vector2.up
: new Vector2(-resolvedDirection.y, resolvedDirection.x);
return rootPosition
+ resolvedDirection * localOffset.x
+ side * localOffset.y;
}
public bool IsContactWindowActive(float activeElapsed)
{
return controller != null
&& controller.CurrentAttackPattern.IsContactWindowActive(
activeElapsed,
controller.ActiveDuration);
}
private void Awake()
{
controller = GetComponent<EnemyController>();
if (IsLancerAttack)
{
lancerVisual = GetComponent<LancerThrustVisual>();
if (lancerVisual == null)
{
lancerVisual = gameObject.AddComponent<LancerThrustVisual>();
}
return;
}
GameObject warningObject = new("Attack Warning");
warningObject.transform.SetParent(transform, false);
warningVisual = warningObject.AddComponent<AttackWarningVisual>();
}
private void Start()
{
playerHealth = FindAnyObjectByType<PlayerHealth>();
}
private void OnDestroy()
{
if (warningVisual != null)
{
Destroy(warningVisual.gameObject);
}
}
private void OnDisable()
{
EndAttack(IsLancerAttack);
}
public void BeginWarning(Vector2 direction, Vector2 capturedTarget)
{
BeginWarning(direction, capturedTarget, controller != null ? controller.WarningDuration : 0f);
}
public void BeginWarning(
Vector2 direction,
Vector2 capturedTarget,
float warningDuration)
{
lockedDirection = ResolveAttackDirection(
controller.CurrentAttackPattern.DirectionMode,
direction,
controller.FacingDirection);
targetPosition = capturedTarget;
attackOrigin = transform.position;
meleeAttackActive = false;
meleeHitConfirmed = false;
if (IsLancerAttack)
{
EnsureLancerVisual();
lancerVisual.Begin(this, controller);
warningVisual?.Hide();
return;
}
warningVisual.Show(
controller,
transform.position,
lockedDirection,
targetPosition);
if (controller.Definition.Kind == EnemyKind.Necromancer
&& controller.CurrentAttackPatternIndex == 2)
{
warningVisual.ShowTargetCircle(
targetPosition,
controller.AttackRadius * 0.72f);
}
if (controller.Definition.Kind == EnemyKind.Necromancer
&& controller.AttackShape == EnemyAttackShape.TargetCircle)
{
CreateNecromancerAoeVisuals(warningDuration);
}
}
public void Activate(float activeElapsed = 0f)
{
if (IsLancerAttack)
{
BeginLancerActive();
return;
}
warningVisual.SetActivePhase(true);
if (controller.Definition.Kind == EnemyKind.Necromancer
&& controller.CurrentAttackPatternIndex == 2)
{
ActivateNecromancerMultiAoe();
return;
}
if (IsNecrofireRayAttack())
{
BeginNecrofireRay();
return;
}
if (IsStageProjectileAttack())
{
LaunchStageProjectile();
return;
}
if (controller.AttackShape == EnemyAttackShape.Body || playerHealth == null)
{
return;
}
if (controller.AttackShape == EnemyAttackShape.TargetCircle)
{
Vector2 playerPosition = playerHealth.transform.position;
Collider2D playerCollider = GetPlayerCollider();
if (playerCollider != null)
{
playerPosition = playerCollider.bounds.center;
}
if (!ContainsPoint(playerPosition))
{
return;
}
Vector2 knockback = playerPosition - targetPosition;
if (knockback.sqrMagnitude <= 0.0001f)
{
knockback = lockedDirection;
}
playerHealth.TryTakeDamage(
controller.AttackDamage,
knockback.normalized,
controller.PlayerKnockbackDistance);
return;
}
if (!IsPersistentMeleeAttack())
{
return;
}
meleeAttackActive = true;
TryApplyMeleeHit(activeElapsed);
}
private void CreateNecromancerAoeVisuals(float warningDuration)
{
ClearStageAoeVisuals();
if (controller.CurrentAttackPatternIndex != 2)
{
stageAoeVisuals.Add(StageEnemyEffectVisual.PlayAoe(
gameObject,
targetPosition,
controller.AttackRadius,
warningDuration + controller.ActiveDuration));
return;
}
Vector2[] points = GetNecromancerMultiAoePoints();
float radius = controller.AttackRadius * 0.72f;
for (int i = 0; i < points.Length; i++)
{
stageAoeVisuals.Add(StageEnemyEffectVisual.PlayAoe(
gameObject,
points[i],
radius,
warningDuration + controller.ActiveDuration));
if (i > 0)
{
GameObject warningObject = new("Necromancer AOE Warning");
stageMultiWarnings.Add(warningObject.AddComponent<AttackWarningVisual>());
stageMultiWarnings[^1].ShowTargetCircle(points[i], radius);
}
}
}
private void ActivateNecromancerMultiAoe()
{
for (int i = 0; i < stageMultiWarnings.Count; i++)
{
stageMultiWarnings[i]?.SetActivePhase(true);
}
if (playerHealth == null)
{
playerHealth = FindAnyObjectByType<PlayerHealth>();
}
if (playerHealth == null)
{
return;
}
Vector2[] points = GetNecromancerMultiAoePoints();
float radius = controller.AttackRadius * 0.72f;
for (int i = 0; i < points.Length; i++)
{
if (ContainsCircleTarget(points[i], radius))
{
Vector2 knockback = ((Vector2)playerHealth.transform.position - points[i]).normalized;
playerHealth.TryTakeDamage(
controller.AttackDamage,
knockback,
controller.PlayerKnockbackDistance);
break;
}
}
}
private Vector2[] GetNecromancerMultiAoePoints()
{
Vector2 side = new Vector2(-lockedDirection.y, lockedDirection.x);
return new[]
{
targetPosition,
targetPosition + side * controller.AttackRadius * 1.2f,
targetPosition - side * controller.AttackRadius * 1.2f,
};
}
private bool IsStageProjectileAttack()
{
if (controller == null || controller.Definition == null)
{
return false;
}
return controller.Definition.Kind == EnemyKind.SkeletonArcher;
}
private bool IsNecrofireRayAttack()
{
return controller != null
&& controller.Definition != null
&& controller.Definition.Kind == EnemyKind.Necrofire;
}
private void BeginNecrofireRay()
{
necrofireRayActive = true;
necrofireRayHitConfirmed = false;
Vector2 direction = lockedDirection.sqrMagnitude > 0.0001f
? lockedDirection
: Vector2.right;
float length = controller.AttackLength;
necrofireRayVisual = StageEnemyEffectVisual.PlayRay(
gameObject,
attackOrigin,
direction,
length,
controller.AttackWidth,
controller.ActiveDuration);
TickNecrofireRay();
}
private void LaunchStageProjectile()
{
if (playerHealth == null)
{
playerHealth = FindAnyObjectByType<PlayerHealth>();
}
if (playerHealth == null)
{
return;
}
Vector2 direction = lockedDirection;
if (direction.sqrMagnitude <= 0.0001f)
{
direction = lockedDirection.sqrMagnitude > 0.0001f
? lockedDirection
: Vector2.right;
}
EnemyProjectile.Launch(
controller,
transform.position,
direction,
controller.AttackDamage,
controller.Definition.ProjectileSpeed,
Mathf.Max(controller.AttackLength, controller.AttackRange * 1.5f),
controller.Definition.ProjectileRadius,
false);
}
public void TickActive(float activeElapsed)
{
if (necrofireRayActive)
{
TickNecrofireRay();
}
if (meleeAttackActive && !meleeHitConfirmed)
{
// A player can enter a short melee active window after the
// first sample. Keep the check shape-limited and latch only
// after TryTakeDamage succeeds, so invulnerability does not
// consume the attack window.
TryApplyMeleeHit(activeElapsed);
}
if (!lancerAttackActive || lancerHitConfirmed || !IsLancerAttack)
{
return;
}
float activeDuration = controller.ActiveDuration;
float normalizedActive = activeDuration <= 0.0001f
? 1f
: Mathf.Clamp01(activeElapsed / activeDuration);
float collisionProgress = Mathf.Min(
normalizedActive,
LancerAttackMotion.ActiveExtensionEndFraction);
if (lancerTipNeedsRebase)
{
lancerPreviousTip = GetLancerTip(lancerPreviousProgress);
lancerTipNeedsRebase = false;
}
if (collisionProgress <= lancerPreviousProgress + 0.0001f)
{
// Keep checking the tip after a blocked attempt. Player
// invulnerability may expire before the active window ends.
if (LancerAttackMotion.IsDamageWindow(normalizedActive))
{
SweepLancerTip(lancerPreviousTip, lancerPreviousTip);
}
return;
}
Vector2 currentTip = GetLancerTip(collisionProgress);
SweepLancerTip(lancerPreviousTip, currentTip);
lancerPreviousTip = currentTip;
lancerPreviousProgress = collisionProgress;
}
public void EndAttack(bool cancelled = false)
{
necrofireRayActive = false;
necrofireRayHitConfirmed = false;
meleeAttackActive = false;
meleeHitConfirmed = false;
if (necrofireRayVisual != null)
{
Destroy(necrofireRayVisual.gameObject);
necrofireRayVisual = null;
}
lancerAttackActive = false;
lancerHitConfirmed = false;
lancerPreviousProgress = 0f;
lancerPreviousTip = Vector2.zero;
lancerTipNeedsRebase = false;
if (lancerVisual != null)
{
lancerVisual.Stop(cancelled);
}
ClearStageAoeVisuals();
for (int i = 0; i < stageMultiWarnings.Count; i++)
{
if (stageMultiWarnings[i] != null)
{
Destroy(stageMultiWarnings[i].gameObject);
}
}
stageMultiWarnings.Clear();
if (warningVisual == null)
{
return;
}
if (cancelled)
{
warningVisual.CollapseAndHide();
}
else
{
warningVisual.Hide();
}
}
private void ClearStageAoeVisuals()
{
for (int i = 0; i < stageAoeVisuals.Count; i++)
{
if (stageAoeVisuals[i] != null)
{
Destroy(stageAoeVisuals[i].gameObject);
}
}
stageAoeVisuals.Clear();
}
private void EnsureLancerVisual()
{
if (lancerVisual != null)
{
return;
}
lancerVisual = GetComponent<LancerThrustVisual>();
if (lancerVisual == null)
{
lancerVisual = gameObject.AddComponent<LancerThrustVisual>();
}
}
private void BeginLancerActive()
{
lancerAttackActive = true;
lancerHitConfirmed = false;
lancerPreviousProgress = 0f;
lancerPreviousTip = GetLancerTip(0f);
lancerTipNeedsRebase = false;
SweepLancerTip(lancerPreviousTip, lancerPreviousTip);
}
public void RebaseActiveTipAfterExternalMotion()
{
if (lancerAttackActive)
{
lancerTipNeedsRebase = true;
}
}
private Vector2 GetLancerTip(float normalizedReach)
{
float worldScale = Mathf.Abs(transform.lossyScale.x);
if (worldScale <= 0.0001f)
{
worldScale = 1f;
}
return LancerAttackMotion.GetTipPosition(
transform.position,
lockedDirection,
controller.AttackLength,
LancerAttackMotion.EvaluateActiveReachFraction(
normalizedReach),
worldScale);
}
private void SweepLancerTip(Vector2 previousTip, Vector2 currentTip)
{
if (playerHealth == null)
{
playerHealth = FindAnyObjectByType<PlayerHealth>();
}
if (playerHealth == null)
{
return;
}
float radius = Mathf.Max(0.001f, controller.AttackWidth * 0.5f);
Vector2 delta = currentTip - previousTip;
if (delta.sqrMagnitude <= 0.000001f)
{
Collider2D[] overlapHits = Physics2D.OverlapCircleAll(
currentTip,
radius);
for (int i = 0; i < overlapHits.Length; i++)
{
if (TryDamageLancerTarget(overlapHits[i]))
{
return;
}
}
return;
}
RaycastHit2D[] castHits = Physics2D.CircleCastAll(
previousTip,
radius,
delta.normalized,
delta.magnitude);
for (int i = 0; i < castHits.Length; i++)
{
if (TryDamageLancerTarget(castHits[i].collider))
{
return;
}
}
}
private bool TryDamageLancerTarget(Collider2D collider)
{
if (collider == null)
{
return false;
}
PlayerHealth health = collider.GetComponentInParent<PlayerHealth>();
if (health == null || health != playerHealth)
{
return false;
}
Vector2 knockback =
((Vector2)health.transform.position - (Vector2)transform.position);
if (knockback.sqrMagnitude <= 0.0001f)
{
knockback = lockedDirection;
}
if (!health.TryTakeDamage(
controller.AttackDamage,
knockback.normalized,
controller.PlayerKnockbackDistance))
{
return false;
}
lancerHitConfirmed = true;
return true;
}
private bool ContainsPoint(Vector2 playerPosition)
{
Vector2 origin = GetAttackOrigin(
GetAttackRootPosition(),
lockedDirection,
GetAuthoredOriginOffset(),
controller.CurrentAttackPattern.DirectionMode);
Vector2 delta = playerPosition - origin;
switch (controller.AttackShape)
{
case EnemyAttackShape.Box:
return ContainsBoxTarget(origin, delta);
case EnemyAttackShape.Cone:
return ContainsConeTarget(origin);
case EnemyAttackShape.TargetCircle:
return ContainsCircleTarget(targetPosition, controller.AttackRadius);
default:
return false;
}
}
private bool ContainsBoxTarget(Vector2 origin, Vector2 rootDelta)
{
Collider2D playerCollider = GetPlayerCollider();
if (playerCollider == null)
{
float forward = Vector2.Dot(rootDelta, lockedDirection);
float lateral = Mathf.Abs(Vector2.Dot(
rootDelta,
new Vector2(-lockedDirection.y, lockedDirection.x)));
return forward >= 0f
&& forward <= GetAttackLength()
&& lateral <= GetAttackWidth() * 0.5f;
}
Vector2 direction = lockedDirection.sqrMagnitude > 0.0001f
? lockedDirection.normalized
: Vector2.right;
Vector2 colliderCenterDelta =
(Vector2)playerCollider.bounds.center - origin;
if (Vector2.Dot(colliderCenterDelta, direction) < 0f)
{
return false;
}
float attackLength = GetAttackLength();
float attackWidth = GetAttackWidth();
Vector2 boxCenter = origin + direction * (attackLength * 0.5f);
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
Collider2D[] hits = Physics2D.OverlapBoxAll(
boxCenter,
new Vector2(attackLength, attackWidth),
angle);
return ContainsPlayerCollider(hits);
}
private bool ContainsConeTarget(Vector2 origin)
{
Collider2D playerCollider = GetPlayerCollider();
if (playerCollider == null)
{
Vector2 delta = (Vector2)playerHealth.transform.position - origin;
return delta.magnitude <= GetAttackRadius()
&& Vector2.Dot(delta.normalized, lockedDirection) >= 0.5f;
}
Vector2 direction = lockedDirection.sqrMagnitude > 0.0001f
? lockedDirection.normalized
: Vector2.right;
Vector2 centerDelta = (Vector2)playerCollider.bounds.center - origin;
if (Vector2.Dot(centerDelta, direction) < 0f)
{
return false;
}
if (controller.CurrentAttackPattern.DirectionMode
== EnemyAttackDirectionMode.HorizontalRoot
&& GetAttackWidth() > 0f
&& !ColliderIntersectsHorizontalBand(
playerCollider,
origin,
GetAttackWidth()))
{
return false;
}
if (!ColliderIntersectsCone(
playerCollider,
origin,
direction,
GetAttackRadius()))
{
return false;
}
return true;
}
private static bool ColliderIntersectsHorizontalBand(
Collider2D collider,
Vector2 origin,
float bandHeight)
{
Bounds bounds = collider.bounds;
float halfHeight = bandHeight * 0.5f;
return bounds.max.y >= origin.y - halfHeight
&& bounds.min.y <= origin.y + halfHeight;
}
private bool IsPersistentMeleeAttack()
{
if (controller == null
|| IsLancerAttack
|| IsNecrofireRayAttack()
|| IsStageProjectileAttack())
{
return false;
}
return controller.AttackShape == EnemyAttackShape.Box
|| controller.AttackShape == EnemyAttackShape.Cone;
}
private void TryApplyMeleeHit(float activeElapsed)
{
if (!meleeAttackActive || meleeHitConfirmed)
{
return;
}
if (!controller.CurrentAttackPattern.IsContactWindowActive(
activeElapsed,
controller.ActiveDuration))
{
return;
}
if (playerHealth == null)
{
playerHealth = FindAnyObjectByType<PlayerHealth>();
}
if (playerHealth == null)
{
return;
}
Collider2D playerCollider = GetPlayerCollider();
Vector2 playerPosition = playerCollider != null
? (Vector2)playerCollider.bounds.center
: playerHealth.transform.position;
if (!ContainsPoint(playerPosition))
{
return;
}
Vector2 knockback = playerPosition - (Vector2)transform.position;
if (knockback.sqrMagnitude <= 0.0001f)
{
knockback = lockedDirection;
}
if (playerHealth.TryTakeDamage(
controller.AttackDamage,
knockback.normalized,
controller.PlayerKnockbackDistance))
{
meleeHitConfirmed = true;
}
}
private bool ColliderIntersectsCone(
Collider2D collider,
Vector2 origin,
Vector2 direction,
float radius)
{
CircleCollider2D circle = collider as CircleCollider2D;
if (circle == null)
{
Vector2 closestPoint = collider.ClosestPoint(origin);
Vector2 deltaToClosest = closestPoint - origin;
return deltaToClosest.sqrMagnitude <= 0.0001f
|| Vector2.Dot(
deltaToClosest.normalized,
direction) >= 0.5f;
}
float worldRadius = circle.radius * Mathf.Max(
Mathf.Abs(circle.transform.lossyScale.x),
Mathf.Abs(circle.transform.lossyScale.y));
return CircleIntersectsCone(
(Vector2)circle.bounds.center,
worldRadius,
origin,
direction,
radius);
}
private static bool CircleIntersectsCone(
Vector2 circleCenter,
float circleRadius,
Vector2 origin,
Vector2 direction,
float radius)
{
Vector2 delta = circleCenter - origin;
float distance = delta.magnitude;
if (distance <= circleRadius)
{
return true;
}
float coneDirectionThreshold =
GameplayConstants.Current.Enemies.ConeDirectionThreshold;
if (distance <= radius + circleRadius
&& Vector2.Dot(delta / distance, direction)
>= coneDirectionThreshold)
{
return true;
}
float sideComponent = Mathf.Sqrt(
1f - coneDirectionThreshold * coneDirectionThreshold);
Vector2 side = new Vector2(-direction.y, direction.x);
Vector2 edgeDirection = direction * coneDirectionThreshold
+ side * sideComponent;
if (CircleIntersectsSegment(
circleCenter,
origin,
origin + edgeDirection * radius,
circleRadius))
{
return true;
}
edgeDirection = direction * coneDirectionThreshold
- side * sideComponent;
return CircleIntersectsSegment(
circleCenter,
origin,
origin + edgeDirection * radius,
circleRadius);
}
private static bool CircleIntersectsSegment(
Vector2 circleCenter,
Vector2 start,
Vector2 end,
float circleRadius)
{
Vector2 segment = end - start;
float segmentLengthSquared = segment.sqrMagnitude;
float t = segmentLengthSquared <= 0.000001f
? 0f
: Mathf.Clamp01(Vector2.Dot(
circleCenter - start,
segment) / segmentLengthSquared);
Vector2 closest = start + segment * t;
return (circleCenter - closest).sqrMagnitude
<= circleRadius * circleRadius + 0.0001f;
}
private bool ContainsCircleTarget(Vector2 center, float radius)
{
Collider2D[] hits = Physics2D.OverlapCircleAll(center, radius);
return ContainsPlayerCollider(hits);
}
private bool ContainsPlayerCollider(Collider2D[] hits)
{
Collider2D playerCollider = GetPlayerCollider();
if (playerCollider == null || hits == null)
{
return false;
}
for (int i = 0; i < hits.Length; i++)
{
if (hits[i] == playerCollider
|| hits[i].GetComponentInParent<PlayerHealth>() == playerHealth)
{
return true;
}
}
return false;
}
private Collider2D GetPlayerCollider()
{
return playerHealth != null
? playerHealth.GetComponent<Collider2D>()
: null;
}
private void TickNecrofireRay()
{
if (!necrofireRayActive || necrofireRayHitConfirmed)
{
return;
}
if (playerHealth == null)
{
playerHealth = FindAnyObjectByType<PlayerHealth>();
}
if (playerHealth == null)
{
return;
}
Vector2 direction = lockedDirection.sqrMagnitude > 0.0001f
? lockedDirection
: Vector2.right;
float length = controller.AttackLength;
float width = Mathf.Max(0.001f, controller.AttackWidth);
Vector2 center = attackOrigin + direction * (length * 0.5f);
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
Collider2D[] overlapHits = Physics2D.OverlapBoxAll(
center,
new Vector2(length, width),
angle);
for (int i = 0; i < overlapHits.Length; i++)
{
Collider2D collider = overlapHits[i];
if (collider == null)
{
continue;
}
PlayerHealth health = collider.GetComponentInParent<PlayerHealth>();
if (health == null || health != playerHealth)
{
continue;
}
Vector2 delta = (Vector2)health.transform.position - attackOrigin;
Vector2 knockback = delta.sqrMagnitude > 0.0001f
? delta.normalized
: direction;
if (health.TryTakeDamage(
controller.AttackDamage,
knockback,
controller.PlayerKnockbackDistance))
{
necrofireRayHitConfirmed = true;
return;
}
}
}
}
}