1023 lines
34 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|