527 lines
19 KiB
C#
527 lines
19 KiB
C#
using System.Collections.Generic;
|
|
using BumpCombat.Core;
|
|
using BumpCombat.Constants;
|
|
using BumpCombat.Enemies;
|
|
using BumpCombat.Player;
|
|
using UnityEngine;
|
|
|
|
namespace BumpCombat.Combat
|
|
{
|
|
[RequireComponent(typeof(PlayerController))]
|
|
[RequireComponent(typeof(PlayerStats))]
|
|
public sealed class BumpCombatResolver : MonoBehaviour
|
|
{
|
|
private float baseDamage => GameplayConstants.Current.Combat.BaseDamage;
|
|
private float dashDamage => GameplayConstants.Current.Combat.DashDamage;
|
|
private float strongDashDamage => GameplayConstants.Current.Combat.StrongDashDamage;
|
|
private float minimumAttackSpeed => GameplayConstants.Current.Combat.MinimumAttackSpeed;
|
|
private float minimumApproachDot => GameplayConstants.Current.Combat.MinimumApproachDot;
|
|
private float knockbackForce => GameplayConstants.Current.Combat.KnockbackForce;
|
|
private float dashKnockbackForce => GameplayConstants.Current.Combat.DashKnockbackForce;
|
|
private float strongDashKnockbackForce => GameplayConstants.Current.Combat.StrongDashKnockbackForce;
|
|
private float strongDashLaunchHeight => GameplayConstants.Current.Combat.StrongDashLaunchHeight;
|
|
private float strongDashLaunchDuration => GameplayConstants.Current.Combat.StrongDashLaunchDuration;
|
|
private float playerImpactRecoilDistance => GameplayConstants.Current.Combat.PlayerImpactRecoilDistance;
|
|
private float frontKnockbackMultiplier => GameplayConstants.Current.Combat.FrontKnockbackMultiplier;
|
|
private float frontPlayerImpactRecoilDistance => GameplayConstants.Current.Combat.FrontPlayerImpactRecoilDistance;
|
|
private float eventBackPlayerImpactRecoilDistance => GameplayConstants.Current.Combat.EventBackPlayerImpactRecoilDistance;
|
|
private float shieldedPlayerImpactRecoilDistance => GameplayConstants.Current.Combat.ShieldedPlayerImpactRecoilDistance;
|
|
private float minimumOffsetDistance => GameplayConstants.Current.Combat.MinimumOffsetDistance;
|
|
private float normalHitInterval => GameplayConstants.Current.Combat.NormalHitInterval;
|
|
private float dashContactSkin => GameplayConstants.Current.Combat.DashContactSkin;
|
|
|
|
private readonly HashSet<int> dashTargets = new();
|
|
private readonly RaycastHit2D[] dashHits = new RaycastHit2D[64];
|
|
private readonly Dictionary<int, float> nextNormalHitTimes = new();
|
|
|
|
private PlayerStats playerStats;
|
|
private PlayerController playerController;
|
|
private DashController dashController;
|
|
private ActiveArtifactController activeArtifactController;
|
|
private CircleCollider2D bodyCollider;
|
|
|
|
private void Awake()
|
|
{
|
|
playerStats = GetComponent<PlayerStats>();
|
|
playerController = GetComponent<PlayerController>();
|
|
dashController = GetComponent<DashController>();
|
|
activeArtifactController = GetComponent<ActiveArtifactController>();
|
|
bodyCollider = GetComponent<CircleCollider2D>();
|
|
}
|
|
|
|
public void BeginDashPathContactSession()
|
|
{
|
|
dashTargets.Clear();
|
|
}
|
|
|
|
private void OnCollisionEnter2D(Collision2D collision)
|
|
{
|
|
TryResolveNormalCollision(collision);
|
|
}
|
|
|
|
private void OnCollisionStay2D(Collision2D collision)
|
|
{
|
|
TryResolveNormalCollision(collision);
|
|
}
|
|
|
|
private void OnTriggerEnter2D(Collider2D other)
|
|
{
|
|
TryResolveCrowdTrigger(other);
|
|
}
|
|
|
|
private void OnTriggerStay2D(Collider2D other)
|
|
{
|
|
TryResolveCrowdTrigger(other);
|
|
}
|
|
|
|
private void TryResolveNormalCollision(
|
|
Collision2D collision)
|
|
{
|
|
EnemyController enemy = collision.collider.GetComponentInParent<EnemyController>();
|
|
if (enemy == null || enemy.IsCrowd)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vector2 hitPosition = collision.contactCount > 0
|
|
? collision.GetContact(0).point
|
|
: Vector2.Lerp(transform.position, enemy.transform.position, 0.5f);
|
|
TryResolveNormalContact(enemy, hitPosition);
|
|
}
|
|
|
|
private void TryResolveCrowdTrigger(
|
|
Collider2D other)
|
|
{
|
|
EnemyController enemy = other.GetComponentInParent<EnemyController>();
|
|
if (enemy == null || !enemy.IsCrowd)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vector2 hitPosition = Vector2.Lerp(
|
|
transform.position,
|
|
enemy.transform.position,
|
|
0.5f);
|
|
TryResolveNormalContact(enemy, hitPosition);
|
|
}
|
|
|
|
private void TryResolveNormalContact(
|
|
EnemyController enemy,
|
|
Vector2 hitPosition)
|
|
{
|
|
if (!RunManager.GameplayInputEnabled
|
|
|| playerController.IsHurtMovementLocked
|
|
|| playerController.IsDamageKnockbackActive
|
|
|| dashController.IsDashing
|
|
|| (activeArtifactController != null
|
|
&& activeArtifactController.LocksMovement))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (enemy.IsDead)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (nextNormalHitTimes.TryGetValue(
|
|
enemy.GetInstanceID(),
|
|
out float nextNormalHitTime)
|
|
&& Time.time < nextNormalHitTime)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vector2 toEnemy = ((Vector2)enemy.transform.position - (Vector2)transform.position).normalized;
|
|
if (playerController.AttackIntentVelocity.magnitude < minimumAttackSpeed
|
|
|| playerController.MoveDirection.sqrMagnitude <= 0f
|
|
|| Vector2.Dot(playerController.MoveDirection, toEnemy) < minimumApproachDot)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vector2 enemyToPlayer =
|
|
((Vector2)transform.position - (Vector2)enemy.transform.position).normalized;
|
|
HitSide side = BumpCombatMath.ClassifySide(
|
|
enemy.FacingDirection,
|
|
enemyToPlayer);
|
|
if (enemy.IsGroggyTierGated && enemy.IsDamageInvulnerable)
|
|
{
|
|
playerController.ApplyImpactRecoil(
|
|
enemyToPlayer,
|
|
shieldedPlayerImpactRecoilDistance);
|
|
nextNormalHitTimes[enemy.GetInstanceID()] =
|
|
Time.time + normalHitInterval;
|
|
CombatFeedback.ShowBlockedBump(
|
|
gameObject,
|
|
hitPosition,
|
|
playerController.MoveDirection);
|
|
return;
|
|
}
|
|
|
|
ResolveEnemy(
|
|
enemy,
|
|
false,
|
|
hitPosition,
|
|
transform.position,
|
|
playerController.MoveDirection,
|
|
side);
|
|
}
|
|
|
|
public void ResolveDashPath(
|
|
Vector2 start,
|
|
Vector2 direction,
|
|
float distance,
|
|
bool isStrongDash = false,
|
|
float? damageOverride = null,
|
|
float? knockbackOverride = null,
|
|
string sourceId = null,
|
|
DamageTag damageTag = DamageTag.Collision,
|
|
ArtifactColor? artifactColor = null,
|
|
int castIdentity = 0,
|
|
ActiveArtifactEffect? artifactEffect = null)
|
|
{
|
|
ResolveDashPathInternal(
|
|
start,
|
|
direction,
|
|
distance,
|
|
isStrongDash,
|
|
damageOverride,
|
|
knockbackOverride,
|
|
sourceId,
|
|
damageTag,
|
|
artifactColor,
|
|
castIdentity,
|
|
artifactEffect,
|
|
true);
|
|
}
|
|
|
|
public void ResolveDashPathSegment(
|
|
Vector2 start,
|
|
Vector2 direction,
|
|
float distance,
|
|
bool isStrongDash = false,
|
|
float? damageOverride = null,
|
|
float? knockbackOverride = null,
|
|
string sourceId = null,
|
|
DamageTag damageTag = DamageTag.Collision,
|
|
ArtifactColor? artifactColor = null,
|
|
int castIdentity = 0,
|
|
ActiveArtifactEffect? artifactEffect = null)
|
|
{
|
|
ResolveDashPathInternal(
|
|
start,
|
|
direction,
|
|
distance,
|
|
isStrongDash,
|
|
damageOverride,
|
|
knockbackOverride,
|
|
sourceId,
|
|
damageTag,
|
|
artifactColor,
|
|
castIdentity,
|
|
artifactEffect,
|
|
false);
|
|
}
|
|
|
|
private void ResolveDashPathInternal(
|
|
Vector2 start,
|
|
Vector2 direction,
|
|
float distance,
|
|
bool isStrongDash,
|
|
float? damageOverride,
|
|
float? knockbackOverride,
|
|
string sourceId,
|
|
DamageTag damageTag,
|
|
ArtifactColor? artifactColor,
|
|
int castIdentity,
|
|
ActiveArtifactEffect? artifactEffect,
|
|
bool resetDashTargets)
|
|
{
|
|
if (resetDashTargets)
|
|
{
|
|
dashTargets.Clear();
|
|
}
|
|
|
|
Vector2 castStart = start;
|
|
float radius = GameplayConstants.Current.Combat.DashFallbackCastRadius;
|
|
if (bodyCollider != null)
|
|
{
|
|
Vector2 colliderOffset =
|
|
(Vector2)bodyCollider.bounds.center - (Vector2)transform.position;
|
|
castStart += colliderOffset;
|
|
radius = Mathf.Max(
|
|
bodyCollider.bounds.extents.x,
|
|
bodyCollider.bounds.extents.y)
|
|
+ dashContactSkin;
|
|
}
|
|
|
|
int hitCount = Physics2D.CircleCast(
|
|
castStart,
|
|
radius,
|
|
direction,
|
|
ContactFilter2D.noFilter,
|
|
dashHits,
|
|
distance);
|
|
|
|
for (int i = 0; i < hitCount; i++)
|
|
{
|
|
EnemyController enemy = dashHits[i].collider.GetComponentInParent<EnemyController>();
|
|
if (enemy == null || enemy.IsDead || !dashTargets.Add(enemy.GetInstanceID()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Vector2 hitPosition = dashHits[i].point;
|
|
if (hitPosition == Vector2.zero)
|
|
{
|
|
hitPosition = dashHits[i].collider.ClosestPoint(
|
|
castStart + direction * dashHits[i].distance);
|
|
}
|
|
Vector2 attackerPositionAtHit =
|
|
castStart + direction * dashHits[i].distance;
|
|
ResolveEnemy(
|
|
enemy,
|
|
true,
|
|
hitPosition,
|
|
attackerPositionAtHit,
|
|
direction,
|
|
null,
|
|
isStrongDash,
|
|
damageOverride,
|
|
knockbackOverride,
|
|
sourceId,
|
|
damageTag,
|
|
artifactColor,
|
|
castIdentity,
|
|
artifactEffect);
|
|
}
|
|
}
|
|
|
|
private bool ResolveEnemy(
|
|
EnemyController enemy,
|
|
bool isDash,
|
|
Vector2 hitPosition,
|
|
Vector2 attackerPosition,
|
|
Vector2 attackDirection,
|
|
HitSide? knownSide = null,
|
|
bool isStrongDash = false,
|
|
float? damageOverride = null,
|
|
float? knockbackOverride = null,
|
|
string sourceId = null,
|
|
DamageTag damageTag = DamageTag.Collision,
|
|
ArtifactColor? artifactColor = null,
|
|
int castIdentity = 0,
|
|
ActiveArtifactEffect? artifactEffect = null)
|
|
{
|
|
Vector2 enemyToPlayer =
|
|
(attackerPosition - (Vector2)enemy.transform.position).normalized;
|
|
HitSide side = knownSide
|
|
?? BumpCombatMath.ClassifySide(enemy.FacingDirection, enemyToPlayer);
|
|
bool isEventEnemy = enemy.IsEventEnemy;
|
|
bool isOffsetHit = !isDash && BumpCombatMath.IsOffsetHit(
|
|
side,
|
|
attackDirection,
|
|
attackerPosition,
|
|
enemy.transform.position,
|
|
minimumOffsetDistance);
|
|
float rawDamage = damageOverride
|
|
?? (isStrongDash
|
|
? strongDashDamage
|
|
: isDash
|
|
? dashDamage
|
|
: baseDamage);
|
|
float finalDamage = BumpCombatMath.ApplySideMultiplier(
|
|
playerStats.CalculateDamage(rawDamage, damageTag),
|
|
side,
|
|
isOffsetHit);
|
|
Vector2 knockbackDirection = -enemyToPlayer;
|
|
bool suppressOrdinaryEventReaction = !isDash && isEventEnemy;
|
|
float appliedKnockback = knockbackOverride
|
|
?? (isStrongDash
|
|
? strongDashKnockbackForce
|
|
: isDash
|
|
? dashKnockbackForce
|
|
: side == HitSide.Front
|
|
? knockbackForce * frontKnockbackMultiplier
|
|
: knockbackForce);
|
|
if (suppressOrdinaryEventReaction)
|
|
{
|
|
appliedKnockback = 0f;
|
|
}
|
|
|
|
EnemyArtifactHitResult artifactHitResult =
|
|
EnemyArtifactHitResult.Rejected;
|
|
bool acceptedHit;
|
|
if (artifactColor.HasValue)
|
|
{
|
|
artifactHitResult = enemy.TryTakeArtifactHit(
|
|
finalDamage,
|
|
knockbackDirection,
|
|
appliedKnockback,
|
|
artifactColor.Value,
|
|
castIdentity,
|
|
!suppressOrdinaryEventReaction);
|
|
acceptedHit = artifactHitResult
|
|
!= EnemyArtifactHitResult.Rejected;
|
|
}
|
|
else
|
|
{
|
|
acceptedHit = enemy.TryTakeDamage(
|
|
finalDamage,
|
|
knockbackDirection,
|
|
appliedKnockback,
|
|
!suppressOrdinaryEventReaction,
|
|
!isDash);
|
|
}
|
|
|
|
if (!acceptedHit)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (artifactHitResult == EnemyArtifactHitResult.Shielded)
|
|
{
|
|
if (enemy.LastArtifactContactCounted
|
|
&& artifactEffect.HasValue)
|
|
{
|
|
CombatEvents.RaiseArtifactEffectiveHit(
|
|
enemy.gameObject,
|
|
castIdentity,
|
|
artifactEffect.Value,
|
|
isStrongDash,
|
|
true);
|
|
CombatFeedback.ShowArtifactImpact(
|
|
CombatHitResult.ForArtifactContact(
|
|
gameObject,
|
|
enemy.gameObject,
|
|
artifactEffect.Value,
|
|
isStrongDash,
|
|
side,
|
|
knockbackDirection,
|
|
hitPosition,
|
|
damageTag,
|
|
sourceId,
|
|
isOffsetHit));
|
|
}
|
|
|
|
// The contact has already advanced the groggy stagger pip.
|
|
// A shielded dash must not become a bump, interrupt, launch,
|
|
// recoil, gauge charge, or damage-hit feedback.
|
|
return false;
|
|
}
|
|
|
|
float damageTaken = enemy.LastAppliedDamage;
|
|
if (damageTaken <= 0f)
|
|
{
|
|
return false;
|
|
}
|
|
if (artifactColor.HasValue && artifactEffect.HasValue)
|
|
{
|
|
CombatEvents.RaiseArtifactEffectiveHit(
|
|
enemy.gameObject,
|
|
castIdentity,
|
|
artifactEffect.Value,
|
|
isStrongDash,
|
|
false);
|
|
}
|
|
float reportedDamage = enemy.LastReportedDamage;
|
|
|
|
bool protectionBegan = enemy.IsDamageInvulnerable;
|
|
if (protectionBegan)
|
|
{
|
|
appliedKnockback = 0f;
|
|
}
|
|
|
|
if (!isDash)
|
|
{
|
|
playerController.TryPlayBumpAttackAnimation(
|
|
side == HitSide.Back);
|
|
}
|
|
|
|
if (!isDash)
|
|
{
|
|
nextNormalHitTimes[enemy.GetInstanceID()] =
|
|
Time.time + normalHitInterval;
|
|
}
|
|
|
|
bool launchSucceeded = false;
|
|
if (!protectionBegan && isStrongDash)
|
|
{
|
|
enemy.TryInterruptAttackFromLaunch();
|
|
if (CanLaunchAfterHit(enemy.CanBeLaunched, enemy.IsDead))
|
|
{
|
|
launchSucceeded = true;
|
|
enemy.PlayLaunchVisual(
|
|
knockbackDirection,
|
|
strongDashLaunchHeight,
|
|
strongDashLaunchDuration);
|
|
}
|
|
else if (ShouldShowLaunchResist(enemy.CanBeLaunched))
|
|
{
|
|
CombatEvents.RaiseLaunchResisted(enemy.gameObject);
|
|
}
|
|
}
|
|
else if (!protectionBegan && side == HitSide.Back)
|
|
{
|
|
bool broke = enemy.TryInterruptAttackFromBackHit();
|
|
if (broke && !isDash)
|
|
{
|
|
CombatEvents.RaiseRearAttackCancelled(enemy.gameObject);
|
|
}
|
|
if (suppressOrdinaryEventReaction && broke)
|
|
{
|
|
enemy.PlayHurtAnimation();
|
|
}
|
|
}
|
|
|
|
if (!isDash && !protectionBegan)
|
|
{
|
|
if (!enemy.IsCrowd
|
|
&& BumpCombatMath.AppliesPlayerImpactRecoil(side, isEventEnemy))
|
|
{
|
|
playerController.ApplyImpactRecoil(
|
|
enemyToPlayer,
|
|
side == HitSide.Front
|
|
? frontPlayerImpactRecoilDistance
|
|
: side == HitSide.Back && isEventEnemy
|
|
? eventBackPlayerImpactRecoilDistance
|
|
: playerImpactRecoilDistance);
|
|
}
|
|
activeArtifactController?.AddNormalHitCharge();
|
|
}
|
|
|
|
CombatEvents.RaiseValidHit(new CombatHitResult(
|
|
gameObject,
|
|
enemy.gameObject,
|
|
isDash,
|
|
side,
|
|
reportedDamage,
|
|
knockbackDirection,
|
|
appliedKnockback,
|
|
hitPosition,
|
|
isOffsetHit,
|
|
isStrongDash,
|
|
damageTag,
|
|
sourceId,
|
|
true,
|
|
true,
|
|
launchSucceeded,
|
|
artifactEffect.HasValue,
|
|
artifactEffect.GetValueOrDefault(),
|
|
artifactEffect.HasValue && isStrongDash));
|
|
return true;
|
|
}
|
|
|
|
public static bool CanLaunchAfterHit(bool canBeLaunched, bool isDead)
|
|
{
|
|
return canBeLaunched && !isDead;
|
|
}
|
|
|
|
public static bool ShouldShowLaunchResist(bool canBeLaunched)
|
|
{
|
|
return !canBeLaunched;
|
|
}
|
|
}
|
|
}
|